4.8 KiB
4.8 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| auth-flow-patterns | 完整的认证流程模式,涵盖前端与后端 |
|
认证流程模式
前端和后端的完整认证流程。涵盖 JWT Bearer 流程、自动令牌刷新、Next.js 服务端认证、RBAC 以及后端中间件顺序。
JWT Bearer 流程(最常见)
1. 登录
客户端 → POST /api/auth/login { email, password }
服务端 → { accessToken(15分钟), refreshToken(7天,httpOnly cookie) }
2. 认证请求
客户端 → GET /api/orders Authorization: Bearer <accessToken>
服务端 → 验证 JWT → 返回数据
3. 令牌刷新(无感透明)
客户端 → 收到 401 → POST /api/auth/refresh(cookie 自动发送)
服务端 → 新的 accessToken
客户端 → 使用新令牌重试原始请求
4. 登出
客户端 → POST /api/auth/logout
服务端 → 使 refresh token 失效 → 清除 cookie
前端:自动令牌刷新
// lib/api-client.ts — 添加到现有的 fetch 封装中
async function apiWithRefresh<T>(path: string, options: RequestInit = {}): Promise<T> {
try {
return await api<T>(path, options);
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
// 尝试刷新
const refreshed = await api<{ accessToken: string }>('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // 发送 httpOnly cookie
});
setAuthToken(refreshed.accessToken);
// 重试原始请求
return api<T>(path, options);
}
throw err;
}
}
Next.js:服务端认证(App Router)
// middleware.ts — 在服务端保护路由
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// app/dashboard/page.tsx — 带认证的服务端组件
import { cookies } from 'next/headers';
export default async function Dashboard() {
const token = (await cookies()).get('session')?.value;
const user = await fetch(`${process.env.API_URL}/api/me`, {
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json());
return <DashboardContent user={user} />;
}
后端:标准中间件顺序
请求 → 1.请求ID → 2.日志 → 3.CORS → 4.限流 → 5.请求体解析
→ 6.认证 → 7.授权 → 8.校验 → 9.处理器 → 10.错误处理 → 响应
后端:JWT 规则
✅ 短时效的 access token(15分钟)+ refresh token(服务端存储)
✅ 最小化声明:userId、roles(不包含整个用户对象)
✅ 定期轮换签名密钥
❌ 切勿将令牌存储在 localStorage 中(存在 XSS 风险)
❌ 切勿在 URL 查询参数中传递令牌(会被记录)
后端:RBAC 模式
function authorize(...roles: Role[]) {
return (req, res, next) => {
if (!req.user) throw new UnauthorizedError();
if (!roles.some(r => req.user.roles.includes(r))) throw new ForbiddenError();
next();
};
}
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
认证方式决策表
| 方法 | 适用场景 | 前端 |
|---|---|---|
| Session | 同域、SSR、Django 模板 | Django templates / htmx |
| JWT | 跨域、SPA、移动端 | React, Vue, 移动端应用 |
| OAuth2 | 第三方登录、API 消费者 | 任何 |
铁律
✅ Access token:短时效(15分钟),存于内存
✅ Refresh token:httpOnly cookie(XSS 安全)
✅ 收到 401 时自动无感刷新
✅ 刷新失败时重定向到登录页
❌ 切勿将令牌存储在 localStorage 中(存在 XSS 风险)
❌ 切勿在 URL 查询参数中传递令牌(会被记录)
❌ 切勿仅依赖客户端认证检查(服务端必须验证)
常见问题
问题 1:「页面加载时认证正常,导航后失效」
原因: 令牌存储在组件状态中(组件卸载后丢失)。
解决方案: 将 access token 存储在持久化位置:
- React Context(导航后仍存活,页面刷新后丢失)
- Cookie(刷新后仍存活)
- React Query 缓存,对 session 设置
staleTime: Infinity
问题 2:「认证请求出现 CORS 错误」
原因: 前端缺少 credentials: 'include' 或后端 CORS 配置中缺少 credentials: true。
解决方案:
- 前端:
fetch(url, { credentials: 'include' }) - 后端:
cors({ origin: 'https://your-frontend.com', credentials: true }) - 后端:使用凭据时必须指定明确的 origin(不能使用
*)