如何在Next.js 13中为API路由和页面实现JWT令牌认证?
Next.js 13 JWT令牌认证分步实现指南
1. 安装依赖
先安装核心依赖和类型定义:
npm install jsonwebtoken bcryptjs cookie npm install -D @types/jsonwebtoken @types/bcryptjs @types/cookie
2. 配置环境变量
在项目根目录创建.env.local文件,添加以下变量:
JWT_SECRET=your_strong_secret_key_here # 建议用32位以上随机字符串 JWT_EXPIRES_IN=7d # 令牌有效期,比如7天
3. 核心工具函数封装
创建lib/auth.ts文件,封装JWT生成、验证和密码哈希工具:
import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; // 密码哈希 export async function hashPassword(password: string) { return bcrypt.hash(password, 12); } // 密码验证 export async function verifyPassword(password: string, hashedPassword: string) { return bcrypt.compare(password, hashedPassword); } // 生成JWT令牌 export function generateToken(userId: string) { return jwt.sign({ userId }, process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRES_IN, }); } // 验证JWT令牌 export function verifyToken(token: string) { try { return jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }; } catch (error) { return null; } }
4. 登录API路由(App Router)
创建app/api/auth/login/route.ts,处理用户登录请求并将JWT存入HttpOnly Cookie:
import { NextResponse } from 'next/server'; import { verifyPassword, generateToken, hashPassword } from '@/lib/auth'; import { cookies } from 'next/headers'; // 模拟数据库用户(实际项目替换为真实数据库查询) const mockUsers = [ { id: '1', email: 'user@example.com', passwordHash: await hashPassword('test123'), // 提前哈希好的测试密码 }, ]; export async function POST(request: Request) { const { email, password } = await request.json(); // 查找用户 const user = mockUsers.find(u => u.email === email); if (!user) { return NextResponse.json({ message: '用户不存在' }, { status: 401 }); } // 验证密码 const isValidPassword = await verifyPassword(password, user.passwordHash); if (!isValidPassword) { return NextResponse.json({ message: '密码错误' }, { status: 401 }); } // 生成令牌 const token = generateToken(user.id); // 设置HttpOnly Cookie(生产环境需开启secure) cookies().set('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60, // 与JWT有效期一致 path: '/', }); return NextResponse.json({ message: '登录成功' }); }
5. 全局认证中间件
创建middleware.ts,保护指定路由,未登录用户将被重定向到登录页:
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { verifyToken } from '@/lib/auth'; import { cookies } from 'next/headers'; export function middleware(request: NextRequest) { // 不需要保护的路由(登录页、公共API等) const publicPaths = ['/', '/login', '/api/auth/login']; const isPublicPath = publicPaths.some(path => request.nextUrl.pathname.startsWith(path)); if (isPublicPath) { return NextResponse.next(); } // 获取Cookie中的令牌 const token = cookies().get('token')?.value; if (!token) { // 未登录,重定向到登录页 return NextResponse.redirect(new URL('/login', request.url)); } // 验证令牌 const decodedToken = verifyToken(token); if (!decodedToken) { cookies().delete('token'); return NextResponse.redirect(new URL('/login', request.url)); } // 令牌有效,继续请求 return NextResponse.next(); } // 指定中间件生效的路由 export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'], };
6. 受保护的服务器组件示例
创建app/dashboard/page.tsx,在服务器组件中获取当前登录用户信息:
import { cookies } from 'next/headers'; import { verifyToken } from '@/lib/auth'; // 模拟根据用户ID获取用户信息的函数 async function getUserById(userId: string) { return mockUsers.find(u => u.id === userId); } export default async function DashboardPage() { const token = cookies().get('token')?.value; if (!token) { // 理论上中间件已经拦截,这里做双重保险 return <div>请先登录</div>; } const decodedToken = verifyToken(token); if (!decodedToken) { return <div>登录状态无效</div>; } const user = await getUserById(decodedToken.userId); return ( <div> <h1>欢迎回来,{user?.email}</h1> <p>这是受保护的仪表盘页面</p> </div> ); }
7. 退出登录API路由
创建app/api/auth/logout/route.ts,清除令牌Cookie:
import { NextResponse } from 'next/server'; import { cookies } from 'next/headers'; export async function POST() { cookies().delete('token'); return NextResponse.json({ message: '退出成功' }); }
8. Pages Router 适配说明
如果使用Pages Router,核心逻辑一致,差异点:
- API路由放在
pages/api/auth/login.ts,使用req和res对象处理Cookie:import type { NextApiRequest, NextApiResponse } from 'next'; import { verifyPassword, generateToken } from '@/lib/auth'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).json({ message: '不支持的请求方法' }); } // 登录逻辑... res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Secure=${process.env.NODE_ENV === 'production'}; SameSite=Strict; Max-Age=${7*24*60*60}; Path=/`); res.status(200).json({ message: '登录成功' }); } - 受保护页面使用
getServerSideProps获取用户信息:export async function getServerSideProps(context: any) { const token = context.req.cookies.token; if (!token) { return { redirect: { destination: '/login', permanent: false }, }; } const decodedToken = verifyToken(token); if (!decodedToken) { return { redirect: { destination: '/login', permanent: false }, }; } const user = await getUserById(decodedToken.userId); return { props: { user } }; }
关键安全注意事项
- 始终使用HttpOnly Cookie存储JWT,防止XSS攻击
- 生产环境开启
secureCookie属性(仅HTTPS传输) - JWT密钥要足够复杂,避免硬编码,用环境变量管理
- 密码必须哈希后存储,禁止明文存储
- 中间件要严格控制受保护路由范围
- 定期更新JWT密钥,令牌过期时间不宜过长
内容的提问来源于stack exchange,提问作者joyadeep limbu
相关产品推荐
相关产品推荐

