28d - AI 辅助认证实现
本文是《AI Agent 实战手册》第 28 章第 4 节。 上一节:28c-Spec-Driven业务逻辑 | 下一节:28e-微服务与Serverless
概述
认证(Authentication)与授权(Authorization)是后端系统的安全基石——它决定了”谁可以访问系统”以及”谁可以做什么”。在 AI 辅助开发时代,认证代码的生成面临独特挑战:研究显示 AI 生成的代码中约 15-25% 包含安全漏洞,而认证逻辑恰恰是最不能容忍漏洞的领域。本节系统覆盖 JWT 认证实现(Access Token + Refresh Token)、OAuth 2.0 社交登录集成(Google/GitHub/Apple)、RBAC 权限模型、Session 管理、密码哈希(bcrypt/Argon2)、AI 生成认证代码的常见安全漏洞,以及完整的安全审查清单。
1. 认证体系全景:从密码到零信任
1.1 认证方式演进
现代后端认证已从单一的用户名/密码模式演进为多层次、多因素的认证体系:
认证方式演进时间线:
2000s 用户名 + 密码(Session Cookie)
↓
2010s OAuth 2.0 社交登录 + API Token
↓
2015s JWT 无状态认证 + Refresh Token 轮换
↓
2020s Passkey/WebAuthn + MFA + 零信任架构
↓
2025s AI Agent 认证(OAuth 2.1 + Token Vault)+ 持续认证1.2 认证 vs 授权
| 维度 | 认证(Authentication) | 授权(Authorization) |
|---|---|---|
| 核心问题 | ”你是谁?" | "你能做什么?“ |
| 验证对象 | 用户身份 | 用户权限 |
| 实现方式 | 密码、OAuth、JWT、Passkey | RBAC、ABAC、ACL |
| 发生时机 | 登录时 | 每次请求时 |
| 失败响应 | 401 Unauthorized | 403 Forbidden |
| AI 生成风险 | 密钥泄露、弱哈希、Token 不过期 | 权限绕过、越权访问、缺少检查 |
1.3 工具推荐
| 工具 | 用途 | 核心能力 | 价格 | 适用场景 |
|---|---|---|---|---|
| Clerk | 托管认证服务 | 预构建 UI 组件、社交登录、MFA、组织管理、Next.js 深度集成 | 免费(10K MAU)/ $25/月起(Pro) | 快速上线的 SaaS 产品,Next.js 项目首选 |
| Auth0 | 企业级认证平台 | Universal Login、RBAC、MFA、合规认证(SOC 2/HIPAA)、自定义域名 | 免费(25K MAU)/ $240/月起(B2C Essential) | 企业级应用,需要合规认证的项目 |
| Supabase Auth | 开源认证服务 | 邮箱/密码、OAuth、Magic Link、RLS 集成、自托管 | 免费(50K MAU)/ $25/月起(Pro) | 全栈项目,已使用 Supabase 数据库 |
| Better Auth | 开源认证库 | TypeScript 原生、框架无关、插件系统、自托管 | 免费(开源) | 需要完全控制的 TypeScript 项目 |
| NextAuth.js / Auth.js | 开源认证库 | 50+ OAuth Provider、JWT/Session、数据库适配器 | 免费(开源) | Next.js / SvelteKit / Express 项目 |
| Lucia | 轻量认证库 | Session 管理、数据库无关、TypeScript 优先 | 免费(开源) | 追求轻量和完全控制的项目 |
| SuperTokens | 开源认证平台 | 预构建 UI、Session 管理、多租户、自托管 | 免费(自托管)/ $0.02/MAU(托管) | 需要自托管的中型项目 |
| Passport.js | Node.js 认证中间件 | 500+ 策略、Express 集成、社区生态 | 免费(开源) | Express/Fastify 项目的 OAuth 集成 |
| jose | JWT 库 | JWT 签名/验证、JWK、JWE、零依赖 | 免费(开源) | 需要底层 JWT 操作的项目 |
| CASL | 权限管理库 | 同构授权(前后端共享)、RBAC/ABAC、TypeScript | 免费(开源) | 需要细粒度权限控制的 TypeScript 项目 |
| Cerbos | 策略即代码授权 | 声明式策略、gRPC/HTTP API、审计日志 | 免费(开源)/ 联系销售(Cloud) | 微服务架构的集中式授权 |
| Oso | 授权即服务 | Polar 策略语言、内置 RBAC/ReBAC、多租户 | 免费(开发)/ 联系销售(生产) | 需要复杂授权逻辑的 SaaS 产品 |
1.4 AI 辅助认证的决策树
你的项目需要什么认证方案?
│
├── 快速上线(< 1 周)
│ ├── Next.js 项目?
│ │ └── Clerk / Auth.js ← AI 生成集成代码
│ ├── 全栈 Supabase?
│ │ └── Supabase Auth ← AI 生成 RLS 策略
│ └── 其他框架?
│ └── Auth0 / SuperTokens ← AI 生成 SDK 集成
│
├── 完全自主控制
│ ├── TypeScript 项目?
│ │ └── Better Auth / Lucia + 自建 JWT ← AI 生成认证模块
│ └── Python / Go / Java?
│ └── 框架内置认证 + 自建 JWT ← AI 生成认证中间件
│
├── 企业级需求(合规/多租户)
│ └── Auth0 / Clerk(Organization)/ Oso
│ ← AI 生成合规配置 + RBAC 策略
│
└── AI Agent 认证
└── OAuth 2.1 + Token Vault(Auth0)
← AI 生成 Agent 认证流程2. JWT 认证实现(Access Token + Refresh Token)
2.1 JWT 认证架构
JWT(JSON Web Token)是现代无状态认证的核心。一个安全的 JWT 认证系统需要 Access Token + Refresh Token 双 Token 机制:
┌─────────────────────────────────────────────────────────────┐
│ JWT 双 Token 认证架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务端 │
│ ┌──────┐ ┌──────────────┐ │
│ │ │── 1. 登录请求 ──────→│ 验证凭证 │ │
│ │ │ │ 生成 Token 对 │ │
│ │ │←─ 2. 返回 ─────────│ │ │
│ │ │ Access Token │ │ │
│ │ │ (内存/变量) │ │ │
│ │ │ Refresh Token │ │ │
│ │ │ (HttpOnly Cookie) │ │ │
│ │ │ └──────────────┘ │
│ │ │ │
│ │ │── 3. API 请求 ──────→ 验证 Access Token │
│ │ │ Authorization: (短期:15 分钟) │
│ │ │ Bearer <AT> │
│ │ │ │
│ │ │── 4. AT 过期 ───────→ 返回 401 │
│ │ │ │
│ │ │── 5. 刷新请求 ──────→ 验证 Refresh Token │
│ │ │ Cookie: RT (长期:7-30 天) │
│ │ │ 轮换 RT(旧 RT 失效) │
│ │ │←─ 6. 新 Token 对 ──│ │
│ └──────┘ │
│ │
│ 安全要点: │
│ • Access Token:短期(15min),存内存,不存 localStorage │
│ • Refresh Token:长期(7-30d),HttpOnly + Secure Cookie │
│ • Refresh Token 轮换:每次刷新生成新 RT,旧 RT 立即失效 │
│ • Token 黑名单:登出时将 RT 加入黑名单(Redis) │
│ │
└─────────────────────────────────────────────────────────────┘2.2 JWT Token 结构详解
JWT 由三部分组成,用 . 分隔:Header.Payload.Signature
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. ← Header(算法 + 类型)
eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1p ← Payload(声明)
biIsImlhdCI6MTcxOTAwMDAwMCwiZXhwIjoxNzE5
MDAwOTAwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature(签名)Payload 中应包含的标准声明:
interface JWTPayload {
// 标准声明(RFC 7519)
sub: string; // Subject —— 用户 ID(必须)
iat: number; // Issued At —— 签发时间(必须)
exp: number; // Expiration —— 过期时间(必须)
iss: string; // Issuer —— 签发者(推荐)
aud: string; // Audience —— 受众(推荐)
jti: string; // JWT ID —— 唯一标识(推荐,用于防重放)
// 自定义声明(按需添加)
role: string; // 用户角色
permissions?: string[]; // 权限列表(谨慎:会增大 Token 体积)
orgId?: string; // 组织 ID(多租户场景)
}⚠️ AI 生成 JWT 的常见错误:
| 错误 | 风险 | 正确做法 |
|---|---|---|
| 使用 HS256 + 硬编码密钥 | 密钥泄露即全部 Token 失效 | 使用 RS256/ES256 非对称算法 |
不设置 exp 过期时间 | Token 永不过期,被盗后永久有效 | Access Token 15min,Refresh Token 7-30d |
| 在 Payload 中存储密码/敏感信息 | Payload 是 Base64 编码,非加密 | 只存 userId、role 等非敏感信息 |
| 将 Token 存储在 localStorage | XSS 攻击可窃取 Token | Access Token 存内存,Refresh Token 存 HttpOnly Cookie |
不验证 iss 和 aud | Token 可被跨服务滥用 | 始终验证签发者和受众 |
| 使用弱密钥(如 “secret”) | 暴力破解可伪造 Token | HS256 至少 256 位随机密钥,推荐 RS256 |
2.3 操作步骤:AI 辅助实现 JWT 认证
步骤 1:定义认证模块的类型和接口
提示词模板:生成 JWT 认证类型定义
你是一位后端安全专家。请为 [框架名称] 项目生成 JWT 认证模块的 TypeScript 类型定义。
## 技术栈
- 运行时:Node.js / Bun / Deno
- 框架:[Express / Fastify / NestJS / Hono]
- ORM:[Prisma / Drizzle / TypeORM]
- JWT 库:jose(推荐)或 jsonwebtoken
## 要求
1. 定义 JWT Payload 接口(包含 sub, iat, exp, iss, aud, jti, role)
2. 定义 Token 对类型(accessToken + refreshToken + expiresIn)
3. 定义认证配置接口(密钥、过期时间、签发者、受众)
4. 定义认证错误类型枚举
5. 使用 RS256 非对称算法(公钥验证,私钥签名)
6. Access Token 过期时间:15 分钟
7. Refresh Token 过期时间:7 天
8. 包含 Token 轮换支持
## 安全要求
- 不在 Payload 中存储任何敏感信息
- 所有时间使用 UTC
- Token ID(jti)使用 UUID v4步骤 2:实现 Token 签发和验证服务
// ============================================
// auth/token.service.ts —— JWT Token 服务
// ============================================
import * as jose from 'jose';
import { randomUUID } from 'crypto';
interface TokenConfig {
privateKey: jose.KeyLike; // RS256 私钥(签名用)
publicKey: jose.KeyLike; // RS256 公钥(验证用)
accessTokenTTL: number; // Access Token 有效期(秒)
refreshTokenTTL: number; // Refresh Token 有效期(秒)
issuer: string; // 签发者
audience: string; // 受众
}
interface TokenPair {
accessToken: string;
refreshToken: string;
accessTokenExpiresAt: Date;
refreshTokenExpiresAt: Date;
}
interface AccessTokenPayload {
sub: string;
role: string;
orgId?: string;
}
class TokenService {
constructor(private readonly config: TokenConfig) {}
/**
* 生成 Token 对
* 安全要点:
* 1. 使用 RS256 非对称算法
* 2. 每个 Token 有唯一 jti(防重放)
* 3. 严格设置 iss 和 aud
*/
async generateTokenPair(payload: AccessTokenPayload): Promise<TokenPair> {
const now = Math.floor(Date.now() / 1000);
// Access Token(短期,15 分钟)
const accessToken = await new jose.SignJWT({
role: payload.role,
orgId: payload.orgId,
})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.setSubject(payload.sub)
.setIssuedAt(now)
.setExpirationTime(now + this.config.accessTokenTTL)
.setIssuer(this.config.issuer)
.setAudience(this.config.audience)
.setJti(randomUUID())
.sign(this.config.privateKey);
// Refresh Token(长期,7 天)
const refreshToken = await new jose.SignJWT({
type: 'refresh',
})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.setSubject(payload.sub)
.setIssuedAt(now)
.setExpirationTime(now + this.config.refreshTokenTTL)
.setIssuer(this.config.issuer)
.setAudience(this.config.audience)
.setJti(randomUUID())
.sign(this.config.privateKey);
return {
accessToken,
refreshToken,
accessTokenExpiresAt: new Date((now + this.config.accessTokenTTL) * 1000),
refreshTokenExpiresAt: new Date((now + this.config.refreshTokenTTL) * 1000),
};
}
/**
* 验证 Access Token
* 安全要点:验证签名 + iss + aud + exp
*/
async verifyAccessToken(token: string): Promise<jose.JWTPayload> {
const { payload } = await jose.jwtVerify(token, this.config.publicKey, {
issuer: this.config.issuer,
audience: this.config.audience,
algorithms: ['RS256'],
});
return payload;
}
/**
* 验证 Refresh Token 并轮换
* 安全要点:
* 1. 验证 Token 有效性
* 2. 检查是否在黑名单中(需配合 Redis)
* 3. 生成新 Token 对
* 4. 将旧 Refresh Token 加入黑名单
*/
async rotateRefreshToken(
oldRefreshToken: string,
isTokenBlacklisted: (jti: string) => Promise<boolean>,
blacklistToken: (jti: string, expiresAt: Date) => Promise<void>,
getUserRole: (userId: string) => Promise<string>,
): Promise<TokenPair> {
// 1. 验证旧 Refresh Token
const { payload } = await jose.jwtVerify(
oldRefreshToken,
this.config.publicKey,
{
issuer: this.config.issuer,
audience: this.config.audience,
algorithms: ['RS256'],
},
);
// 2. 检查黑名单(防止重放攻击)
if (payload.jti && await isTokenBlacklisted(payload.jti)) {
// 检测到 Token 重用 —— 可能的攻击!
// 安全策略:撤销该用户的所有 Refresh Token
throw new Error('TOKEN_REUSE_DETECTED');
}
// 3. 将旧 Token 加入黑名单
if (payload.jti && payload.exp) {
await blacklistToken(payload.jti, new Date(payload.exp * 1000));
}
// 4. 生成新 Token 对
const role = await getUserRole(payload.sub!);
return this.generateTokenPair({
sub: payload.sub!,
role,
});
}
}步骤 3:实现认证中间件
// ============================================
// auth/auth.middleware.ts —— Express 认证中间件
// ============================================
import { Request, Response, NextFunction } from 'express';
interface AuthenticatedRequest extends Request {
user?: {
id: string;
role: string;
orgId?: string;
};
}
/**
* JWT 认证中间件
* 安全要点:
* 1. 从 Authorization header 提取 Bearer Token
* 2. 验证 Token 签名和声明
* 3. 将用户信息附加到 request 对象
* 4. 统一错误处理(不泄露内部信息)
*/
function authMiddleware(tokenService: TokenService) {
return async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: 'MISSING_TOKEN',
message: '请提供有效的认证令牌',
});
}
const token = authHeader.slice(7); // 去掉 "Bearer " 前缀
try {
const payload = await tokenService.verifyAccessToken(token);
req.user = {
id: payload.sub!,
role: payload.role as string,
orgId: payload.orgId as string | undefined,
};
next();
} catch (error) {
// 不泄露具体错误原因给客户端
if (error instanceof jose.errors.JWTExpired) {
return res.status(401).json({
error: 'TOKEN_EXPIRED',
message: '令牌已过期,请刷新',
});
}
return res.status(401).json({
error: 'INVALID_TOKEN',
message: '无效的认证令牌',
});
}
};
}步骤 4:实现 Refresh Token 端点
// ============================================
// auth/auth.controller.ts —— 认证控制器(部分)
// ============================================
/**
* POST /auth/refresh
* Refresh Token 轮换端点
*
* 安全要点:
* 1. Refresh Token 从 HttpOnly Cookie 中读取(非 body)
* 2. 设置 Secure + SameSite=Strict
* 3. 轮换后旧 Token 立即失效
*/
async function refreshTokenHandler(req: Request, res: Response) {
const refreshToken = req.cookies?.refreshToken;
if (!refreshToken) {
return res.status(401).json({
error: 'MISSING_REFRESH_TOKEN',
message: '请重新登录',
});
}
try {
const tokenPair = await tokenService.rotateRefreshToken(
refreshToken,
(jti) => redis.exists(`blacklist:${jti}`).then(Boolean),
(jti, expiresAt) => redis.set(
`blacklist:${jti}`, '1',
'EXAT', Math.floor(expiresAt.getTime() / 1000)
),
(userId) => userRepo.findById(userId).then(u => u?.role ?? 'user'),
);
// 设置新的 Refresh Token Cookie
res.cookie('refreshToken', tokenPair.refreshToken, {
httpOnly: true, // 防止 XSS 读取
secure: true, // 仅 HTTPS
sameSite: 'strict', // 防止 CSRF
path: '/auth/refresh', // 限制 Cookie 发送路径
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
});
return res.json({
accessToken: tokenPair.accessToken,
expiresAt: tokenPair.accessTokenExpiresAt.toISOString(),
});
} catch (error) {
if (error.message === 'TOKEN_REUSE_DETECTED') {
// 安全事件:清除该用户所有 session
// 记录安全日志
res.clearCookie('refreshToken');
return res.status(401).json({
error: 'SESSION_REVOKED',
message: '检测到异常活动,请重新登录',
});
}
res.clearCookie('refreshToken');
return res.status(401).json({
error: 'INVALID_REFRESH_TOKEN',
message: '请重新登录',
});
}
}提示词模板:AI 审查 JWT 实现安全性
你是一位应用安全专家。请审查以下 JWT 认证实现代码,检查安全漏洞。
## 代码
[粘贴你的 JWT 认证代码]
## 审查清单
请逐项检查以下安全要点:
1. **算法安全**:是否使用 RS256/ES256?是否防止了 "alg: none" 攻击?
2. **密钥管理**:密钥是否硬编码?是否从环境变量/密钥管理服务加载?
3. **Token 过期**:Access Token 是否 ≤ 15 分钟?Refresh Token 是否 ≤ 30 天?
4. **Token 存储**:Refresh Token 是否使用 HttpOnly + Secure Cookie?
5. **Token 轮换**:Refresh Token 是否在每次使用后轮换?
6. **重放防护**:是否有 Token 黑名单机制?是否检测 Token 重用?
7. **声明验证**:是否验证 iss、aud、exp?
8. **错误处理**:错误响应是否泄露内部信息?
9. **CSRF 防护**:Cookie 是否设置 SameSite=Strict?
10. **登出实现**:登出时是否清除 Cookie 并将 Token 加入黑名单?
## 输出格式
对每个检查项,标注 ✅ 通过 / ❌ 未通过 / ⚠️ 需改进,并给出修复建议。3. OAuth 2.0 社交登录集成
3.1 OAuth 2.0 授权码流程
OAuth 2.0 Authorization Code Flow 是社交登录的标准流程,适用于有后端的 Web 应用:
┌─────────────────────────────────────────────────────────────┐
│ OAuth 2.0 Authorization Code Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户 你的前端 你的后端 OAuth Provider │
│ │ │ │ │ │
│ │─ 点击 ───→│ │ │ │
│ │ "Google │ │ │ │
│ │ 登录" │ │ │ │
│ │ │─ 重定向 ────→│ │ │
│ │ │ │─ 302 ───────→│ │
│ │ │ │ /authorize │ │
│ │ │ │ ?client_id= │ │
│ │ │ │ &redirect_uri│ │
│ │ │ │ &scope= │ │
│ │ │ │ &state= │ │
│ │ │ │ &code_challenge│ │
│ │←─────────────────────────────────────────│ │
│ │ Google 登录页面 │ │
│ │─ 授权同意 ──────────────────────────────→│ │
│ │ │ │←─ 回调 ──────│ │
│ │ │ │ ?code=xxx │ │
│ │ │ │ &state=yyy │ │
│ │ │ │ │ │
│ │ │ │─ POST ───────→│ │
│ │ │ │ /token │ │
│ │ │ │ code + secret │ │
│ │ │ │←─ Token ──────│ │
│ │ │ │ access_token │ │
│ │ │ │ id_token │ │
│ │ │ │ │ │
│ │ │ │─ GET ────────→│ │
│ │ │ │ /userinfo │ │
│ │ │ │←─ 用户信息 ───│ │
│ │ │ │ │ │
│ │ │←─ JWT ──────│ │ │
│ │ │ (你的 Token) │ │ │
│ │←──────────│ │ │ │
│ │ 登录成功 │ │ │ │
│ │
│ 安全要点: │
│ • state 参数防 CSRF(随机值,回调时验证) │
│ • PKCE(code_challenge)防授权码拦截 │
│ • client_secret 只在后端使用,永不暴露给前端 │
│ • redirect_uri 必须精确匹配(不允许通配符) │
│ │
└─────────────────────────────────────────────────────────────┘3.2 主流 OAuth Provider 对比
| Provider | 用户基数 | 适用场景 | 特殊要求 | 获取信息 |
|---|---|---|---|---|
| 全球最广 | 通用 Web/Mobile 应用 | Google Cloud Console 配置 | email, name, picture, locale | |
| GitHub | 开发者群体 | 开发者工具、技术产品 | GitHub OAuth App 或 GitHub App | email, name, avatar, repos |
| Apple | iOS/Mac 用户 | iOS 应用(App Store 要求) | Apple Developer 账号,需处理”隐藏邮箱” | email(可能是 relay), name |
| Microsoft | 企业用户 | B2B/企业应用 | Azure AD 配置 | email, name, tenant |
| Discord | 游戏/社区 | 社区产品、游戏相关 | Discord Developer Portal | email, username, avatar |
| Twitter/X | 社交媒体用户 | 社交产品 | OAuth 2.0(新)或 OAuth 1.0a | username, name, profile_image |
3.3 操作步骤:AI 辅助实现 Google OAuth 登录
步骤 1:配置 Google Cloud Console
1. 访问 https://console.cloud.google.com/
2. 创建项目或选择已有项目
3. 启用 Google+ API 或 People API
4. 进入"凭据"页面,创建 OAuth 2.0 客户端 ID
5. 设置授权重定向 URI:
- 开发环境:http://localhost:3000/auth/google/callback
- 生产环境:https://yourdomain.com/auth/google/callback
6. 记录 Client ID 和 Client Secret步骤 2:使用 AI 生成 OAuth 集成代码
提示词模板:生成 OAuth 2.0 社交登录
你是一位后端安全专家。请为 [框架] 项目实现 Google OAuth 2.0 社交登录。
## 技术栈
- 框架:[Express / NestJS / Fastify / Hono]
- 数据库:[PostgreSQL + Prisma / MongoDB + Mongoose]
- 已有认证:JWT(Access Token + Refresh Token)
## 功能要求
1. Google OAuth 2.0 Authorization Code Flow + PKCE
2. 首次登录自动创建用户(注册)
3. 已有用户直接登录
4. 支持账号关联(同一邮箱的密码账号和 Google 账号关联)
5. 登录成功后签发 JWT Token 对
## 安全要求
1. 使用 state 参数防 CSRF
2. 使用 PKCE(code_verifier + code_challenge)
3. client_secret 从环境变量读取
4. redirect_uri 精确匹配
5. 验证 id_token 的签名和声明
6. 处理 Google 返回的 email_verified 字段
## 数据库 Schema
用户表需支持多种登录方式:
- id, email, name, avatar
- passwordHash(可为空,社交登录用户无密码)
- providers: [{ provider: 'google', providerId: '...', ... }]
## 输出
1. OAuth 路由(/auth/google, /auth/google/callback)
2. OAuth 服务(处理授权码交换和用户创建/关联)
3. 数据库 Schema(用户表 + OAuth 账号表)
4. 环境变量配置示例步骤 3:实现 OAuth 服务核心逻辑
// ============================================
// auth/oauth.service.ts —— OAuth 服务
// ============================================
interface OAuthUserInfo {
provider: 'google' | 'github' | 'apple';
providerId: string;
email: string;
emailVerified: boolean;
name: string;
avatar?: string;
}
class OAuthService {
constructor(
private readonly userRepo: UserRepository,
private readonly oauthAccountRepo: OAuthAccountRepository,
private readonly tokenService: TokenService,
) {}
/**
* 处理 OAuth 回调
* 核心逻辑:
* 1. 检查是否已有关联的 OAuth 账号 → 直接登录
* 2. 检查是否已有相同邮箱的用户 → 关联账号
* 3. 都没有 → 创建新用户
*/
async handleOAuthCallback(userInfo: OAuthUserInfo): Promise<TokenPair> {
// 1. 查找已关联的 OAuth 账号
const existingOAuth = await this.oauthAccountRepo.findByProviderAndId(
userInfo.provider,
userInfo.providerId,
);
if (existingOAuth) {
// 已有关联 → 直接登录
const user = await this.userRepo.findById(existingOAuth.userId);
if (!user) throw new Error('USER_NOT_FOUND');
return this.tokenService.generateTokenPair({
sub: user.id,
role: user.role,
});
}
// 2. 查找相同邮箱的用户
if (userInfo.email && userInfo.emailVerified) {
const existingUser = await this.userRepo.findByEmail(userInfo.email);
if (existingUser) {
// 邮箱已存在 → 关联 OAuth 账号
await this.oauthAccountRepo.create({
userId: existingUser.id,
provider: userInfo.provider,
providerId: userInfo.providerId,
email: userInfo.email,
});
return this.tokenService.generateTokenPair({
sub: existingUser.id,
role: existingUser.role,
});
}
}
// 3. 创建新用户
const newUser = await this.userRepo.create({
email: userInfo.email,
name: userInfo.name,
avatar: userInfo.avatar,
emailVerified: userInfo.emailVerified,
role: 'user',
});
await this.oauthAccountRepo.create({
userId: newUser.id,
provider: userInfo.provider,
providerId: userInfo.providerId,
email: userInfo.email,
});
return this.tokenService.generateTokenPair({
sub: newUser.id,
role: newUser.role,
});
}
}3.4 多 Provider 统一架构
当需要支持多个 OAuth Provider 时,使用策略模式统一处理:
// ============================================
// auth/providers/ —— OAuth Provider 策略模式
// ============================================
interface OAuthProvider {
name: string;
getAuthorizationUrl(state: string, codeChallenge: string): string;
exchangeCode(code: string, codeVerifier: string): Promise<OAuthTokens>;
getUserInfo(accessToken: string): Promise<OAuthUserInfo>;
}
// Google Provider
class GoogleOAuthProvider implements OAuthProvider {
name = 'google';
getAuthorizationUrl(state: string, codeChallenge: string): string {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
redirect_uri: process.env.GOOGLE_REDIRECT_URI!,
response_type: 'code',
scope: 'openid email profile',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
access_type: 'offline', // 获取 refresh_token
prompt: 'consent', // 强制显示同意页面
});
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
}
async exchangeCode(code: string, codeVerifier: string): Promise<OAuthTokens> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
redirect_uri: process.env.GOOGLE_REDIRECT_URI!,
grant_type: 'authorization_code',
code_verifier: codeVerifier,
}),
});
return response.json();
}
async getUserInfo(accessToken: string): Promise<OAuthUserInfo> {
const response = await fetch(
'https://www.googleapis.com/oauth2/v3/userinfo',
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
const data = await response.json();
return {
provider: 'google',
providerId: data.sub,
email: data.email,
emailVerified: data.email_verified,
name: data.name,
avatar: data.picture,
};
}
}
// GitHub Provider
class GitHubOAuthProvider implements OAuthProvider {
name = 'github';
getAuthorizationUrl(state: string, _codeChallenge: string): string {
const params = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
redirect_uri: process.env.GITHUB_REDIRECT_URI!,
scope: 'read:user user:email',
state,
});
return `https://github.com/login/oauth/authorize?${params}`;
}
async exchangeCode(code: string, _codeVerifier: string): Promise<OAuthTokens> {
const response = await fetch(
'https://github.com/login/oauth/access_token',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID!,
client_secret: process.env.GITHUB_CLIENT_SECRET!,
code,
redirect_uri: process.env.GITHUB_REDIRECT_URI!,
}),
},
);
return response.json();
}
async getUserInfo(accessToken: string): Promise<OAuthUserInfo> {
const [userRes, emailsRes] = await Promise.all([
fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${accessToken}` },
}),
fetch('https://api.github.com/user/emails', {
headers: { Authorization: `Bearer ${accessToken}` },
}),
]);
const user = await userRes.json();
const emails = await emailsRes.json();
const primaryEmail = emails.find(
(e: any) => e.primary && e.verified
);
return {
provider: 'github',
providerId: String(user.id),
email: primaryEmail?.email ?? user.email,
emailVerified: primaryEmail?.verified ?? false,
name: user.name ?? user.login,
avatar: user.avatar_url,
};
}
}
// Provider 注册表
const oauthProviders: Record<string, OAuthProvider> = {
google: new GoogleOAuthProvider(),
github: new GitHubOAuthProvider(),
// apple: new AppleOAuthProvider(),
};3.5 Apple 登录的特殊处理
Apple Sign In 有几个独特的要求,AI 生成代码时经常遗漏:
Apple Sign In 特殊要点:
1. 【App Store 要求】如果 iOS 应用提供第三方登录,必须同时提供 Apple 登录
2. 【隐藏邮箱】用户可选择隐藏真实邮箱,Apple 会提供 relay 邮箱
(格式:xxxxx@privaterelay.appleid.com)
3. 【用户信息只返回一次】首次授权时返回 name,后续不再返回
→ 必须在首次回调时保存用户名
4. 【JWT 验证】Apple 返回 id_token(JWT),需用 Apple 公钥验证
→ 公钥从 https://appleid.apple.com/auth/keys 获取
5. 【Client Secret 是 JWT】Apple 的 client_secret 需要用你的私钥动态生成
→ 有效期最长 6 个月
6. 【回调方式】Apple 使用 POST 方式回调(不是 GET)
→ 需要处理 form-encoded body4. 密码哈希:bcrypt vs Argon2
4.1 密码哈希算法对比
密码哈希是认证系统的最后一道防线。选择正确的哈希算法至关重要:
| 维度 | bcrypt | Argon2id | scrypt | PBKDF2 |
|---|---|---|---|---|
| 推荐度(2025) | ⭐⭐⭐⭐ 推荐 | ⭐⭐⭐⭐⭐ 首选 | ⭐⭐⭐ 可用 | ⭐⭐ 遗留系统 |
| 设计年份 | 1999 | 2015(PHC 冠军) | 2009 | 2000 |
| 内存硬度 | 固定 4KB | 可配置(推荐 64MB+) | 可配置 | 无 |
| GPU/ASIC 抗性 | 中等 | 极强 | 强 | 弱 |
| 并行度控制 | 无 | 可配置 | 无 | 无 |
| 密码长度限制 | 72 字节 | 无限制 | 无限制 | 无限制 |
| 生态成熟度 | 极高(所有语言) | 高(主流语言) | 中 | 极高 |
| OWASP 推荐 | ✅ 推荐 | ✅ 首选推荐 | ✅ 推荐 | ✅ 可接受 |
| 适用场景 | 通用 Web 应用 | 新项目首选 | 加密货币相关 | 遗留系统兼容 |
2025 年推荐策略:
- 新项目:优先使用 Argon2id
- 已有项目使用 bcrypt:无需迁移,bcrypt 仍然安全
- 遗留系统使用 MD5/SHA:必须迁移到 bcrypt 或 Argon2id
4.2 操作步骤:实现安全的密码哈希
// ============================================
// auth/password.service.ts —— 密码哈希服务
// ============================================
import { hash, verify } from '@node-rs/argon2';
// 备选:import bcrypt from 'bcryptjs';
interface PasswordConfig {
// Argon2id 推荐参数(OWASP 2025)
memoryCost: number; // 内存成本:65536 KB(64 MB)
timeCost: number; // 时间成本:3 次迭代
parallelism: number; // 并行度:1
hashLength: number; // 哈希长度:32 字节
}
const DEFAULT_CONFIG: PasswordConfig = {
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 1,
hashLength: 32,
};
class PasswordService {
constructor(private readonly config: PasswordConfig = DEFAULT_CONFIG) {}
/**
* 哈希密码
* 安全要点:
* 1. 使用 Argon2id(兼具抗侧信道和抗 GPU 攻击)
* 2. 自动生成随机 salt(内置于输出中)
* 3. 参数编码在哈希字符串中(自描述格式)
*/
async hashPassword(plainPassword: string): Promise<string> {
// 密码强度预检查
if (plainPassword.length < 8) {
throw new Error('密码长度不能少于 8 个字符');
}
return hash(plainPassword, {
memoryCost: this.config.memoryCost,
timeCost: this.config.timeCost,
parallelism: this.config.parallelism,
outputLen: this.config.hashLength,
});
// 输出格式:$argon2id$v=19$m=65536,t=3,p=1$salt$hash
}
/**
* 验证密码
* 安全要点:
* 1. 使用恒定时间比较(防止时序攻击)
* 2. 从哈希字符串中自动提取参数
*/
async verifyPassword(
plainPassword: string,
hashedPassword: string,
): Promise<boolean> {
return verify(hashedPassword, plainPassword);
}
/**
* 检查是否需要重新哈希
* 场景:升级哈希参数后,用户下次登录时自动升级
*/
needsRehash(hashedPassword: string): boolean {
// 检查是否是旧格式(bcrypt)或旧参数
if (hashedPassword.startsWith('$2b$') || hashedPassword.startsWith('$2a$')) {
return true; // bcrypt → 需要迁移到 Argon2id
}
// 检查 Argon2 参数是否是最新推荐值
const match = hashedPassword.match(/m=(\d+),t=(\d+),p=(\d+)/);
if (!match) return true;
const [, m, t, p] = match.map(Number);
return (
m < this.config.memoryCost ||
t < this.config.timeCost ||
p < this.config.parallelism
);
}
}4.3 密码哈希迁移策略
从旧算法(bcrypt/MD5/SHA)迁移到 Argon2id 的渐进式策略:
密码哈希迁移策略(零停机):
阶段 1:双重验证
┌─────────────────────────────────────────┐
│ 用户登录时: │
│ 1. 尝试用 Argon2id 验证 │
│ 2. 如果失败,尝试用旧算法(bcrypt)验证 │
│ 3. 如果旧算法验证成功: │
│ a. 用 Argon2id 重新哈希密码 │
│ b. 更新数据库中的哈希值 │
│ c. 记录迁移日志 │
│ 4. 登录成功 │
└─────────────────────────────────────────┘
阶段 2:监控迁移进度
┌─────────────────────────────────────────┐
│ 定期检查: │
│ • 已迁移用户数 / 总用户数 │
│ • 目标:90% 用户在 3 个月内自然迁移 │
│ • 对长期未登录用户:下次登录时强制迁移 │
└─────────────────────────────────────────┘
阶段 3:清理旧算法
┌─────────────────────────────────────────┐
│ 当迁移率 > 95% 时: │
│ • 移除旧算法验证代码 │
│ • 未迁移用户下次登录时要求重置密码 │
└─────────────────────────────────────────┘提示词模板:AI 生成密码策略
你是一位安全工程师。请为 [项目名称] 设计完整的密码安全策略。
## 要求
1. **密码强度规则**:
- 最小长度、字符类型要求
- 常见密码黑名单检查(如 "password123")
- 密码强度评分(弱/中/强)
2. **哈希算法**:
- 使用 Argon2id,参数符合 OWASP 2025 推荐
- 包含从 [当前算法] 的迁移策略
3. **密码重置流程**:
- 安全的重置 Token 生成(一次性、有过期时间)
- 邮件发送(不在 URL 中暴露用户信息)
- 重置后使所有现有 session 失效
4. **暴力破解防护**:
- 登录失败次数限制
- 渐进式延迟(1s → 2s → 4s → 8s → 锁定)
- IP 级别和账号级别的双重限制
## 技术栈
[你的技术栈]5. RBAC 权限模型
5.1 权限模型对比
| 模型 | 全称 | 核心概念 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| ACL | Access Control List | 资源 → 用户/权限列表 | 低 | 文件系统、简单应用 |
| RBAC | Role-Based Access Control | 用户 → 角色 → 权限 | 中 | 大多数 Web 应用 |
| ABAC | Attribute-Based Access Control | 基于属性的动态策略 | 高 | 企业级、合规要求高 |
| ReBAC | Relationship-Based Access Control | 基于关系图的权限 | 高 | 社交网络、协作工具 |
2025 年推荐: 大多数项目从 RBAC 开始,按需扩展到 ABAC。
5.2 RBAC 数据模型设计
┌─────────────────────────────────────────────────────────────┐
│ RBAC 数据模型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Users │───→│ UserRoles │←───│ Roles │ │
│ │ │ │ │ │ │ │
│ │ id │ │ userId │ │ id │ │
│ │ email │ │ roleId │ │ name │ │
│ │ name │ │ orgId? │ │ description │ │
│ └──────────┘ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ RolePermissions │ │
│ │ │ │
│ │ roleId │ │
│ │ permissionId │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Permissions │ │
│ │ │ │
│ │ id │ │
│ │ resource │ │
│ │ action │ │
│ │ description │ │
│ └─────────────────┘ │
│ │
│ 示例数据: │
│ Roles: admin, editor, viewer │
│ Permissions: posts:create, posts:read, posts:update, │
│ posts:delete, users:manage, settings:edit │
│ admin → [所有权限] │
│ editor → [posts:create, posts:read, posts:update] │
│ viewer → [posts:read] │
│ │
└─────────────────────────────────────────────────────────────┘5.3 操作步骤:AI 辅助实现 RBAC
步骤 1:定义权限和角色的数据库 Schema
// prisma/schema.prisma —— RBAC 数据模型
model User {
id String @id @default(uuid())
email String @unique
name String
passwordHash String?
emailVerified Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 关联
userRoles UserRole[]
oauthAccounts OAuthAccount[]
}
model Role {
id String @id @default(uuid())
name String @unique // admin, editor, viewer
description String?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
// 关联
userRoles UserRole[]
rolePermissions RolePermission[]
}
model Permission {
id String @id @default(uuid())
resource String // posts, users, settings
action String // create, read, update, delete, manage
description String?
// 关联
rolePermissions RolePermission[]
@@unique([resource, action])
}
model UserRole {
id String @id @default(uuid())
userId String
roleId String
orgId String? // 多租户:角色可以限定在组织范围内
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@unique([userId, roleId, orgId])
}
model RolePermission {
id String @id @default(uuid())
roleId String
permissionId String
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@unique([roleId, permissionId])
}
model OAuthAccount {
id String @id @default(uuid())
userId String
provider String // google, github, apple
providerId String // Provider 端的用户 ID
email String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerId])
}步骤 2:实现权限检查服务
// ============================================
// auth/permission.service.ts —— 权限检查服务
// ============================================
interface PermissionCheck {
resource: string; // 如 "posts"
action: string; // 如 "create"
}
class PermissionService {
constructor(
private readonly prisma: PrismaClient,
private readonly cache: RedisClient, // 权限缓存
) {}
/**
* 检查用户是否有指定权限
* 性能优化:使用 Redis 缓存用户权限集合
*/
async hasPermission(
userId: string,
check: PermissionCheck,
orgId?: string,
): Promise<boolean> {
const permissions = await this.getUserPermissions(userId, orgId);
const required = `${check.resource}:${check.action}`;
// 检查精确匹配或通配符
return (
permissions.has(required) ||
permissions.has(`${check.resource}:*`) ||
permissions.has('*:*') // 超级管理员
);
}
/**
* 获取用户的所有权限(带缓存)
*/
async getUserPermissions(
userId: string,
orgId?: string,
): Promise<Set<string>> {
const cacheKey = `permissions:${userId}:${orgId ?? 'global'}`;
// 1. 尝试从缓存读取
const cached = await this.cache.smembers(cacheKey);
if (cached.length > 0) {
return new Set(cached);
}
// 2. 从数据库查询
const userRoles = await this.prisma.userRole.findMany({
where: {
userId,
...(orgId ? { orgId } : {}),
},
include: {
role: {
include: {
rolePermissions: {
include: { permission: true },
},
},
},
},
});
const permissions = new Set<string>();
for (const ur of userRoles) {
for (const rp of ur.role.rolePermissions) {
permissions.add(`${rp.permission.resource}:${rp.permission.action}`);
}
}
// 3. 写入缓存(TTL 5 分钟)
if (permissions.size > 0) {
await this.cache.sadd(cacheKey, ...permissions);
await this.cache.expire(cacheKey, 300);
}
return permissions;
}
/**
* 清除用户权限缓存
* 在角色变更时调用
*/
async invalidateCache(userId: string): Promise<void> {
const keys = await this.cache.keys(`permissions:${userId}:*`);
if (keys.length > 0) {
await this.cache.del(...keys);
}
}
}步骤 3:实现授权中间件
// ============================================
// auth/authorize.middleware.ts —— 授权中间件
// ============================================
/**
* 权限检查中间件工厂
* 用法:router.post('/posts', authorize('posts', 'create'), createPost)
*/
function authorize(resource: string, action: string) {
return async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
) => {
if (!req.user) {
return res.status(401).json({
error: 'UNAUTHENTICATED',
message: '请先登录',
});
}
const hasPermission = await permissionService.hasPermission(
req.user.id,
{ resource, action },
req.user.orgId,
);
if (!hasPermission) {
// 安全要点:不要告诉用户"需要什么权限"
// 避免信息泄露
return res.status(403).json({
error: 'FORBIDDEN',
message: '没有权限执行此操作',
});
}
next();
};
}
/**
* 角色检查中间件(简化版)
* 用法:router.delete('/users/:id', requireRole('admin'), deleteUser)
*/
function requireRole(...roles: string[]) {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'UNAUTHENTICATED' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'FORBIDDEN' });
}
next();
};
}
// ============================================
// 路由使用示例
// ============================================
// 文章路由
router.get('/posts', authMiddleware, authorize('posts', 'read'), listPosts);
router.post('/posts', authMiddleware, authorize('posts', 'create'), createPost);
router.put('/posts/:id', authMiddleware, authorize('posts', 'update'), updatePost);
router.delete('/posts/:id', authMiddleware, authorize('posts', 'delete'), deletePost);
// 管理路由
router.get('/admin/users', authMiddleware, requireRole('admin'), listUsers);
router.put('/admin/roles', authMiddleware, requireRole('admin'), updateRoles);提示词模板:AI 生成 RBAC 权限系统
你是一位后端架构师。请为 [项目名称] 设计并实现 RBAC 权限系统。
## 业务需求
[描述你的角色和权限需求]
示例:
- 超级管理员(super_admin):所有权限
- 管理员(admin):用户管理、内容管理、设置管理
- 编辑(editor):内容创建、编辑、发布
- 作者(author):创建和编辑自己的内容
- 查看者(viewer):只读访问
## 技术要求
1. 数据库 Schema([Prisma / Drizzle / TypeORM])
2. 权限检查服务(带 Redis 缓存)
3. 授权中间件(Express / NestJS Guard)
4. 种子数据脚本(初始化角色和权限)
5. 管理 API(CRUD 角色和权限分配)
## 高级需求(可选)
- [ ] 多租户支持(角色限定在组织范围内)
- [ ] 资源级权限("只能编辑自己的文章")
- [ ] 权限继承(editor 继承 viewer 的所有权限)
- [ ] 临时权限(有过期时间的权限授予)
- [ ] 审计日志(记录权限变更历史)
## 安全要求
- 权限检查必须在服务端执行(不信任客户端)
- 403 响应不泄露权限细节
- 角色变更后立即清除缓存
- 默认拒绝(deny by default):未明确授权的操作一律拒绝6. Session 管理
6.1 Session vs JWT:何时选择 Session
| 维度 | Session(服务端状态) | JWT(无状态) |
|---|---|---|
| 状态存储 | 服务端(Redis/数据库) | 客户端(Token 自包含) |
| 可撤销性 | 即时撤销(删除 session) | 需要黑名单机制 |
| 扩展性 | 需要共享 session 存储 | 天然支持分布式 |
| 安全性 | 服务端控制,更安全 | Token 泄露风险更高 |
| 性能 | 每次请求查询 session 存储 | 无需查询,本地验证 |
| 适用场景 | 传统 Web 应用、高安全要求 | API 服务、微服务、移动端 |
| AI 生成难度 | 中等 | 较高(容易出安全漏洞) |
推荐策略:
- 纯 API 服务(前后端分离):JWT
- 传统 SSR Web 应用:Session
- 混合架构:Session + JWT(Session 管理登录状态,JWT 用于 API 调用)
6.2 安全的 Session 实现
// ============================================
// session/session.config.ts —— Session 配置(Express + Redis)
// ============================================
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({
url: process.env.REDIS_URL,
});
const sessionConfig: session.SessionOptions = {
store: new RedisStore({
client: redisClient,
prefix: 'sess:',
ttl: 86400, // 24 小时
}),
name: '__session', // 自定义 Cookie 名(不用默认的 connect.sid)
secret: process.env.SESSION_SECRET!, // 至少 256 位随机字符串
resave: false, // 不强制重新保存未修改的 session
saveUninitialized: false, // 不保存空 session(GDPR 合规)
cookie: {
httpOnly: true, // 防止 XSS 读取
secure: process.env.NODE_ENV === 'production', // 生产环境仅 HTTPS
sameSite: 'lax', // 防止 CSRF(lax 允许顶级导航)
maxAge: 24 * 60 * 60 * 1000, // 24 小时
domain: process.env.COOKIE_DOMAIN, // 限定域名
path: '/',
},
// 滚动过期:活跃用户自动续期
rolling: true,
};
// 安全要点:生产环境必须设置 trust proxy
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}
app.use(session(sessionConfig));6.3 Session 安全最佳实践
Session 安全清单:
✅ Session ID 使用加密安全的随机数生成器
✅ 登录成功后重新生成 Session ID(防止 Session Fixation)
✅ 登出时销毁 Session(服务端 + 客户端 Cookie)
✅ 设置合理的过期时间(活跃 session 24h,空闲 session 30min)
✅ Cookie 设置 HttpOnly + Secure + SameSite
✅ 使用 Redis/数据库存储 Session(不用内存存储)
✅ 限制每个用户的并发 Session 数量
✅ 敏感操作(改密码、改邮箱)要求重新认证
✅ 记录 Session 创建的 IP 和 User-Agent(异常检测)
✅ 定期清理过期 Session// Session 安全操作示例
// 登录成功后:重新生成 Session ID
app.post('/auth/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password);
// 防止 Session Fixation 攻击
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'SESSION_ERROR' });
req.session.userId = user.id;
req.session.role = user.role;
req.session.loginAt = Date.now();
req.session.ip = req.ip;
req.session.userAgent = req.headers['user-agent'];
req.session.save((err) => {
if (err) return res.status(500).json({ error: 'SESSION_ERROR' });
res.json({ message: '登录成功' });
});
});
});
// 登出:彻底销毁 Session
app.post('/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'LOGOUT_ERROR' });
res.clearCookie('__session', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});
res.json({ message: '已登出' });
});
});7. AI 生成认证代码的常见安全漏洞
7.1 漏洞全景
研究表明,AI 生成的代码中认证相关的安全漏洞尤为突出。以下是最常见的漏洞类型及其修复方法:
AI 生成认证代码的 Top 10 安全漏洞:
┌─────────────────────────────────────────────────────────────┐
│ 排名 │ 漏洞类型 │ 出现频率 │ 严重程度 │
├────────┼───────────────────────┼──────────┼──────────────┤
│ 1 │ 硬编码密钥/密码 │ 极高 │ 🔴 严重 │
│ 2 │ 缺少输入验证 │ 极高 │ 🔴 严重 │
│ 3 │ 不安全的密码存储 │ 高 │ 🔴 严重 │
│ 4 │ JWT 配置错误 │ 高 │ 🔴 严重 │
│ 5 │ 缺少速率限制 │ 高 │ 🟡 中等 │
│ 6 │ 不安全的 Session 配置 │ 中 │ 🟡 中等 │
│ 7 │ CORS 配置过于宽松 │ 中 │ 🟡 中等 │
│ 8 │ 错误信息泄露 │ 中 │ 🟡 中等 │
│ 9 │ 缺少 CSRF 防护 │ 中 │ 🟡 中等 │
│ 10 │ 不安全的密码重置 │ 低 │ 🔴 严重 │
└─────────────────────────────────────────────────────────────┘7.2 漏洞详解与修复
漏洞 1:硬编码密钥
// ❌ AI 经常生成的危险代码
const JWT_SECRET = 'my-super-secret-key'; // 硬编码!
const token = jwt.sign(payload, JWT_SECRET);
// ✅ 正确做法
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET must be at least 32 characters');
}漏洞 2:缺少输入验证
// ❌ AI 经常生成的危险代码
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body; // 没有验证!
const user = await db.user.findUnique({ where: { email } });
// ...
});
// ✅ 正确做法(使用 Zod 验证)
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email().max(255).toLowerCase().trim(),
password: z.string().min(8).max(128),
});
app.post('/auth/login', async (req, res) => {
const result = loginSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'VALIDATION_ERROR',
details: result.error.flatten(),
});
}
const { email, password } = result.data;
// ...
});漏洞 3:不安全的密码存储
// ❌ AI 经常生成的危险代码
import crypto from 'crypto';
const hash = crypto.createHash('sha256').update(password).digest('hex');
// SHA-256 不是密码哈希算法!没有 salt,没有迭代
// ❌ 另一个常见错误
import bcrypt from 'bcryptjs';
const hash = await bcrypt.hash(password, 4); // cost factor 太低!
// ✅ 正确做法
import { hash } from '@node-rs/argon2';
const passwordHash = await hash(password, {
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 1,
});
// 或者 bcrypt cost factor >= 12
const passwordHash = await bcrypt.hash(password, 12);漏洞 4:JWT 配置错误
// ❌ AI 经常生成的危险代码
const token = jwt.sign(
{ userId: user.id, email: user.email, password: user.password },
// 1. 在 payload 中包含密码!
'secret',
// 2. 弱密钥
// 3. 没有设置过期时间
);
// ✅ 正确做法
const token = await new jose.SignJWT({ role: user.role })
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.setSubject(user.id) // 只包含用户 ID
.setIssuedAt()
.setExpirationTime('15m') // 15 分钟过期
.setIssuer('https://myapp.com')
.setAudience('https://api.myapp.com')
.setJti(randomUUID()) // 唯一标识
.sign(privateKey); // RS256 私钥签名漏洞 5:缺少速率限制
// ❌ AI 经常忘记添加速率限制
app.post('/auth/login', loginHandler); // 无限制!可被暴力破解
// ✅ 正确做法
import rateLimit from 'express-rate-limit';
// 全局 API 限制
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
standardHeaders: true,
legacyHeaders: false,
});
// 登录端点严格限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5, // 每个 IP 最多 5 次登录尝试
message: { error: 'TOO_MANY_ATTEMPTS', message: '登录尝试过多,请稍后再试' },
skipSuccessfulRequests: true, // 成功的请求不计数
});
app.post('/auth/login', loginLimiter, loginHandler);
app.post('/auth/register', loginLimiter, registerHandler);
app.post('/auth/forgot-password', loginLimiter, forgotPasswordHandler);漏洞 6:错误信息泄露
// ❌ AI 经常生成的信息泄露代码
app.post('/auth/login', async (req, res) => {
const user = await db.user.findUnique({ where: { email } });
if (!user) {
return res.status(401).json({ error: '用户不存在' });
// 泄露了"该邮箱未注册"的信息!
}
if (!await verifyPassword(password, user.passwordHash)) {
return res.status(401).json({ error: '密码错误' });
// 泄露了"该邮箱已注册但密码错误"的信息!
}
});
// ✅ 正确做法:统一错误消息
app.post('/auth/login', async (req, res) => {
const user = await db.user.findUnique({ where: { email } });
// 即使用户不存在,也执行一次哈希比较(防止时序攻击)
const isValid = user
? await verifyPassword(password, user.passwordHash)
: await verifyPassword(password, DUMMY_HASH); // 恒定时间
if (!user || !isValid) {
return res.status(401).json({
error: 'INVALID_CREDENTIALS',
message: '邮箱或密码错误', // 统一消息
});
}
});8. 安全审查清单
8.1 认证实现安全审查清单
以下清单适用于审查 AI 生成的认证代码。每次 AI 生成认证相关代码后,逐项检查:
═══════════════════════════════════════════════════════════════
认证安全审查清单 v2025
═══════════════════════════════════════════════════════════════
【密码安全】
□ 使用 Argon2id 或 bcrypt(cost ≥ 12)哈希密码
□ 不使用 MD5、SHA-1、SHA-256 等通用哈希算法
□ 密码最小长度 ≥ 8 字符
□ 检查常见密码黑名单(如 Have I Been Pwned API)
□ 密码重置 Token 一次性使用,有过期时间(≤ 1 小时)
□ 密码变更后使所有现有 Session/Token 失效
【JWT 安全】
□ 使用 RS256 或 ES256 非对称算法(不用 HS256 + 弱密钥)
□ Access Token 过期时间 ≤ 15 分钟
□ Refresh Token 过期时间 ≤ 30 天
□ Refresh Token 使用 HttpOnly + Secure + SameSite Cookie
□ 实现 Refresh Token 轮换(每次使用后生成新 Token)
□ 实现 Token 黑名单(Redis,用于登出和 Token 撤销)
□ 检测 Refresh Token 重用(可能的攻击指标)
□ 验证 JWT 的 iss、aud、exp 声明
□ JWT Payload 不包含敏感信息(密码、密钥等)
□ 使用 jti 声明防止重放攻击
【OAuth 2.0 安全】
□ 使用 Authorization Code Flow(不用 Implicit Flow)
□ 实现 PKCE(code_challenge + code_verifier)
□ state 参数使用加密安全随机值,回调时验证
□ client_secret 只在后端使用,不暴露给前端
□ redirect_uri 精确匹配(不使用通配符)
□ 验证 OAuth Provider 返回的 id_token 签名
□ 处理 email_verified 字段(未验证的邮箱不自动关联)
【Session 安全】
□ Session ID 使用加密安全随机数生成
□ 登录成功后重新生成 Session ID
□ 登出时销毁 Session(服务端 + 清除 Cookie)
□ Cookie 设置 HttpOnly + Secure + SameSite
□ 使用 Redis/数据库存储 Session(不用内存)
□ 设置合理的 Session 过期时间
□ 限制每个用户的并发 Session 数量
【输入验证】
□ 所有认证端点使用 Schema 验证(Zod/Joi/Yup)
□ 邮箱格式验证 + 长度限制
□ 密码长度限制(最小 8,最大 128)
□ 防止 SQL 注入(使用参数化查询/ORM)
□ 防止 NoSQL 注入(验证输入类型)
【速率限制】
□ 登录端点:每 IP 每 15 分钟最多 5 次
□ 注册端点:每 IP 每小时最多 10 次
□ 密码重置:每 IP 每小时最多 3 次
□ Token 刷新:每用户每分钟最多 10 次
□ 失败后渐进式延迟(1s → 2s → 4s → 锁定)
【错误处理】
□ 登录失败使用统一错误消息(不区分"用户不存在"和"密码错误")
□ 错误响应不泄露内部实现细节
□ 用户不存在时仍执行密码哈希比较(防时序攻击)
□ 500 错误不返回堆栈跟踪
【密钥管理】
□ 所有密钥从环境变量或密钥管理服务加载
□ 代码中无硬编码密钥(包括测试代码)
□ JWT 签名密钥定期轮换
□ 数据库连接字符串不在代码中
□ .env 文件在 .gitignore 中
【HTTPS 与传输安全】
□ 生产环境强制 HTTPS
□ 设置 HSTS 头(Strict-Transport-Security)
□ Cookie 设置 Secure 标志
□ 禁用 HTTP 降级
【日志与监控】
□ 记录所有认证事件(登录、登出、失败、Token 刷新)
□ 不在日志中记录密码、Token、密钥
□ 异常登录检测(新设备、新 IP、异常时间)
□ 设置认证失败告警阈值
═══════════════════════════════════════════════════════════════8.2 AI 代码审查提示词模板
提示词模板:认证代码安全审查
你是一位应用安全审计专家(OWASP Top 10 认证)。请对以下认证相关代码进行全面安全审查。
## 代码
[粘贴你的认证代码]
## 审查范围
1. 密码存储安全性
2. Token 生成和验证
3. Session 管理
4. 输入验证
5. 错误处理(信息泄露)
6. 速率限制
7. CSRF/XSS 防护
8. 密钥管理
9. 日志安全(是否记录敏感信息)
10. 依赖安全(已知漏洞)
## 输出格式
对每个发现的问题,请提供:
1. **严重程度**:🔴 严重 / 🟡 中等 / 🟢 低
2. **漏洞类型**:对应 OWASP Top 10 或 CWE 编号
3. **问题描述**:具体说明漏洞位置和原因
4. **攻击场景**:攻击者如何利用此漏洞
5. **修复代码**:提供修复后的代码片段
6. **验证方法**:如何确认漏洞已修复
最后提供一个总体安全评分(A-F)和优先修复建议。提示词模板:Steering 规则——认证安全
# 认证安全 Steering 规则
## 密码处理
- 始终使用 Argon2id 或 bcrypt(cost >= 12)哈希密码
- 永远不要使用 MD5、SHA-1、SHA-256 存储密码
- 永远不要在日志、错误消息或 API 响应中包含密码
## JWT 实现
- 使用 RS256 或 ES256 算法,不使用 HS256
- Access Token 过期时间不超过 15 分钟
- Refresh Token 存储在 HttpOnly + Secure Cookie 中
- 实现 Refresh Token 轮换
- 始终验证 iss、aud、exp 声明
## 密钥管理
- 所有密钥必须从环境变量加载
- 永远不要在代码中硬编码密钥
- 测试代码中使用专用测试密钥
## 错误处理
- 认证失败使用统一消息:"邮箱或密码错误"
- 不区分"用户不存在"和"密码错误"
- 500 错误不返回堆栈跟踪
## 速率限制
- 所有认证端点必须有速率限制
- 登录:每 IP 每 15 分钟最多 5 次
- 注册:每 IP 每小时最多 10 次
## 输入验证
- 所有输入必须使用 Zod/Joi 验证
- 邮箱必须验证格式和长度
- 密码长度限制:8-128 字符实战案例:从零构建完整认证系统
案例背景
一个 SaaS 产品需要完整的认证系统,支持邮箱/密码注册、Google/GitHub 社交登录、RBAC 权限控制。技术栈:Express + Prisma + PostgreSQL + Redis。
案例流程
完整认证系统构建流程(AI 辅助 + 人工审查):
第 1 步:需求规约(15 分钟)
┌─────────────────────────────────────────────┐
│ 使用 AI 生成认证需求规约: │
│ • 用户注册(邮箱/密码 + 社交登录) │
│ • 用户登录(JWT + Refresh Token 轮换) │
│ • 密码重置(邮件 Token) │
│ • RBAC(admin, editor, viewer) │
│ • 安全要求(速率限制、输入验证、日志) │
│ │
│ 人工审查:确认业务规则完整性 │
└─────────────────────────────────────────────┘
↓
第 2 步:技术设计(20 分钟)
┌─────────────────────────────────────────────┐
│ 使用 AI 生成技术设计: │
│ • 数据库 Schema(User, Role, Permission, │
│ OAuthAccount, RefreshToken) │
│ • JWT 配置(RS256, 15min AT, 7d RT) │
│ • API 端点设计 │
│ • 中间件架构 │
│ │
│ 人工审查:确认安全架构合理性 │
└─────────────────────────────────────────────┘
↓
第 3 步:逐模块实现(AI 生成 + 安全审查)
┌─────────────────────────────────────────────┐
│ 3a. 密码哈希服务 │
│ AI 生成 → 安全审查清单检查 → 修复 │
│ │
│ 3b. JWT Token 服务 │
│ AI 生成 → 安全审查清单检查 → 修复 │
│ │
│ 3c. OAuth 集成(Google + GitHub) │
│ AI 生成 → 安全审查清单检查 → 修复 │
│ │
│ 3d. RBAC 权限系统 │
│ AI 生成 → 安全审查清单检查 → 修复 │
│ │
│ 3e. 认证中间件 + 路由 │
│ AI 生成 → 安全审查清单检查 → 修复 │
│ │
│ 3f. 速率限制 + 输入验证 │
│ AI 生成 → 安全审查清单检查 → 修复 │
└─────────────────────────────────────────────┘
↓
第 4 步:安全测试(30 分钟)
┌─────────────────────────────────────────────┐
│ 使用 AI 生成安全测试用例: │
│ • 暴力破解测试(速率限制是否生效) │
│ • Token 过期测试 │
│ • Token 重用检测测试 │
│ • 权限越权测试 │
│ • SQL 注入测试 │
│ • XSS 测试 │
│ │
│ 人工执行:运行测试并验证结果 │
└─────────────────────────────────────────────┘案例分析
关键决策点:
- 选择 RS256 而非 HS256:非对称算法允许公钥分发给微服务验证 Token,私钥只在认证服务中保存
- Refresh Token 存数据库而非仅 JWT:支持即时撤销和重用检测
- RBAC 使用 Redis 缓存:权限检查是高频操作,缓存避免每次查询数据库
- OAuth 使用策略模式:新增 Provider 只需实现接口,不修改核心逻辑
- 安全审查在每个模块完成后立即执行:不要等到全部完成再审查
AI 生成代码中发现的典型问题:
| 模块 | AI 生成的问题 | 修复方式 |
|---|---|---|
| 密码哈希 | 使用 bcrypt cost=10(太低) | 改为 cost=12 或 Argon2id |
| JWT | 没有设置 iss 和 aud | 添加签发者和受众验证 |
| OAuth | 没有验证 state 参数 | 添加 CSRF 防护 |
| RBAC | 权限检查在前端执行 | 移到服务端中间件 |
| 登录 | 区分”用户不存在”和”密码错误” | 统一错误消息 |
| 速率限制 | 完全没有 | 添加 express-rate-limit |
避坑指南
❌ 常见错误
-
直接使用 AI 生成的认证代码上线
- 问题:AI 生成的认证代码平均包含 2-3 个安全漏洞,最常见的是硬编码密钥和缺少速率限制
- 正确做法:每次 AI 生成认证代码后,必须用安全审查清单逐项检查,重点关注密钥管理、Token 配置和输入验证
-
将 JWT 存储在 localStorage
- 问题:localStorage 可被 XSS 攻击读取,一旦注入恶意脚本即可窃取所有 Token
- 正确做法:Access Token 存在内存变量中(页面刷新后通过 Refresh Token 重新获取),Refresh Token 存在 HttpOnly Cookie 中
-
使用 SHA-256 哈希密码
- 问题:SHA-256 是通用哈希算法,不是密码哈希算法——没有 salt、没有迭代、GPU 每秒可计算数十亿次
- 正确做法:使用 Argon2id(首选)或 bcrypt(cost ≥ 12),它们专为密码哈希设计,具有内存硬度和自适应迭代
-
OAuth 回调不验证 state 参数
- 问题:缺少 state 验证使应用容易受到 CSRF 攻击,攻击者可以将受害者的账号关联到攻击者的 OAuth 账号
- 正确做法:生成加密安全的随机 state 值,存储在 session 中,回调时严格比对
-
Refresh Token 不轮换
- 问题:如果 Refresh Token 被盗,攻击者可以无限期获取新的 Access Token
- 正确做法:每次使用 Refresh Token 后生成新的 Token 对,旧 Token 立即失效,检测到 Token 重用时撤销该用户所有 Token
-
RBAC 权限检查只在前端执行
- 问题:前端权限检查只是 UI 层面的隐藏,攻击者可以直接调用 API 绕过
- 正确做法:权限检查必须在服务端中间件中执行,前端权限检查仅用于 UI 展示优化
-
登录错误消息区分”用户不存在”和”密码错误”
- 问题:攻击者可以通过错误消息枚举有效的邮箱地址
- 正确做法:统一返回”邮箱或密码错误”,即使用户不存在也执行一次密码哈希比较(防止时序攻击)
-
忘记对认证端点添加速率限制
- 问题:没有速率限制的登录端点可以被暴力破解,密码重置端点可以被滥用发送垃圾邮件
- 正确做法:登录端点每 IP 每 15 分钟最多 5 次尝试,密码重置每 IP 每小时最多 3 次
✅ 最佳实践
- “AI 生成,人工审查”原则:认证代码是安全关键代码,AI 生成后必须经过安全审查清单的逐项检查
- 使用成熟的认证库:优先使用 Clerk、Auth0、Better Auth 等成熟方案,而非从零实现
- 最小权限原则:默认拒绝所有访问,只授予必要的最小权限
- 纵深防御:不依赖单一安全机制,组合使用 JWT + 速率限制 + 输入验证 + CORS + CSRF 防护
- 定期轮换密钥:JWT 签名密钥、Session 密钥、OAuth Client Secret 定期轮换
- 安全日志:记录所有认证事件,但不记录密码、Token 等敏感信息
- 依赖更新:定期更新认证相关依赖,关注安全公告
相关资源与延伸阅读
- OWASP Authentication Cheat Sheet —— OWASP 认证最佳实践速查表,涵盖密码存储、Session 管理、MFA 等核心主题
- OWASP Password Storage Cheat Sheet —— 密码哈希算法选择和参数配置的权威指南,包含 Argon2id 和 bcrypt 推荐参数
- RFC 7519 - JSON Web Token (JWT) —— JWT 标准规范,定义了 Token 结构、声明和验证规则
- RFC 6749 - OAuth 2.0 Authorization Framework —— OAuth 2.0 核心规范,定义了授权码流程、Token 端点等
- RFC 7636 - PKCE (Proof Key for Code Exchange) —— PKCE 扩展规范,防止授权码拦截攻击
- jose npm 包文档 —— 零依赖的 JWT/JWS/JWE/JWK 实现,支持 Node.js、Deno、Bun 和浏览器
- Clerk 文档 - Authentication —— Clerk 认证服务的完整文档,包含 Next.js、React、Express 集成指南
- Auth0 开发者文档 —— Auth0 平台的开发者文档,涵盖 Universal Login、RBAC、MFA 配置
- CASL 授权库文档 —— TypeScript 同构授权库,支持 RBAC 和 ABAC 模式
- Argon2 Password Hashing - IETF —— Argon2 算法的 IETF 标准文档(RFC 9106)
参考来源
- Common Vulnerabilities in AI-Generated Code (2025-06)—— AI 生成代码的常见安全漏洞分析
- The 2.74× Problem: AI Code Ships With Nearly 3× More Security Flaws (2025-08)—— AI 辅助代码的安全缺陷统计研究
- AI Agent Security Best Practices 2025 (2025-06)—— AI Agent 安全最佳实践指南
- Best Auth Provider Comparison: Clerk vs Auth0 vs Supabase vs Firebase (2026) (2025-02)—— 主流认证服务提供商对比
- Top 10 Authentication Services and Libraries in 2026 (2026-01)—— 认证服务和库的综合评测
- Argon2 vs bcrypt vs scrypt: Best Password Hashing Algorithm (2026-01)—— 密码哈希算法深度对比
- OAuth 2.1, PRMs, and Best Practices for AI Agents (2025-11)—— AI Agent 时代的 OAuth 2.1 授权实践
- Auth0 Token Vault: Secure Token Exchange for AI Agents (2025-06)—— Auth0 为 AI Agent 设计的安全 Token 交换方案
- RBAC Implementation in 5 Steps (2025-09)—— RBAC 权限模型实现指南
- From Auth to Action: Secure & Scalable AI Agent Infrastructure (2026) (2026-02)—— AI Agent 安全基础设施完整指南
📖 返回 总览与导航 | 上一节:28c-Spec-Driven业务逻辑 | 下一节:28e-微服务与Serverless