保护应用程序数据,理解身份验证至关重要。本页将引导您了解如何使用 React 和 Next.js 功能来实现身份验证。
在开始之前,将此过程分解为三个概念会很有帮助:
此图显示了使用 React 和 Next.js 功能的身份验证流程:

本页的示例仅出于教育目的,介绍了基本的用户名和密码身份验证。虽然您可以实现自定义身份验证解决方案,但为了提高安全性和简化性,我们建议使用身份验证库。这些库提供了身份验证、会话管理和授权的内置解决方案,以及社交登录、多因素身份验证和基于角色的访问控制等附加功能。您可以在 Auth Libraries 部分找到列表。
您可以使用 <form> 元素结合 React 的 Server Actions 和 useActionState 来捕获用户凭据、验证表单字段,并调用您的 Authentication Provider's API 或数据库。
由于 Server Actions 始终在服务器上执行,因此它们为处理身份验证逻辑提供了安全的环境。
以下是实现注册/登录功能的步骤:
要捕获用户凭据,请创建一个表单,在提交时调用 Server Action。例如,一个接受用户姓名、电子邮件和密码的注册表单:
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>
);
}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) {}export async function signup(formData) {}使用 Server Action 在服务器上验证表单字段。如果您的身份验证提供商不提供表单验证,您可以使用像 Zod 或 Yup 这样的模式验证库。
以 Zod 为例,您可以定义一个包含适当错误消息的表单模式:
import * as z from "zod";
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { error: "Name must be at least 2 characters long." })
.trim(),
email: z.email({ error: "Please enter a valid email." }).trim(),
password: z
.string()
.min(8, { error: "Be at least 8 characters long" })
.regex(/[a-zA-Z]/, { error: "Contain at least one letter." })
.regex(/[0-9]/, { error: "Contain at least one number." })
.regex(/[^a-zA-Z0-9]/, {
error: "Contain at least one special character.",
})
.trim(),
});
export type FormState =
| {
errors?: {
name?: string[];
email?: string[];
password?: string[];
};
message?: string;
}
| undefined;import * as z from "zod";
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { error: "Name must be at least 2 characters long." })
.trim(),
email: z.email({ error: "Please enter a valid email." }).trim(),
password: z
.string()
.min(8, { error: "Be at least 8 characters long" })
.regex(/[a-zA-Z]/, { error: "Contain at least one letter." })
.regex(/[0-9]/, { error: "Contain at least one number." })
.regex(/[^a-zA-Z0-9]/, {
error: "Contain at least one special character.",
})
.trim(),
});为了防止不必要地调用您的身份验证提供商的 API 或数据库,如果任何表单字段与定义的模式不匹配,您可以在 Server Action 中提前 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...
}import { SignupFormSchema } from "@/app/lib/definitions";
export async function signup(state, 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 钩子在表单提交时显示验证错误:
"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>
);
}"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>
);
}Good to know:
- 在 React 19 中,
useFormStatus在返回对象中包含了额外键,如data、method和action。如果您未使用 React 19,则只有pending键可用。- 在修改数据之前,您应该始终确保用户也有权执行该操作。请参阅 Authentication and Authorization。
验证表单字段后,您可以通过调用身份验证提供商的 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
}export async function signup(state, 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 Library 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 或数据库中,或两者兼而有之。继续阅读 Session Management 部分以了解更多信息。
Tips:
- 上述示例为了教育目的而详细分解了身份验证步骤。这突出表明,实现自己的安全解决方案会迅速变得复杂。考虑使用 Auth Library 来简化此过程。
- 为了改善用户体验,您可能希望在注册流程的早期检查重复的电子邮件或用户名。例如,在用户输入用户名或输入字段失去焦点时。这有助于防止不必要的表单提交,并向用户提供即时反馈。您可以使用 use-debounce 等库来管理这些检查的频率。
以下是实现注册和/或登录表单的步骤:
考虑一个用户可以输入其凭据的登录表单:
import { FormEvent } from "react";
import { useRouter } from "next/router";
export default function LoginPage() {
const router = useRouter();
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get("email");
const password = formData.get("password");
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
router.push("/profile");
} else {
// Handle errors
}
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Email" required />
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button type="submit">Login</button>
</form>
);
}import { FormEvent } from "react";
import { useRouter } from "next/router";
export default function LoginPage() {
const router = useRouter();
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get("email");
const password = formData.get("password");
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
router.push("/profile");
} else {
// Handle errors
}
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Email" required />
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button type="submit">Login</button>
</form>
);
}上述表单有两个输入字段用于捕获用户的电子邮件和密码。提交时,它会触发一个函数,该函数向 API 路由 (/api/auth/login) 发送 POST 请求。
然后,您可以在 API 路由中调用您的 Authentication Provider's API 来处理身份验证:
import type { NextApiRequest, NextApiResponse } from "next";
import { signIn } from "@/auth";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const { email, password } = req.body;
await signIn("credentials", { email, password });
res.status(200).json({ success: true });
} catch (error) {
if (error.type === "CredentialsSignin") {
res.status(401).json({ error: "Invalid credentials." });
} else {
res.status(500).json({ error: "Something went wrong." });
}
}
}import { signIn } from "@/auth";
export default async function handler(req, res) {
try {
const { email, password } = req.body;
await signIn("credentials", { email, password });
res.status(200).json({ success: true });
} catch (error) {
if (error.type === "CredentialsSignin") {
res.status(401).json({ error: "Invalid credentials." });
} else {
res.status(500).json({ error: "Something went wrong." });
}
}
}会话管理确保用户的身份验证状态在请求之间保持不变。它涉及创建、存储、刷新和删除会话或令牌。
会话有两种类型:
Good to know: 虽然您可以选择使用其中一种或两种方法,但我们建议使用会话管理库,例如 iron-session 或 Jose。
要创建和管理无状态会话,您需要遵循以下几个步骤:
除了上述内容,考虑添加功能,在用户再次访问应用程序时更新(或刷新)会话,并在用户注销时删除会话。
Good to know: 检查您的 auth library 是否包含会话管理功能。
您可以通过几种方式生成用于签署会话的密钥。例如,您可能选择在终端中使用 openssl 命令:
openssl rand -base64 32此命令生成一个 32 个字符的随机字符串,您可以将其用作密钥并存储在环境变量文件中:
SESSION_SECRET=your_secret_key然后您可以在会话管理逻辑中引用此密钥:
const secretKey = process.env.SESSION_SECRET;接下来,您可以使用首选的会话管理库来加密和解密会话。继续上一个示例,我们将使用 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");
}
}import "server-only";
import { SignJWT, jwtVerify } from "jose";
const secretKey = process.env.SESSION_SECRET;
const encodedKey = new TextEncoder().encode(secretKey);
export async function encrypt(payload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("7d")
.sign(encodedKey);
}
export async function decrypt(session) {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ["HS256"],
});
return payload;
} catch (error) {
console.log("Failed to verify session");
}
}Tips:
payload应包含在后续请求中使用的最少且唯一的用户数据,例如用户的 ID、角色等。它不应包含个人身份信息,如电话号码、电子邮件地址、信用卡信息等,或敏感数据,如密码。
要将会话存储在 cookie 中,请使用 Next.js cookies API。cookie 应该在服务器上设置,并包含推荐的选项:
请参阅 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: "/",
});
}import "server-only";
import { cookies } from "next/headers";
export async function createSession(userId) {
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: "/",
});
}回到您的 Server Action 中,您可以调用 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");
}import { createSession } from "@/app/lib/session";
export async function signup(state, 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");
}Tips:
- Cookie 应该在服务器上设置以防止客户端篡改。
- 🎥 观看:了解更多关于 Next.js 的无状态会话和身份验证 → YouTube (11 minutes)。
您还可以延长会话的过期时间。这对于用户再次访问应用程序后保持登录状态很有用。例如:
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: "/",
});
}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)(
await cookies()
).set("session", session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: "lax",
path: "/",
});
}Tip: 检查您的身份验证库是否支持刷新令牌,这些令牌可用于延长用户会话。
要删除会话,您可以删除 cookie:
import "server-only";
import { cookies } from "next/headers";
export async function deleteSession() {
const cookieStore = await cookies();
cookieStore.delete("session");
}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() {
await deleteSession();
redirect("/login");
}import { cookies } from "next/headers";
import { deleteSession } from "@/app/lib/session";
export async function logout() {
await deleteSession();
redirect("/login");
}您可以使用 API Routes 在服务器上将会话设置为 cookie:
import { serialize } from "cookie";
import type { NextApiRequest, NextApiResponse } from "next";
import { encrypt } from "@/app/lib/session";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const sessionData = req.body;
const encryptedSessionData = encrypt(sessionData);
const cookie = serialize("session", encryptedSessionData, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 7, // One week
path: "/",
});
res.setHeader("Set-Cookie", cookie);
res.status(200).json({ message: "Successfully set cookie!" });
}import { serialize } from "cookie";
import { encrypt } from "@/app/lib/session";
export default function handler(req, res) {
const sessionData = req.body;
const encryptedSessionData = encrypt(sessionData);
const cookie = serialize("session", encryptedSessionData, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 7, // One week
path: "/",
});
res.setHeader("Set-Cookie", cookie);
res.status(200).json({ message: "Successfully set 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: "/",
});
}import cookies from "next/headers";
import { db } from "@/app/lib/db";
import { encrypt } from "@/app/lib/session";
export async function createSession(id) {
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: "/",
});
}Tips:
- 为了更快访问,您可以考虑在会话生命周期内添加服务器缓存。您也可以将会话数据保存在主数据库中,并组合数据请求以减少查询次数。
- 您可以选择使用数据库会话来处理更高级的用例,例如跟踪用户上次登录时间、活动设备数量,或让用户能够从所有设备注销。
实施会话管理后,您需要添加授权逻辑来控制用户在应用程序中可以访问和执行的操作。继续阅读 Authorization 部分以了解更多信息。
Creating a Session on the Server:
import db from "../../lib/db";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const user = req.body;
const sessionId = generateSessionId();
await db.insertSession({
sessionId,
userId: user.id,
createdAt: new Date(),
});
res.status(200).json({ sessionId });
} catch (error) {
res.status(500).json({ error: "Internal Server Error" });
}
}import db from "../../lib/db";
export default async function handler(req, res) {
try {
const user = req.body;
const sessionId = generateSessionId();
await db.insertSession({
sessionId,
userId: user.id,
createdAt: new Date(),
});
res.status(200).json({ sessionId });
} catch (error) {
res.status(500).json({ error: "Internal Server Error" });
}
}用户通过身份验证并创建会话后,您可以实施授权来控制用户在应用程序中可以访问和执行的操作。
主要有两种类型的授权检查:
对于这两种情况,我们建议:
在某些情况下,您可能希望使用 Proxy 并根据权限重定向用户:
然而,由于 Proxy 在每条路由上运行,包括预取路由,因此重要的是只从 cookie 读取会话(乐观检查),并避免数据库检查以防止性能问题。
例如:
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 proxy(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 Proxy should not run on
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
};import { 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 proxy(req) {
// 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);
// 5. Redirect to /login if the user is not authenticated
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL("/login", req.nextUrl));
}
// 6. 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 Proxy should not run on
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
};虽然 Proxy 对于初始检查很有用,但它不应该是您保护数据的唯一防线。大多数安全检查应该尽可能靠近数据源执行,更多信息请参阅 Data Access Layer。
Tips:
- 在 Proxy 中,您还可以使用
req.cookies.get('session').value读取 cookie。- Proxy 使用 Node.js 运行时,请检查您的 Auth library 和 session management library 是否兼容。如果您的 Auth library 只支持 Edge Runtime,您可能需要使用 Middleware。
- 您可以使用 Proxy 中的
matcher属性指定 Proxy 应该在哪些路由上运行。尽管对于身份验证,建议 Proxy 在所有路由上运行。
我们建议创建一个 DAL 来集中管理您的数据请求和授权逻辑。
DAL 应该包含一个函数,用于在用户与您的应用程序交互时验证用户的会话。至少,该函数应该检查会话是否有效,然后重定向或返回进行进一步请求所需的用户信息。
例如,为您的 DAL 创建一个单独的文件,其中包含 verifySession() 函数。然后使用 React 的 cache API 在 React 渲染过程中记忆函数的返回值:
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 };
});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;
}
});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;
}
});Tip:
在检索数据时,建议您只返回应用程序中将使用的必要数据,而不是整个对象。例如,如果您要获取用户数据,您可能只返回用户的 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,
};
}import "server-only";
import { getUser } from "@/app/lib/dal";
function canSeeUsername(viewer) {
return true;
}
function canSeePhoneNumber(viewer, team) {
return viewer.isAdmin || team === viewer.team;
}
export async function getProfileDTO(slug) {
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,您可以确保所有数据请求都是安全且一致的,从而使应用程序在扩展时更易于维护、审计和调试。
Good to know:
- 有几种不同的方法可以定义 DTO,从使用
toJSON()到像上面示例中的单个函数,或 JS 类。由于这些是 JavaScript 模式,而不是 React 或 Next.js 功能,我们建议您研究一下,为您的应用程序找到最佳模式。- 了解更多关于安全最佳实践的信息,请阅读我们的 Security in Next.js article。
Server Components 中的身份验证检查对于基于角色的访问很有用。例如,根据用户的角色有条件地渲染组件:
import { verifySession } from "@/app/lib/dal";
export default async 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");
}
}import { verifySession } from "@/app/lib/dal";
export default async 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 中进行检查时要小心,因为这些布局不会在导航时重新渲染,这意味着用户会话不会在每次路由更改时都进行检查。
相反,您应该在靠近数据源或将有条件渲染的组件处进行检查。
例如,考虑一个共享布局,它获取用户数据并在导航中显示用户图像。您不应在布局中进行身份验证检查,而应在布局中获取用户数据 (getUser()) 并在 DAL 中进行身份验证检查。
这保证了无论在应用程序中的何处调用 getUser(),都会执行身份验证检查,并防止开发人员忘记检查用户是否被授权访问数据。
例如,在仪表盘页面中,您可以验证用户会话并获取用户数据:
import { verifySession } from "@/app/lib/dal";
export default async function DashboardPage() {
const session = await verifySession();
// Fetch user-specific data from your database or data source
const user = await getUserData(session.userId);
return (
<div>
<h1>Welcome, {user.name}</h1>
{/* Dashboard content */}
</div>
);
}import { verifySession } from "@/app/lib/dal";
export default async function DashboardPage() {
const session = await verifySession();
// Fetch user-specific data from your database or data source
const user = await getUserData(session.userId);
return (
<div>
<h1>Welcome, {user.name}</h1>
{/* Dashboard content */}
</div>
);
}您还可以在根据用户权限有条件地渲染 UI 元素的叶组件中执行身份验证检查。例如,一个显示仅限管理员操作的组件:
import { verifySession } from "@/app/lib/dal";
export default async function AdminActions() {
const session = await verifySession();
const userRole = session?.user?.role;
if (userRole !== "admin") {
return null;
}
return (
<div>
<button>Delete User</button>
<button>Edit Settings</button>
</div>
);
}import { verifySession } from "@/app/lib/dal";
export default async function AdminActions() {
const session = await verifySession();
const userRole = session?.user?.role;
if (userRole !== "admin") {
return null;
}
return (
<div>
<button>Delete User</button>
<button>Edit Settings</button>
</div>
);
}这种模式允许您根据用户权限显示或隐藏 UI 元素,同时确保身份验证检查在每个组件的渲染时进行。
Good to know:
- SPAs 中常见的模式是在用户未授权时在布局或顶级组件中
return null。不建议使用此模式,因为 Next.js 应用程序有多个入口点,这将无法阻止访问嵌套路由段和 Server Actions。- 确保从这些组件调用的任何 Server Actions 也执行自己的授权检查,因为仅客户端 UI 限制不足以保证安全。
对待 Server Actions 应与面向公众的 API 端点采取相同的安全考虑,并验证用户是否被允许执行更改。
在下面的示例中,我们在允许操作继续之前检查用户的角色:
"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
}"use server";
import { verifySession } from "@/app/lib/dal";
export async function serverAction() {
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 应与面向公众的 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
}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,它具有两层安全检查。它首先检查活动会话,然后验证登录用户是否为“admin”。
由于交错渲染,使用上下文提供程序进行身份验证是可行的。但是,React context 不支持 Server Components,这使得它们只适用于 Client Components。
这种方式可行,但任何子 Server Components 将首先在服务器上渲染,并且无法访问上下文提供程序的会话数据:
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 (
// ...
);
}'use client';
import { useSession } from "auth-lib";
export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)
return (
// ...
);
}如果 Client Components 中需要会话数据(例如,用于客户端数据获取),请使用 React 的 taintUniqueValue API,以防止敏感会话数据暴露给客户端。
Next.js 中的 API 路由对于处理服务器端逻辑和数据管理至关重要。保护这些路由以确保只有授权用户才能访问特定功能至关重要。这通常涉及验证用户的身份验证状态及其基于角色的权限。
以下是保护 API 路由的示例:
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await getSession(req);
// Check if the user is authenticated
if (!session) {
res.status(401).json({
error: "User is not authenticated",
});
return;
}
// Check if the user has the 'admin' role
if (session.user.role !== "admin") {
res.status(401).json({
error: "Unauthorized access: User does not have admin privileges.",
});
return;
}
// Proceed with the route for authorized users
// ... implementation of the API Route
}export default async function handler(req, res) {
const session = await getSession(req);
// Check if the user is authenticated
if (!session) {
res.status(401).json({
error: "User is not authenticated",
});
return;
}
// Check if the user has the 'admin' role
if (session.user.role !== "admin") {
res.status(401).json({
error: "Unauthorized access: User does not have admin privileges.",
});
return;
}
// Proceed with the route for authorized users
// ... implementation of the API Route
}此示例演示了一个 API 路由,它具有用于身份验证和授权的两层安全检查。它首先检查活动会话,然后验证登录用户是否为“admin”。这种方法确保了安全的访问,仅限于经过身份验证和授权的用户,从而为请求处理保持了强大的安全性。
既然您已经了解了 Next.js 中的身份验证,以下是 Next.js 兼容的库和资源,可帮助您实现安全的身份验证和会话管理:
要继续学习身份验证和安全,请查看以下资源: