第 161 章:Middleware 与鉴权
学习目标
- 掌握 Next.js Middleware 编写
- 学会基于 JWT 的鉴权
- 理解 NextAuth.js 集成
- 学会保护路由
一、Middleware 是什么
在请求到达页面 / API 之前运行的代码,可用于鉴权、重定向、Header 注入。
二、基础 Middleware
2.1 创建 middleware.ts
ts
// middleware.ts(项目根目录,不是 app/ 下)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 1. 检查 token
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 2. 注入 Header
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', '123');
return NextResponse.next({
request: { headers: requestHeaders },
});
}
// 3. 配置匹配路径
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};2.2 常用方法
ts
// 重定向
return NextResponse.redirect(new URL('/login', request.url));
// 重写
return NextResponse.rewrite(new URL('/about', request.url));
// 返回 JSON
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
// 设置 Cookie
const response = NextResponse.next();
response.cookies.set('logged-in', 'true');
return response;三、JWT 鉴权
3.1 安装
bash
pnpm add jose3.2 验证 token
ts
// lib/auth.ts
import { jwtVerify, SignJWT } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
export async function createToken(payload: { userId: number; role: string }) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(secret);
}
export async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret);
return payload as { userId: number; role: string };
} catch {
return null;
}
}3.3 在 Middleware 中验证
ts
// middleware.ts
import { verifyToken } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
const payload = await verifyToken(token);
if (!payload) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 注入用户信息
const headers = new Headers(request.headers);
headers.set('x-user-id', String(payload.userId));
headers.set('x-user-role', payload.role);
return NextResponse.next({ request: { headers } });
}3.4 在页面 / API 读取
ts
// app/api/profile/route.ts
import { headers } from 'next/headers';
export async function GET() {
const userId = (await headers()).get('x-user-id');
// ...
}四、NextAuth.js(推荐)
完整鉴权方案,支持 OAuth、邮箱、密码等。
4.1 安装
bash
pnpm add next-auth@beta4.2 配置
ts
// auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isProtected = nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !isLoggedIn) return false;
return true;
},
},
});4.3 路由处理
ts
// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from '@/auth';
// 或
import { handlers } from '@/auth';
export const { GET, POST } = handlers;4.4 中间件
ts
// middleware.ts
export { auth as middleware } from '@/auth';4.5 在 Server Component 用
tsx
import { auth } from '@/auth';
export default async function Dashboard() {
const session = await auth();
if (!session?.user) return null;
return <h1>欢迎,{session.user.name}</h1>;
}4.6 登录按钮
tsx
import { signIn, signOut } from '@/auth';
export function SignInButton() {
return (
<form action={async () => {
'use server';
await signIn('github');
}}>
<button>登录</button>
</form>
);
}五、基于角色的权限
ts
// middleware.ts
const ROLE_PERMISSIONS: Record<string, string[]> = {
admin: ['/admin', '/dashboard'],
user: ['/dashboard'],
};
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) return NextResponse.redirect(new URL('/login', request.url));
const payload = await verifyToken(token);
if (!payload) return NextResponse.redirect(new URL('/login', request.url));
const allowedPaths = ROLE_PERMISSIONS[payload.role] || [];
if (!allowedPaths.some(p => request.nextUrl.pathname.startsWith(p))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.next();
}六、登录页示例
tsx
// app/login/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
async function handleSubmit(e: FormEvent) {
e.preventDefault();
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (res.ok) {
router.push('/dashboard');
} else {
alert('登录失败');
}
}
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">登录</button>
</form>
);
}ts
// app/api/auth/login/route.ts
export async function POST(request: Request) {
const { email, password } = await request.json();
const user = await db.user.findUnique({ where: { email } });
if (!user || !await verifyPassword(password, user.password)) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 });
}
const token = await createToken({ userId: user.id, role: user.role });
const response = Response.json({ ok: true });
response.headers.append(
'Set-Cookie',
`token=${token}; HttpOnly; Path=/; Max-Age=604800; SameSite=Lax`
);
return response;
}七、本章小结
| 概念 | 关键 |
|---|---|
| Middleware | 请求前置处理 |
| JWT | jose 库 |
| NextAuth | 完整鉴权方案 |
| 角色 | 中间件检查 |
动手练习
- 写一个 middleware,保护 /dashboard 路由
- 实现 JWT 签发和验证
- 集成 NextAuth.js(用 GitHub 登录)
- 实现基于角色的访问控制
推荐阅读
下一章:第 162 章:部署到 Vercel →