驗證
理解驗證對於保護應用程式的資料至關重要。本頁面將引導您瞭解使用 React 和 Next.js 的哪些功能來實作驗證。
在開始之前,將流程分解為三個概念會有所幫助
- 驗證 (Authentication):驗證使用者是否為他們聲稱的身分。這需要使用者提供身分證明,例如使用者名稱和密碼。
- 工作階段管理 (Session Management):追蹤使用者跨請求的驗證狀態。
- 授權 (Authorization):決定使用者可以存取哪些路由和資料。
此圖表顯示使用 React 和 Next.js 功能的驗證流程


本頁面上的範例逐步介紹了用於教育目的的基本使用者名稱和密碼驗證。雖然您可以實作自訂驗證解決方案,但為了提高安全性和簡化性,我們建議使用驗證函式庫。這些函式庫為驗證、工作階段管理和授權提供內建解決方案,以及社交登入、多因素驗證和基於角色的存取控制等額外功能。您可以在驗證函式庫章節中找到清單。
驗證
註冊和登入功能
您可以將 <form>
元素與 React 的 伺服器行為 和 useActionState
結合使用,以擷取使用者憑證、驗證表單欄位,並呼叫您的驗證提供者的 API 或資料庫。
由於伺服器行為始終在伺服器上執行,因此它們為處理驗證邏輯提供了安全的環境。
以下是實作註冊/登入功能的步驟
1. 擷取使用者憑證
若要擷取使用者憑證,請建立一個表單,該表單在提交時調用伺服器行為。例如,一個註冊表單,接受使用者的姓名、電子郵件和密碼
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
export async function signup(formData: FormData) {}
2. 在伺服器上驗證表單欄位
使用伺服器行為在伺服器上驗證表單欄位。如果您的驗證提供者未提供表單驗證,您可以使用架構驗證函式庫,例如 Zod 或 Yup。
以 Zod 為例,您可以定義一個包含適當錯誤訊息的表單架構
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
export type FormState =
| {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
| undefined
為了防止不必要地呼叫驗證提供者的 API 或資料庫,如果任何表單欄位與定義的架構不符,您可以在伺服器行為中儘早 return
。
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
export async function signup(state: FormState, formData: FormData) {
// Validate form fields
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Call the provider or db to create a user...
}
回到您的 <SignupForm />
,您可以使用 React 的 useActionState
Hook 在表單提交時顯示驗證錯誤
'use client'
import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'
export default function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button disabled={pending} type="submit">
Sign Up
</button>
</form>
)
}
要知道
- 在 React 19 中,
useFormStatus
在傳回的物件上包含額外的鍵,例如 data、method 和 action。如果您未使用 React 19,則僅pending
鍵可用。- 在變更資料之前,您應始終確保使用者也已獲得執行操作的授權。請參閱驗證和授權。
3. 建立使用者或檢查使用者憑證
驗證表單欄位後,您可以呼叫驗證提供者的 API 或資料庫,以建立新的使用者帳戶或檢查使用者是否存在。
繼續之前的範例
export async function signup(state: FormState, formData: FormData) {
// 1. Validate form fields
// ...
// 2. Prepare data for insertion into database
const { name, email, password } = validatedFields.data
// e.g. Hash the user's password before storing it
const hashedPassword = await bcrypt.hash(password, 10)
// 3. Insert the user into the database or call an Auth Library's API
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'An error occurred while creating your account.',
}
}
// TODO:
// 4. Create user session
// 5. Redirect user
}
成功建立使用者帳戶或驗證使用者憑證後,您可以建立工作階段來管理使用者的驗證狀態。根據您的工作階段管理策略,工作階段可以儲存在 Cookie 或資料庫中,或兩者都儲存。繼續工作階段管理章節以瞭解更多資訊。
提示
- 上面的範例很冗長,因為它為了教育目的而分解了驗證步驟。這突顯了實作您自己的安全解決方案可能很快變得複雜。考慮使用驗證函式庫來簡化流程。
- 為了改善使用者體驗,您可能希望在註冊流程的早期檢查重複的電子郵件或使用者名稱。例如,當使用者輸入使用者名稱或輸入欄位失去焦點時。這有助於防止不必要的表單提交,並為使用者提供即時回饋。您可以使用諸如 use-debounce 等函式庫來管理這些檢查的頻率,以進行請求防抖。
工作階段管理
工作階段管理確保使用者的驗證狀態在跨請求時保持不變。它涉及建立、儲存、重新整理和刪除工作階段或權杖。
有兩種工作階段類型
- 無狀態:工作階段資料 (或權杖) 儲存在瀏覽器的 Cookie 中。Cookie 會隨每個請求一起傳送,以便在伺服器上驗證工作階段。此方法更簡單,但如果實作不正確,則可能安全性較低。
- 資料庫:工作階段資料儲存在資料庫中,使用者的瀏覽器僅接收加密的工作階段 ID。此方法更安全,但可能很複雜且使用更多伺服器資源。
要知道: 雖然您可以使用任一方法或兩種方法都使用,但我們建議使用工作階段管理函式庫,例如 iron-session 或 Jose。
無狀態工作階段
若要建立和管理無狀態工作階段,您需要遵循幾個步驟
除了上述內容外,請考慮新增功能,以便在使用者返回應用程式時更新 (或重新整理)工作階段,並在使用者登出時刪除工作階段。
要知道: 檢查您的 驗證函式庫 是否包含工作階段管理。
1. 產生密鑰
您可以透過多種方式產生密鑰來簽署您的工作階段。例如,您可以選擇在終端機中使用 openssl
命令
openssl rand -base64 32
此命令會產生一個 32 個字元的隨機字串,您可以將其用作密鑰並儲存在您的環境變數檔案中
SESSION_SECRET=your_secret_key
然後,您可以在工作階段管理邏輯中參考此金鑰
const secretKey = process.env.SESSION_SECRET
2. 加密和解密工作階段
接下來,您可以使用您偏好的工作階段管理函式庫來加密和解密工作階段。繼續之前的範例,我們將使用 Jose (與 Edge Runtime 相容) 和 React 的 server-only
套件,以確保您的工作階段管理邏輯僅在伺服器上執行。
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload: SessionPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session: string | undefined = '') {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Failed to verify session')
}
}
提示:
- payload 應包含最少、唯一的使用者資料,這些資料將在後續請求中使用,例如使用者的 ID、角色等。它不應包含個人身分識別資訊,例如電話號碼、電子郵件地址、信用卡資訊等,或密碼等敏感資料。
3. 設定 Cookie (建議選項)
若要將工作階段儲存在 Cookie 中,請使用 Next.js cookies
API。Cookie 應在伺服器上設定,並包含建議的選項
- HttpOnly:防止用戶端 JavaScript 存取 Cookie。
- Secure:使用 https 傳送 Cookie。
- SameSite:指定是否可以將 Cookie 與跨網站請求一起傳送。
- Max-Age 或 Expires:在一段時間後刪除 Cookie。
- Path:定義 Cookie 的 URL 路徑。
請參閱 MDN 以取得有關這些選項中每個選項的更多資訊。
import 'server-only'
import { cookies } from 'next/headers'
export async function createSession(userId: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, expiresAt })
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
回到您的伺服器行為,您可以調用 createSession()
函數,並使用 redirect()
API 將使用者重新導向到適當的頁面
import { createSession } from '@/app/lib/session'
export async function signup(state: FormState, formData: FormData) {
// Previous steps:
// 1. Validate form fields
// 2. Prepare data for insertion into database
// 3. Insert the user into the database or call an Library API
// Current steps:
// 4. Create user session
await createSession(user.id)
// 5. Redirect user
redirect('/profile')
}
提示:
- Cookie 應在伺服器上設定,以防止用戶端篡改。
- 🎥 觀看:瞭解有關使用 Next.js 的無狀態工作階段和驗證的更多資訊 → YouTube (11 分鐘)。
更新 (或重新整理) 工作階段
您也可以延長工作階段的到期時間。這對於在使用者再次存取應用程式後保持使用者登入狀態很有用。例如
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export async function updateSession() {
const session = (await cookies()).get('session')?.value
const payload = await decrypt(session)
if (!session || !payload) {
return null
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
提示: 檢查您的驗證函式庫是否支援重新整理權杖,重新整理權杖可用於延長使用者的工作階段。
刪除工作階段
若要刪除工作階段,您可以刪除 Cookie
import 'server-only'
import { cookies } from 'next/headers'
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}
然後您可以在您的應用程式中重複使用 deleteSession()
函數,例如,在登出時
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
deleteSession()
redirect('/login')
}
資料庫工作階段
若要建立和管理資料庫工作階段,您需要遵循以下步驟
- 在資料庫中建立一個表格以儲存工作階段和資料 (或檢查您的驗證函式庫是否處理此問題)。
- 實作插入、更新和刪除工作階段的功能
- 在將工作階段 ID 儲存在使用者的瀏覽器中之前對其進行加密,並確保資料庫和 Cookie 保持同步 (這是選用的,但建議用於中介層中的樂觀驗證檢查)。
例如
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
export async function createSession(id: number) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
// 1. Create a session in the database
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// Return the session ID
.returning({ id: sessions.id })
const sessionId = data[0].id
// 2. Encrypt the session ID
const session = await encrypt({ sessionId, expiresAt })
// 3. Store the session in cookies for optimistic auth checks
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
提示:
- 為了更快地檢索資料,請考慮使用像 Vercel Redis 這樣的資料庫。但是,您也可以將工作階段資料保留在您的主要資料庫中,並組合資料請求以減少查詢次數。
- 您可以選擇使用資料庫工作階段來處理更進階的使用案例,例如追蹤使用者上次登入的時間或活動裝置的數量,或讓使用者能夠登出所有裝置。
在實作工作階段管理後,您需要新增授權邏輯,以控制使用者可以在應用程式中存取和執行的操作。繼續授權章節以瞭解更多資訊。
授權
一旦使用者通過驗證並建立工作階段,您就可以實作授權來控制使用者可以在您的應用程式中存取和執行的操作。
有兩種主要的授權檢查類型
- 樂觀:檢查使用者是否已獲得授權,可以使用儲存在 Cookie 中的工作階段資料來存取路由或執行操作。這些檢查適用於快速操作,例如顯示/隱藏 UI 元素或根據權限或角色重新導向使用者。
- 安全:檢查使用者是否已獲得授權,可以使用儲存在資料庫中的工作階段資料來存取路由或執行操作。這些檢查更安全,用於需要存取敏感資料或操作的操作。
對於這兩種情況,我們建議
- 建立資料存取層以集中您的授權邏輯
- 使用資料傳輸物件 (DTO)僅傳回必要的資料
- 選擇性地使用中介層來執行樂觀檢查。
使用中介層進行樂觀檢查 (選用)
在某些情況下,您可能想要使用中介層並根據權限重新導向使用者
- 執行樂觀檢查。由於中介層在每個路由上執行,因此它是集中重新導向邏輯和預先篩選未經授權使用者的好方法。
- 保護在使用者之間共用資料的靜態路由 (例如,付費牆後面的內容)。
然而,由於 Middleware 會在每個路由上執行,包括預先載入(prefetch)的路由,因此務必僅從 Cookie 讀取 session(樂觀檢查),並避免資料庫檢查,以防止效能問題。
例如
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
// 1. Specify protected and public routes
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req: NextRequest) {
// 2. Check if the current route is protected or public
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. Decrypt the session from the cookie
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 4. Redirect to /login if the user is not authenticated
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 5. Redirect to /dashboard if the user is authenticated
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// Routes Middleware should not run on
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
雖然 Middleware 對於初始檢查可能很有用,但不應作為保護資料的唯一防線。大多數的安全檢查應盡可能靠近您的資料來源執行,詳情請參閱資料存取層。
提示:
- 在 Middleware 中,您也可以使用
req.cookies.get('session').value
讀取 Cookie。- Middleware 使用 Edge Runtime,請檢查您的 Auth library 和 session 管理 library 是否相容。
- 您可以使用 Middleware 中的
matcher
屬性來指定 Middleware 應在哪些路由上執行。雖然對於身份驗證,建議 Middleware 在所有路由上執行。
建立資料存取層 (DAL)
我們建議建立 DAL 以集中管理您的資料請求和授權邏輯。
DAL 應包含一個函式,用於驗證使用者與應用程式互動時的 session。至少,該函式應檢查 session 是否有效,然後重新導向或傳回使用者資訊,以進行後續請求。
例如,為您的 DAL 建立一個單獨的檔案,其中包含一個 verifySession()
函式。然後使用 React 的 cache API,在 React render pass 期間記憶(memoize)該函式的傳回值
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session?.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId }
})
然後,您可以在資料請求、Server Actions、Route Handlers 中調用 verifySession()
函式
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// Explicitly return the columns you need rather than the whole user object
columns: {
id: true,
name: true,
email: true,
},
})
const user = data[0]
return user
} catch (error) {
console.log('Failed to fetch user')
return null
}
})
提示:
- DAL 可用於保護在請求時提取的資料。但是,對於在使用者之間共享資料的靜態路由,資料將在建置時提取,而不是在請求時提取。使用Middleware來保護靜態路由。
- 為了安全檢查,您可以透過將 session ID 與您的資料庫進行比較,來檢查 session 是否有效。使用 React 的 cache 函式,以避免在 render pass 期間向資料庫發出不必要的重複請求。
- 您可能希望在 JavaScript 類別中整合相關的資料請求,該類別在任何方法之前執行
verifySession()
。
使用資料傳輸物件 (DTO)
在檢索資料時,建議您僅傳回應用程式中將使用的必要資料,而不是整個物件。例如,如果您要提取使用者資料,您可能只傳回使用者的 ID 和名稱,而不是可能包含密碼、電話號碼等的整個使用者物件。
但是,如果您無法控制傳回的資料結構,或者在團隊中工作,想要避免將整個物件傳遞給客戶端,您可以使用諸如指定哪些欄位可以安全地暴露給客戶端的策略。
import 'server-only'
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer: User) {
return true
}
function canSeePhoneNumber(viewer: User, team: string) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug: string) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Return specific columns here
})
const user = data[0]
const currentUser = await getUser(user.id)
// Or return only what's specific to the query here
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}
透過將您的資料請求和授權邏輯集中在 DAL 中,並使用 DTO,您可以確保所有資料請求都是安全且一致的,從而更輕鬆地維護、稽核和除錯隨著應用程式擴展。
要知道:
- 定義 DTO 有幾種不同的方法,從使用
toJSON()
,到像上面範例中的個別函式,或 JS 類別。由於這些是 JavaScript 模式,而不是 React 或 Next.js 功能,我們建議您進行一些研究,找到最適合您應用程式的模式。- 在我們的Next.js 安全性文章中了解更多關於安全性最佳實踐的資訊。
Server Components
Server Components 中的身份驗證檢查對於基於角色的存取非常有用。例如,根據使用者的角色有條件地渲染元件
import { verifySession } from '@/app/lib/dal'
export default function Dashboard() {
const session = await verifySession()
const userRole = session?.user?.role // Assuming 'role' is part of the session object
if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}
在範例中,我們使用 DAL 中的 verifySession()
函式來檢查 'admin'、'user' 和未授權的角色。此模式確保每個使用者僅與適合其角色的元件互動。
Layouts 和身份驗證檢查
由於部分渲染,在 Layouts 中進行檢查時要謹慎,因為這些不會在導航時重新渲染,這表示使用者 session 不會在每次路由變更時進行檢查。
相反地,您應該在靠近資料來源或將有條件渲染的元件附近進行檢查。
例如,考慮一個共享的 layout,它提取使用者資料並在導航欄中顯示使用者圖像。您應該在 layout 中提取使用者資料 (getUser()
),並在您的 DAL 中進行身份驗證檢查,而不是在 layout 中進行身份驗證檢查。
這保證了無論在應用程式中的何處調用 getUser()
,都會執行身份驗證檢查,並防止開發人員忘記檢查使用者是否被授權存取資料。
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getUser();
return (
// ...
)
}
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// Get user ID from session and fetch data
})
要知道
- SPA 中的常見模式是在 layout 或頂層元件中
return null
,如果使用者未經授權。此模式不建議使用,因為 Next.js 應用程式具有多個入口點,這將無法阻止存取巢狀路由區段和 Server Actions。
Server Actions
將 Server Actions 視為與面向公眾的 API 端點相同的安全考量,並驗證使用者是否被允許執行 mutation。
在下面的範例中,我們在允許操作繼續之前檢查使用者的角色
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction(formData: FormData) {
const session = await verifySession()
const userRole = session?.user?.role
// Return early if user is not authorized to perform the action
if (userRole !== 'admin') {
return null
}
// Proceed with the action for authorized users
}
Route Handlers
將 Route Handlers 視為與面向公眾的 API 端點相同的安全考量,並驗證使用者是否被允許存取 Route Handler。
例如
import { verifySession } from '@/app/lib/dal'
export async function GET() {
// User authentication and role verification
const session = await verifySession()
// Check if the user is authenticated
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}
// Check if the user has the 'admin' role
if (session.user.role !== 'admin') {
// User is authenticated but does not have the right permissions
return new Response(null, { status: 403 })
}
// Continue for authorized users
}
上面的範例示範了一個具有雙層安全檢查的 Route Handler。它首先檢查是否有活動的 session,然後驗證登入的使用者是否為 'admin'。
Context Providers
由於交錯處理,將 context providers 用於身份驗證是可行的。但是,React context
在 Server Components 中不受支援,使其僅適用於 Client Components。
這樣做是可行的,但任何子 Server Components 都會先在伺服器上渲染,並且無法存取 context provider 的 session 資料
import { ContextProvider } from 'auth-lib'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ContextProvider>{children}</ContextProvider>
</body>
</html>
)
}
"use client";
import { useSession } from "auth-lib";
export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)
return (
// ...
);
}
如果 Client Components 中需要 session 資料(例如,用於客戶端資料提取),請使用 React 的 taintUniqueValue
API,以防止敏感的 session 資料暴露給客戶端。
資源
既然您已經了解 Next.js 中的身份驗證,以下是一些與 Next.js 相容的 library 和資源,可協助您實作安全的身份驗證和 session 管理
Auth Libraries
Session 管理 Libraries
延伸閱讀
若要繼續學習關於身份驗證和安全性的知識,請查看以下資源
這有幫助嗎?