Next.js 中如何实现身份验证?
Next.js 提供了多种身份验证解决方案,开发者可以根据项目需求选择合适的方式。以下是 Next.js 中常用的身份验证方法:1. NextAuth.jsNextAuth.js 是 Next.js 最流行的身份验证库,提供完整的身份验证解决方案。基本配置// app/api/auth/[...nextauth]/route.jsimport NextAuth from 'next-auth';import CredentialsProvider from 'next-auth/providers/credentials';import GoogleProvider from 'next-auth/providers/google';export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), CredentialsProvider({ name: 'Credentials', credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" } }, async authorize(credentials) { const user = await authenticateUser(credentials); if (user) { return user; } return null; } }), ], pages: { signIn: '/auth/signin', error: '/auth/error', }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; } return token; }, async session({ session, token }) { session.user.id = token.id; session.user.role = token.role; return session; }, }, session: { strategy: 'jwt', }, secret: process.env.NEXTAUTH_SECRET,});export { handlers as GET, handlers as POST };使用 Session'use client';import { useSession, signIn, signOut } from 'next-auth/react';export default function Navbar() { const { data: session, status } = useSession(); if (status === 'loading') { return <div>Loading...</div>; } return ( <nav> {session ? ( <> <p>Welcome, {session.user.name}</p> <button onClick={() => signOut()}>Sign Out</button> </> ) : ( <button onClick={() => signIn()}>Sign In</button> )} </nav> );}服务器端获取 Sessionimport { auth } from '@/auth';export default async function Dashboard() { const session = await auth(); if (!session) { redirect('/auth/signin'); } return ( <div> <h1>Welcome, {session.user.name}</h1> <p>Role: {session.user.role}</p> </div> );}保护路由import { auth } from '@/auth';export default async function ProtectedPage() { const session = await auth(); if (!session) { redirect('/auth/signin'); } return <div>Protected content</div>;}2. 自定义 JWT 实现使用 Next.js API Routes 和 JWT 实现自定义身份验证。JWT 工具函数// lib/auth.jsimport jwt from 'jsonwebtoken';const SECRET = process.env.JWT_SECRET;export function signToken(payload) { return jwt.sign(payload, SECRET, { expiresIn: '7d' });}export function verifyToken(token) { try { return jwt.verify(token, SECRET); } catch (error) { return null; }}export function setAuthCookie(res, token) { res.setHeader('Set-Cookie', `token=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=604800`);}export function clearAuthCookie(res) { res.setHeader('Set-Cookie', 'token=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0');}登录 API// app/api/auth/login/route.jsimport { signToken, setAuthCookie } from '@/lib/auth';import { compare } from 'bcryptjs';import { db } from '@/lib/db';export async function POST(request) { try { const { email, password } = await request.json(); const user = await db.user.findUnique({ where: { email } }); if (!user || !await compare(password, user.password)) { return Response.json( { error: 'Invalid credentials' }, { status: 401 } ); } const token = signToken({ userId: user.id, email: user.email }); const response = Response.json({ user: { id: user.id, email: user.email, name: user.name } }); setAuthCookie(response, token); return response; } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); }}注册 API// app/api/auth/register/route.jsimport { hash } from 'bcryptjs';import { db } from '@/lib/db';export async function POST(request) { try { const { email, password, name } = await request.json(); const existingUser = await db.user.findUnique({ where: { email } }); if (existingUser) { return Response.json( { error: 'User already exists' }, { status: 400 } ); } const hashedPassword = await hash(password, 10); const user = await db.user.create({ data: { email, password: hashedPassword, name, }, }); return Response.json( { user: { id: user.id, email: user.email, name: user.name } }, { status: 201 } ); } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); }}获取当前用户// lib/getCurrentUser.jsimport { cookies } from 'next/headers';import { verifyToken } from '@/lib/auth';import { db } from '@/lib/db';export async function getCurrentUser() { const cookieStore = await cookies(); const token = cookieStore.get('token')?.value; if (!token) { return null; } const decoded = verifyToken(token); if (!decoded) { return null; } const user = await db.user.findUnique({ where: { id: decoded.userId }, select: { id: true, email: true, name: true, role: true } }); return user;}3. 中间件保护路由// middleware.jsimport { NextResponse } from 'next/server';import { verifyToken } from '@/lib/auth';export function middleware(request) { const token = request.cookies.get('token')?.value; const { pathname } = request.nextUrl; // 公开路由 const publicPaths = ['/auth/signin', '/auth/register', '/api/auth/login', '/api/auth/register']; if (publicPaths.some(path => pathname.startsWith(path))) { return NextResponse.next(); } // API 路由 if (pathname.startsWith('/api')) { if (!token || !verifyToken(token)) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } return NextResponse.next(); } // 页面路由 if (!token || !verifyToken(token)) { return NextResponse.redirect(new URL('/auth/signin', request.url)); } return NextResponse.next();}export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico).*)', ],};4. OAuth 集成Google OAuth// app/api/auth/google/route.jsimport { OAuth2Client } from 'google-auth-library';import { signToken, setAuthCookie } from '@/lib/auth';import { db } from '@/lib/db';const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);export async function POST(request) { try { const { idToken } = await request.json(); const ticket = await client.verifyIdToken({ idToken, audience: process.env.GOOGLE_CLIENT_ID, }); const payload = ticket.getPayload(); let user = await db.user.findUnique({ where: { email: payload.email } }); if (!user) { user = await db.user.create({ data: { email: payload.email, name: payload.name, image: payload.picture, provider: 'google', }, }); } const token = signToken({ userId: user.id, email: user.email }); const response = Response.json({ user: { id: user.id, email: user.email, name: user.name } }); setAuthCookie(response, token); return response; } catch (error) { return Response.json( { error: 'Authentication failed' }, { status: 401 } ); }}5. 权限控制基于角色的访问控制(RBAC)// lib/permissions.jsexport const ROLES = { ADMIN: 'admin', USER: 'user', GUEST: 'guest',};export function hasPermission(userRole, requiredRole) { const roleHierarchy = { [ROLES.ADMIN]: 3, [ROLES.USER]: 2, [ROLES.GUEST]: 1, }; return roleHierarchy[userRole] >= roleHierarchy[requiredRole];}// 使用示例export default function AdminPanel({ user }) { if (!hasPermission(user.role, ROLES.ADMIN)) { return <div>Access denied</div>; } return <div>Admin content</div>;}服务器端权限检查import { getCurrentUser } from '@/lib/getCurrentUser';import { ROLES, hasPermission } from '@/lib/permissions';export default async function AdminPage() { const user = await getCurrentUser(); if (!user || !hasPermission(user.role, ROLES.ADMIN)) { redirect('/'); } return <div>Admin content</div>;}6. 密码重置发送重置邮件// app/api/auth/forgot-password/route.jsimport { db } from '@/lib/db';import { signToken } from '@/lib/auth';import { sendEmail } from '@/lib/email';export async function POST(request) { try { const { email } = await request.json(); const user = await db.user.findUnique({ where: { email } }); if (!user) { // 不透露用户是否存在 return Response.json({ message: 'If user exists, email sent' }); } const resetToken = signToken( { userId: user.id, type: 'password_reset' }, { expiresIn: '1h' } ); await sendEmail({ to: email, subject: 'Password Reset', html: `<a href="${process.env.APP_URL}/auth/reset-password?token=${resetToken}">Reset Password</a>`, }); return Response.json({ message: 'Email sent' }); } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); }}重置密码// app/api/auth/reset-password/route.jsimport { verifyToken } from '@/lib/auth';import { hash } from 'bcryptjs';import { db } from '@/lib/db';export async function POST(request) { try { const { token, password } = await request.json(); const decoded = verifyToken(token); if (!decoded || decoded.type !== 'password_reset') { return Response.json( { error: 'Invalid or expired token' }, { status: 400 } ); } const hashedPassword = await hash(password, 10); await db.user.update({ where: { id: decoded.userId }, data: { password: hashedPassword }, }); return Response.json({ message: 'Password reset successful' }); } catch (error) { return Response.json( { error: 'Internal server error' }, { status: 500 } ); }}身份验证最佳实践使用 HTTPS:始终使用 HTTPS 传输敏感数据HttpOnly Cookies:使用 HttpOnly 防止 XSS 攻击CSRF 保护:实施 CSRF 令牌保护密码哈希:使用 bcrypt 或 argon2 哈希密码速率限制:防止暴力破解攻击安全头部:设置适当的安全头部日志记录:记录身份验证事件用于审计会话管理:设置合理的会话过期时间多因素认证:为敏感操作实施 MFA定期更新:保持依赖项和安全补丁更新通过合理实施这些身份验证方法,可以构建安全可靠的 Next.js 应用。