173 lines
4.8 KiB
Markdown
173 lines
4.8 KiB
Markdown
---
|
||
name: auth-flow-patterns
|
||
description: 完整的认证流程模式,涵盖前端与后端
|
||
metadata:
|
||
type: reference
|
||
---
|
||
|
||
# 认证流程模式
|
||
|
||
前端和后端的完整认证流程。涵盖 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
|
||
```
|
||
|
||
---
|
||
|
||
## 前端:自动令牌刷新
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
```typescript
|
||
// 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 模式
|
||
|
||
```typescript
|
||
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`。
|
||
|
||
**解决方案:**
|
||
1. 前端:`fetch(url, { credentials: 'include' })`
|
||
2. 后端:`cors({ origin: 'https://your-frontend.com', credentials: true })`
|
||
3. 后端:使用凭据时必须指定明确的 origin(不能使用 `*`)
|