Skip to Content

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、PasskeyRBAC、ABAC、ACL
发生时机登录时每次请求时
失败响应401 Unauthorized403 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.jsNode.js 认证中间件500+ 策略、Express 集成、社区生态免费(开源)Express/Fastify 项目的 OAuth 集成
joseJWT 库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 存储在 localStorageXSS 攻击可窃取 TokenAccess Token 存内存,Refresh Token 存 HttpOnly Cookie
不验证 issaudToken 可被跨服务滥用始终验证签发者和受众
使用弱密钥(如 “secret”)暴力破解可伪造 TokenHS256 至少 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用户基数适用场景特殊要求获取信息
Google全球最广通用 Web/Mobile 应用Google Cloud Console 配置email, name, picture, locale
GitHub开发者群体开发者工具、技术产品GitHub OAuth App 或 GitHub Appemail, name, avatar, repos
AppleiOS/Mac 用户iOS 应用(App Store 要求)Apple Developer 账号,需处理”隐藏邮箱”email(可能是 relay), name
Microsoft企业用户B2B/企业应用Azure AD 配置email, name, tenant
Discord游戏/社区社区产品、游戏相关Discord Developer Portalemail, username, avatar
Twitter/X社交媒体用户社交产品OAuth 2.0(新)或 OAuth 1.0ausername, 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 body

4. 密码哈希:bcrypt vs Argon2

4.1 密码哈希算法对比

密码哈希是认证系统的最后一道防线。选择正确的哈希算法至关重要:

维度bcryptArgon2idscryptPBKDF2
推荐度(2025)⭐⭐⭐⭐ 推荐⭐⭐⭐⭐⭐ 首选⭐⭐⭐ 可用⭐⭐ 遗留系统
设计年份19992015(PHC 冠军)20092000
内存硬度固定 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 权限模型对比

模型全称核心概念复杂度适用场景
ACLAccess Control List资源 → 用户/权限列表文件系统、简单应用
RBACRole-Based Access Control用户 → 角色 → 权限大多数 Web 应用
ABACAttribute-Based Access Control基于属性的动态策略企业级、合规要求高
ReBACRelationship-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 测试 │ │ │ │ 人工执行:运行测试并验证结果 │ └─────────────────────────────────────────────┘

案例分析

关键决策点:

  1. 选择 RS256 而非 HS256:非对称算法允许公钥分发给微服务验证 Token,私钥只在认证服务中保存
  2. Refresh Token 存数据库而非仅 JWT:支持即时撤销和重用检测
  3. RBAC 使用 Redis 缓存:权限检查是高频操作,缓存避免每次查询数据库
  4. OAuth 使用策略模式:新增 Provider 只需实现接口,不修改核心逻辑
  5. 安全审查在每个模块完成后立即执行:不要等到全部完成再审查

AI 生成代码中发现的典型问题:

模块AI 生成的问题修复方式
密码哈希使用 bcrypt cost=10(太低)改为 cost=12 或 Argon2id
JWT没有设置 iss 和 aud添加签发者和受众验证
OAuth没有验证 state 参数添加 CSRF 防护
RBAC权限检查在前端执行移到服务端中间件
登录区分”用户不存在”和”密码错误”统一错误消息
速率限制完全没有添加 express-rate-limit

避坑指南

❌ 常见错误

  1. 直接使用 AI 生成的认证代码上线

    • 问题:AI 生成的认证代码平均包含 2-3 个安全漏洞,最常见的是硬编码密钥和缺少速率限制
    • 正确做法:每次 AI 生成认证代码后,必须用安全审查清单逐项检查,重点关注密钥管理、Token 配置和输入验证
  2. 将 JWT 存储在 localStorage

    • 问题:localStorage 可被 XSS 攻击读取,一旦注入恶意脚本即可窃取所有 Token
    • 正确做法:Access Token 存在内存变量中(页面刷新后通过 Refresh Token 重新获取),Refresh Token 存在 HttpOnly Cookie 中
  3. 使用 SHA-256 哈希密码

    • 问题:SHA-256 是通用哈希算法,不是密码哈希算法——没有 salt、没有迭代、GPU 每秒可计算数十亿次
    • 正确做法:使用 Argon2id(首选)或 bcrypt(cost ≥ 12),它们专为密码哈希设计,具有内存硬度和自适应迭代
  4. OAuth 回调不验证 state 参数

    • 问题:缺少 state 验证使应用容易受到 CSRF 攻击,攻击者可以将受害者的账号关联到攻击者的 OAuth 账号
    • 正确做法:生成加密安全的随机 state 值,存储在 session 中,回调时严格比对
  5. Refresh Token 不轮换

    • 问题:如果 Refresh Token 被盗,攻击者可以无限期获取新的 Access Token
    • 正确做法:每次使用 Refresh Token 后生成新的 Token 对,旧 Token 立即失效,检测到 Token 重用时撤销该用户所有 Token
  6. RBAC 权限检查只在前端执行

    • 问题:前端权限检查只是 UI 层面的隐藏,攻击者可以直接调用 API 绕过
    • 正确做法:权限检查必须在服务端中间件中执行,前端权限检查仅用于 UI 展示优化
  7. 登录错误消息区分”用户不存在”和”密码错误”

    • 问题:攻击者可以通过错误消息枚举有效的邮箱地址
    • 正确做法:统一返回”邮箱或密码错误”,即使用户不存在也执行一次密码哈希比较(防止时序攻击)
  8. 忘记对认证端点添加速率限制

    • 问题:没有速率限制的登录端点可以被暴力破解,密码重置端点可以被滥用发送垃圾邮件
    • 正确做法:登录端点每 IP 每 15 分钟最多 5 次尝试,密码重置每 IP 每小时最多 3 次

✅ 最佳实践

  1. “AI 生成,人工审查”原则:认证代码是安全关键代码,AI 生成后必须经过安全审查清单的逐项检查
  2. 使用成熟的认证库:优先使用 Clerk、Auth0、Better Auth 等成熟方案,而非从零实现
  3. 最小权限原则:默认拒绝所有访问,只授予必要的最小权限
  4. 纵深防御:不依赖单一安全机制,组合使用 JWT + 速率限制 + 输入验证 + CORS + CSRF 防护
  5. 定期轮换密钥:JWT 签名密钥、Session 密钥、OAuth Client Secret 定期轮换
  6. 安全日志:记录所有认证事件,但不记录密码、Token 等敏感信息
  7. 依赖更新:定期更新认证相关依赖,关注安全公告

相关资源与延伸阅读

  1. OWASP Authentication Cheat Sheet  —— OWASP 认证最佳实践速查表,涵盖密码存储、Session 管理、MFA 等核心主题
  2. OWASP Password Storage Cheat Sheet  —— 密码哈希算法选择和参数配置的权威指南,包含 Argon2id 和 bcrypt 推荐参数
  3. RFC 7519 - JSON Web Token (JWT)  —— JWT 标准规范,定义了 Token 结构、声明和验证规则
  4. RFC 6749 - OAuth 2.0 Authorization Framework  —— OAuth 2.0 核心规范,定义了授权码流程、Token 端点等
  5. RFC 7636 - PKCE (Proof Key for Code Exchange)  —— PKCE 扩展规范,防止授权码拦截攻击
  6. jose npm 包文档  —— 零依赖的 JWT/JWS/JWE/JWK 实现,支持 Node.js、Deno、Bun 和浏览器
  7. Clerk 文档 - Authentication  —— Clerk 认证服务的完整文档,包含 Next.js、React、Express 集成指南
  8. Auth0 开发者文档  —— Auth0 平台的开发者文档,涵盖 Universal Login、RBAC、MFA 配置
  9. CASL 授权库文档  —— TypeScript 同构授权库,支持 RBAC 和 ABAC 模式
  10. Argon2 Password Hashing - IETF  —— Argon2 算法的 IETF 标准文档(RFC 9106)

参考来源


📖 返回 总览与导航 | 上一节:28c-Spec-Driven业务逻辑 | 下一节:28e-微服务与Serverless

Last updated on