本教程面向有一定Web开发基础、希望用Claude Code高效构建生产级认证授权系统的开发者。你将学到JWT双Token机制、OAuth2.0/PKCE流程、Refresh Token轮换与窃取检测、RBAC/ABAC权限模型设计,以及NextAuth.js/Clerk/Supabase Auth三大方案的选型策略。教程以一个完整的多租户SaaS认证系统为实战案例,贯穿所有核心知识点。
适用人群:中级前端/全栈开发者,已有Node.js或Next.js项目经验,希望系统掌握Claude Code在安全敏感场景下的开发方法。
| 要求 | 说明 |
|---|---|
| Claude Code | 已安装并完成基础配置(claude --version >= 1.0) |
| Node.js | v20+,推荐使用LTS版本 |
| 包管理器 | pnpm或npm |
| 基础知识 | HTTP协议、REST API、JavaScript/TypeScript |
| 数据库 | PostgreSQL或SQLite(开发环境) |
认证(Authentication)回答"你是谁",授权(Authorization)回答"你能做什么"。这是两个独立的关注点,但在实践中经常被混淆。Claude Code可以帮助你在项目初期就建立清晰的安全边界。
在CLAUDE.md中明确安全约束是第一步。创建项目根目录的.claude/CLAUDE.md或项目级CLAUDE.md:
# 项目安全约束
## 认证规则
- JWT必须使用RS256或ES256非对称算法,禁止使用HS256
- Access Token有效期不超过15分钟
- Refresh Token必须使用HttpOnly + Secure + SameSite=Strict的Cookie
- 禁止在localStorage中存储任何Token
- 所有密码必须使用bcrypt或argon2哈希,cost factor >= 12
## 授权规则
- 采用RBAC模型,角色定义在数据库而非代码中
- 中间件必须验证每个受保护路由的权限
- API返回403而非404,当资源存在但无权访问时
## 禁止事项
- 不要在任何响应中返回密码哈希
- 不要使用console.log输出Token
- 不要硬编码密钥或密钥
这些约束会持续指导Claude Code在后续开发中做出安全选择。
在开始编码前,用Claude Code帮你评估适合项目的认证架构。以下是选型决策流程:
flowchart TD
A[项目认证需求分析] --> B{用户规模?}
B -->|< 10K用户| C{需要社交登录?}
B -->|> 10K用户| D{企业级需求?}
C -->|是| E[OAuth2.0 + PKCE]
C -->|否| F[Session-based Auth]
D -->|是| G[SSO/SAML + RBAC]
D -->|否| H{实时性要求?}
H -->|高| I[JWT + Refresh Token]
H -->|低| J[Session + Redis Store]
E --> K{自建 vs 托管?}
F --> K
G --> L[Clerk/Auth0]
I --> K
J --> K
K -->|自建| M[NextAuth.js / 自定义]
K -->|托管| N[Supabase Auth / Clerk]
向Claude Code提供以下Prompt进行选型分析:
我正在构建一个[描述你的项目]。项目技术栈是[Next.js/Express/etc],
预计用户量[规模],需要[列出:社交登录/企业SSO/多租户/API认证]。
请帮我分析最适合的认证架构方案,给出推荐和理由。
mkdir auth-demo && cd auth-demo
pnpm init
pnpm add express jsonwebtoken bcryptjs cookie-parser helmet cors
pnpm add -D typescript @types/express @types/jsonwebtoken @types/bcryptjs @types/cookie-parser tsx
然后用Claude Code生成项目骨架:
请帮我搭建一个Express + TypeScript的认证服务项目骨架。要求:
1. src/目录结构:config/、middleware/、routes/、services/、types/
2. 环境变量配置使用dotenv
3. 全局错误处理中间件
4. Helmet安全头配置
5. CORS白名单配置
这是认证系统的核心。Claude Code在无约束条件下常见的错误包括:使用HS256对称算法、Token永不过期、密钥硬编码等。通过CLAUDE.md约束和精确的Prompt可以避免这些问题。
向Claude Code发出以下指令:
请实现 src/services/token.service.ts,要求:
1. generateTokenPair(userId: string, role: string) 返回 { accessToken, refreshToken }
2. Access Token: RS256算法,有效期15分钟,payload包含userId、role、iat、exp
3. Refresh Token: 随机生成加密字符串(用crypto.randomBytes),存入数据库
4. verifyAccessToken(token: string) 验证并返回解码payload
5. rotateRefreshToken(oldToken: string) 实现 Refresh Token轮换:
- 验证旧Token有效性
- 废弃旧Token
- 生成新Token对
- 如果旧Token已被使用过(重放攻击),废弃该用户所有Refresh Token
生成的核心逻辑应包含Refresh Token窃取检测机制:
// src/services/token.service.ts 核心片段
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { db } from '../config/database';
interface TokenPair {
accessToken: string;
refreshToken: string;
}
interface TokenPayload {
userId: string;
role: string;
iat: number;
exp: number;
}
export class TokenService {
// 从环境变量加载RSA密钥对
private static get privateKey() {
return Buffer.from(process.env.JWT_PRIVATE_KEY_B64!, 'base64');
}
private static get publicKey() {
return Buffer.from(process.env.JWT_PUBLIC_KEY_B64!, 'base64');
}
static async generateTokenPair(
userId: string,
role: string
): Promise<TokenPair> {
// Access Token - 短期
const accessToken = jwt.sign(
{ userId, role },
this.privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'auth-demo',
audience: 'auth-demo-api',
jwtid: crypto.randomUUID(),
}
);
// Refresh Token - 长期但一次性
const refreshToken = crypto.randomBytes(64).toString('hex');
const hashedToken = await hashToken(refreshToken);
await db.query(
`INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '7 days')`,
[userId, hashedToken]
);
return { accessToken, refreshToken };
}
static async rotateRefreshToken(
oldRefreshToken: string
): Promise<TokenPair | null> {
// 查找Token记录
const result = await db.query(
`SELECT id, user_id, token_hash, is_revoked, expires_at
FROM refresh_tokens
WHERE expires_at > NOW()
ORDER BY created_at DESC
LIMIT 10`,
[] // 先获取所有活跃Token进行匹配
);
let matchedRow = null;
for (const row of result.rows) {
const isValid = await verifyTokenHash(oldRefreshToken, row.token_hash);
if (isValid) {
matchedRow = row;
break;
}
}
if (!matchedRow) return null;
// 窃取检测:如果Token已被使用,说明遭到重放攻击
if (matchedRow.is_revoked) {
// 废弃该用户的所有Refresh Token - 安全优先
await db.query(
`UPDATE refresh_tokens SET is_revoked = true
WHERE user_id = $1`,
[matchedRow.user_id]
);
console.error(
`SECURITY: Refresh token reuse detected for user ${matchedRow.user_id}. All sessions revoked.`
);
return null;
}
// 正常轮换:废弃旧Token
await db.query(
`UPDATE refresh_tokens SET is_revoked = true WHERE id = $1`,
[matchedRow.id]
);
// 获取用户角色用于生成新Access Token
const user = await db.query(
`SELECT role FROM users WHERE id = $1`,
[matchedRow.user_id]
);
// 生成新Token对
return this.generateTokenPair(matchedRow.user_id, user.rows[0].role);
}
}
让Claude Code生成认证和授权中间件:
请实现两个中间件:
1. src/middleware/auth.middleware.ts
- authenticate: 从Cookie中提取Access Token(而非Authorization头),
验证签名和过期时间,将解码后的userId和role附加到req对象
- 如果Token过期,返回特定错误码 TOKEN_EXPIRED(区别于无效Token)
2. src/middleware/authorize.middleware.ts
- requireRole(...roles: string[]): 返回中间件,检查req.user.role是否在允许列表中
- requirePermission(permission: string): 基于RBAC检查用户是否有特定权限
关键实现要点——Claude Code需要明确的Token提取策略约束:
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export interface AuthRequest extends Request {
user?: {
userId: string;
role: string;
};
}
export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
// 优先从Cookie获取,而非Authorization头
const token = req.cookies?.accessToken;
if (!token) {
return res.status(401).json({
error: 'UNAUTHORIZED',
message: 'No authentication token provided',
});
}
try {
const publicKey = Buffer.from(process.env.JWT_PUBLIC_KEY_B64!, 'base64');
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'auth-demo',
audience: 'auth-demo-api',
}) as { userId: string; role: string; exp: number };
req.user = { userId: decoded.userId, role: decoded.role };
next();
} catch (err: any) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'TOKEN_EXPIRED',
message: 'Access token has expired',
});
}
return res.status(401).json({
error: 'INVALID_TOKEN',
message: 'Invalid authentication token',
});
}
}
Claude Code在认证开发中最容易犯的五个错误及修复方案:
| 陷阱 | Claude Code的默认倾向 | 正确做法 |
|---|---|---|
| Token存储位置 | 存入localStorage(方便前端读取) | HttpOnly + Secure Cookie |
| 密码处理 | 明文比对或使用MD5 | bcrypt/argon2,cost >= 12 |
| CORS配置 | Access-Control-Allow-Origin: * |
白名单机制,限制凭据传递 |
| 错误信息 | "User not found" vs "Wrong password" | 统一返回"Invalid credentials",防止用户名枚举 |
| Token算法 | 默认HS256(对称加密) | RS256/ES256(非对称加密) |
在CLAUDE.md中添加以下约束以避免这些问题:
## 安全审计清单
每次修改认证相关代码后,必须自检:
- [ ] 没有Token出现在浏览器localStorage
- [ ] 没有明文密码出现在日志或响应中
- [ ] CORS配置不包含通配符
- [ ] 登录失败响应不区分"用户不存在"和"密码错误"
- [ ] JWT使用非对称算法
当需要集成Google、GitHub等第三方登录时,OAuth2.0是标准方案。对于没有后端的SPA或移动应用,PKCE(Proof Key for Code Exchange)扩展是必须的。
sequenceDiagram
participant U as 用户浏览器
participant FE as 前端应用
participant AS as 认证服务器
participant RS as 资源服务器
U->>FE: 点击"Google登录"
FE->>FE: 生成code_verifier + code_challenge
FE->>AS: 重定向到授权端点<br/>+ code_challenge + redirect_uri
AS->>U: 显示Google登录页面
U->>AS: 输入凭据并授权
AS->>FE: 回调redirect_uri<br/>+ authorization_code
FE->>AS: 用code + code_verifier换取Token
AS->>AS: 验证code_challenge = hash(code_verifier)
AS->>FE: 返回access_token + refresh_token
FE->>RS: 携带access_token请求资源
RS->>FE: 返回受保护资源
向Claude Code提供精确的实现指令:
请实现 src/routes/oauth.routes.ts 和 src/services/oauth.service.ts,
支持Google OAuth2.0登录:
要求:
1. GET /auth/google - 生成授权URL,使用PKCE(code_verifier/challenge),
将code_verifier暂存到加密的Cookie中
2. GET /auth/google/callback - 处理回调:
- 验证state参数防CSRF
- 用code + code_verifier换取Google Token
- 获取Google用户信息
- 如果用户已存在,创建新Session
- 如果用户不存在,创建新用户(从Google信息填充),然后创建Session
3. POST /auth/logout - 清除Cookie,废弃Refresh Token
4. 所有Token通过HttpOnly Secure Cookie传递
PKCE核心实现:
// src/services/oauth.service.ts
import crypto from 'crypto';
export class OAuthService {
// PKCE: 生成code_verifier和code_challenge
static generatePKCE() {
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return { codeVerifier, codeChallenge };
}
// 生成防CSRF的state参数
static generateState() {
return crypto.randomBytes(32).toString('hex');
}
// 构建Google授权URL
static getGoogleAuthUrl(
codeChallenge: string,
state: string,
redirectUri: string
): string {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid email profile',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
access_type: 'offline',
prompt: 'consent',
});
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
}
// 用授权码换取Token
static async exchangeCodeForTokens(
code: string,
codeVerifier: string,
redirectUri: string
) {
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: redirectUri,
grant_type: 'authorization_code',
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
throw new Error('Failed to exchange code for tokens');
}
return response.json();
}
}
Q: Claude Code生成的OAuth回调为什么缺少state验证?
这是最常见的遗漏。Claude Code默认倾向于生成"功能可用"的代码,但state参数的CSRF防护经常被忽略。解决方案是在CLAUDE.md中明确要求:
## OAuth安全要求
- 所有OAuth流程必须使用state参数防CSRF
- state必须存储在服务端Session或加密Cookie中
- 回调时必须验证state完全匹配
- PKCE的code_verifier不得出现在URL参数中
Q: Token刷新时出现401循环怎么处理?
这通常是因为前端并发请求都触发了Token刷新。Claude Code生成的代码需要包含请求队列机制:
// 前端Token刷新防竞态
let isRefreshing = false;
let refreshSubscribers: Array<(token: string) => void> = [];
function onTokenRefreshed(newToken: string) {
refreshSubscribers.forEach((cb) => cb(newToken));
refreshSubscribers = [];
}
async function handleTokenRefresh(failedRequest: any) {
if (isRefreshing) {
return new Promise((resolve) => {
refreshSubscribers.push((token: string) => {
failedRequest.headers.set('Cookie', `accessToken=${token}`);
resolve(axios(failedRequest));
});
});
}
isRefreshing = true;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include',
});
const newToken = response.headers.get('Set-Cookie');
onTokenRefreshed(newToken!);
return axios(failedRequest);
} finally {
isRefreshing = false;
}
}
RBAC(Role-Based Access Control)是生产级应用最常用的授权模型。让Claude Code帮你设计数据库Schema:
请设计一个支持RBAC的数据库Schema,要求:
1. users表:id, email, password_hash, status, created_at
2. roles表:id, name, description
3. permissions表:id, resource, action, description
4. role_permissions表:多对多关联
5. user_roles表:多对多关联
6. 支持未来扩展到ABAC(基于属性的访问控制)
请生成完整的PostgreSQL DDL,包含索引和约束。
生成的Schema:
-- 用户表
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255),
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'deleted')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 角色表
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 权限表
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource VARCHAR(100) NOT NULL, -- 如 'posts', 'users', 'settings'
action VARCHAR(50) NOT NULL, -- 如 'read', 'write', 'delete', 'admin'
description TEXT,
UNIQUE(resource, action)
);
-- 角色-权限关联
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- 用户-角色关联
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ DEFAULT NOW(),
assigned_by UUID REFERENCES users(id),
PRIMARY KEY (user_id, role_id)
);
-- 性能索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_user_roles_user ON user_roles(user_id);
CREATE INDEX idx_role_permissions_role ON role_permissions(role_id);
CREATE INDEX idx_permissions_resource_action ON permissions(resource, action);
// src/middleware/authorize.middleware.ts
import { AuthRequest } from './auth.middleware';
import { Response, NextFunction } from 'express';
import { db } from '../config/database';
// 基于角色的检查
export function requireRole(...allowedRoles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'UNAUTHORIZED' });
}
// 从数据库或缓存获取用户角色
const hasRole = allowedRoles.includes(req.user.role);
if (!hasRole) {
return res.status(403).json({ error: 'FORBIDDEN', message: 'Insufficient role' });
}
next();
};
}
// 基于权限的检查(更细粒度)
export function requirePermission(resource: string, action: string) {
return async (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'UNAUTHORIZED' });
}
const result = await db.query(
`SELECT 1 FROM user_roles ur
JOIN role_permissions rp ON ur.role_id = rp.role_id
JOIN permissions p ON rp.permission_id = p.id
WHERE ur.user_id = $1
AND p.resource = $2
AND p.action = $3`,
[req.user.userId, resource, action]
);
if (result.rows.length === 0) {
return res.status(403).json({ error: 'FORBIDDEN', message: 'Insufficient permission' });
}
next();
};
}
flowchart LR
subgraph RBAC
R1[用户] -->|拥有角色| R2[管理员]
R2 -->|角色拥有| R3[权限: users:delete]
end
subgraph ABAC
A1[用户] -->|属性| A2[部门=工程<br/>级别=L5]
A2 -->|策略| A3[资源.所有者<br/>IN 用户.下属列表]
A3 -->|环境| A4[时间=工作日<br/>IP=内网]
end
RBAC -->|简单场景| S1[用户管理<br/>内容审核]
ABAC -->|复杂场景| S2[多租户隔离<br/>动态权限<br/>合规审计]
向Claude Code的选型Prompt:
请根据以下需求帮我决定使用RBAC还是ABAC:
- 1000+用户
- 3个业务部门
- 需要数据行级权限(用户只能看到自己部门的数据)
- 需要临时权限提升(审批流程)
推荐方案,并说明理由。如果推荐ABAC,给出从RBAC演进的路径。
| 维度 | NextAuth.js | Clerk | Supabase Auth |
|---|---|---|---|
| 部署模式 | 自托管 | SaaS | SaaS + 自托管 |
| 定价 | 免费开源 | 免费层10K MAU,之后$25/月 | 免费层50K MAU |
| 数据库要求 | 自备 | 内置 | Supabase生态 |
| 社交登录 | 50+ Provider | 20+ Provider | 15+ Provider |
| 自定义程度 | 完全 | 中等 | 高 |
| 企业SSO | 需Enterprise插件 | 内置SAML/OIDC | 内置SAML |
| 学习曲线 | 中等 | 低 | 中等 |
| Claude Code适配 | 高(代码级控制) | 中(配置为主) | 高(SQL级控制) |
flowchart TD
A[选择认证方案] --> B{项目类型?}
B -->|SaaS产品| C{需要企业客户?}
B -->|内部工具| D{技术栈?}
B -->|开源项目| E[NextAuth.js]
C -->|是| F[Clerk - 内置企业SSO]
C -->|否| G{预算?}
D -->|Next.js| E
D -->|其他| H[Supabase Auth]
G -->|有限| I[Supabase Auth - 免费层慷慨]
G -->|充足| F
在Next.js项目中,向Claude Code发出以下指令:
请为我的Next.js App Router项目集成NextAuth.js v5(Auth.js),要求:
1. 使用Auth.js v5的authorize模式
2. Credentials Provider:邮箱+密码登录
3. Google Provider:OAuth登录
4. JWT Session策略(不使用数据库Session)
5. 回调中实现:jwt回调中注入role信息,session回调中暴露给客户端
6. 受保护路由通过middleware.ts实现
7. 登录/注册页面使用Server Actions
项目结构:
- app/auth/login/page.tsx - 登录页
- app/auth/register/page.tsx - 注册页
- app/api/auth/[...nextauth]/route.ts - Auth API
- auth.ts - Auth配置
- middleware.ts - 路由保护
Auth.js v5核心配置:
// auth.ts
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { db } from '@/lib/db';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await db.user.findUnique({
where: { email: credentials.email as string },
include: { roles: { include: { role: true } } },
});
if (!user || !user.passwordHash) return null;
const isValid = await bcrypt.compare(
credentials.password as string,
user.passwordHash
);
if (!isValid) return null;
return {
id: user.id,
email: user.email,
role: user.roles[0]?.role.name || 'user',
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
// 首次登录时注入role
if (user) {
token.role = (user as any).role;
token.userId = user.id;
}
return token;
},
async session({ session, token }) {
// 将JWT中的role暴露给客户端
if (session.user) {
(session.user as any).role = token.role;
(session.user as any).id = token.userId;
}
return session;
},
},
pages: {
signIn: '/auth/login',
error: '/auth/error',
},
session: {
strategy: 'jwt',
maxAge: 24 * 60 * 60, // 24小时
},
});
构建一个支持多租户的项目管理SaaS认证系统,包含: - 邮箱密码注册/登录 - Google OAuth - 租户隔离 - RBAC权限(Owner/Admin/Member/Viewer) - API Key认证(供第三方调用) - 审计日志
flowchart TB
subgraph 客户端
UI[Web前端]
API_CLIENT[API客户端]
end
subgraph 认证层
AUTH[Auth Router<br/>/api/auth/*]
MW[Auth Middleware]
PERM[Permission Guard]
end
subgraph 业务层
TENANT[Tenant Service]
PROJECT[Project Service]
AUDIT[Audit Service]
end
subgraph 数据层
DB[(PostgreSQL)]
REDIS[(Redis<br/>Token黑名单)]
end
UI -->|Cookie/Token| AUTH
API_CLIENT -->|API Key| AUTH
AUTH --> MW
MW --> PERM
PERM --> TENANT
PERM --> PROJECT
TENANT --> DB
PROJECT --> DB
MW --> REDIS
AUDIT --> DB
// src/middleware/tenant.middleware.ts
import { AuthRequest } from './auth.middleware';
import { Response, NextFunction } from 'express';
import { db } from '../config/database';
export async function resolveTenant(
req: AuthRequest,
res: Response,
next: NextFunction
) {
const tenantSlug = req.params.tenantSlug || req.headers['x-tenant'];
if (!tenantSlug) {
return res.status(400).json({ error: 'TENANT_REQUIRED' });
}
// 查询租户并验证用户属于该租户
const result = await db.query(
`SELECT t.id, t.name, ut.role
FROM tenants t
JOIN user_tenants ut ON t.id = ut.tenant_id
WHERE t.slug = $1 AND ut.user_id = $2 AND t.status = 'active'`,
[tenantSlug, req.user!.userId]
);
if (result.rows.length === 0) {
return res.status(403).json({ error: 'TENANT_ACCESS_DENIED' });
}
// 注入租户上下文
req.tenant = {
id: result.rows[0].id,
name: result.rows[0].name,
userRole: result.rows[0].role,
};
next();
}
将以下Prompt发送给Claude Code,它会按步骤生成完整的认证系统:
请帮我构建一个完整的多租户认证系统。我已有以下文件:
- src/services/token.service.ts (Token生成/验证)
- src/middleware/auth.middleware.ts (认证中间件)
- src/middleware/authorize.middleware.ts (授权中间件)
- src/middleware/tenant.middleware.ts (租户隔离)
现在需要你实现:
1. src/routes/auth.routes.ts
- POST /register - 注册(邮箱验证)
- POST /login - 登录(限流:5次/分钟/IP)
- POST /refresh - Token刷新
- POST /logout - 登出
- GET /me - 获取当前用户信息
2. src/routes/oauth.routes.ts
- GET /auth/google - Google OAuth发起
- GET /auth/google/callback - Google OAuth回调
3. src/routes/tenant.routes.ts
- POST /tenants - 创建租户
- GET /tenants/:slug/members - 获取成员列表
- POST /tenants/:slug/invite - 邀请成员
- PUT /tenants/:slug/members/:userId/role - 修改成员角色
4. src/services/audit.service.ts
- 记录所有认证和权限变更操作
- 支持按用户、租户、操作类型查询
请确保:
- 所有路由都有适当的认证和授权中间件
- 密码使用argon2哈希
- 登录限流使用Redis计数器
- 所有Token通过HttpOnly Secure Cookie传递
让Claude Code生成认证测试:
请为认证系统生成集成测试,覆盖以下场景:
1. 注册成功 → 登录成功 → 获取用户信息 → 登出
2. 注册成功 → 登录 → Token过期 → 刷新Token → 继续
3. 同一Refresh Token被使用两次 → 所有Session被废弃
4. 无效Token访问受保护路由 → 401
5. 普通用户访问管理员路由 → 403
6. 用户访问非所属租户 → 403
7. Google OAuth回调处理成功
使用vitest + supertest。
测试核心用例示例:
// tests/auth.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { app } from '../src/app';
describe('认证流程集成测试', () => {
describe('注册 → 登录 → 访问 → 登出', () => {
it('完整正向流程', async () => {
// 注册
const registerRes = await request(app)
.post('/api/auth/register')
.send({
email: '[email protected]',
password: 'SecureP@ssw0rd!',
name: 'Test User',
});
expect(registerRes.status).toBe(201);
// 登录
const loginRes = await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'SecureP@ssw0rd!',
});
expect(loginRes.status).toBe(200);
expect(loginRes.headers['set-cookie']).toBeDefined();
// 提取Cookie
const cookies = loginRes.headers['set-cookie'];
const accessTokenCookie = cookies.find((c: string) =>
c.startsWith('accessToken=')
);
// 获取用户信息
const meRes = await request(app)
.get('/api/auth/me')
.set('Cookie', accessTokenCookie);
expect(meRes.status).toBe(200);
expect(meRes.body.email).toBe('[email protected]');
});
it('Refresh Token重放检测', async () => {
// 登录获取Token
const loginRes = await request(app)
.post('/api/auth/login')
.send({ email: '[email protected]', password: 'SecureP@ssw0rd!' });
const cookies = loginRes.headers['set-cookie'];
const refreshTokenCookie = cookies.find((c: string) =>
c.startsWith('refreshToken=')
);
// 第一次刷新 - 应该成功
const refresh1 = await request(app)
.post('/api/auth/refresh')
.set('Cookie', refreshTokenCookie);
expect(refresh1.status).toBe(200);
// 用旧Token再次刷新 - 应该触发窃取检测
const refresh2 = await request(app)
.post('/api/auth/refresh')
.set('Cookie', refreshTokenCookie);
expect(refresh2.status).toBe(401);
// 新Token也应该失效
const newCookies = refresh1.headers['set-cookie'];
const newAccessToken = newCookies.find((c: string) =>
c.startsWith('accessToken=')
);
const meRes = await request(app)
.get('/api/auth/me')
.set('Cookie', newAccessToken);
expect(meRes.status).toBe(401);
});
});
});
# 安装依赖
pnpm install
# 配置环境变量
cp .env.example .env
# 编辑.env,填入:
# JWT_PRIVATE_KEY_B64=<base64编码的RSA私钥>
# JWT_PUBLIC_KEY_B64=<base64编码的RSA公钥>
# GOOGLE_CLIENT_ID=<Google OAuth Client ID>
# GOOGLE_CLIENT_SECRET=<Google OAuth Client Secret>
# DATABASE_URL=postgresql://...
# 生成RSA密钥对
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
base64 -w0 private.pem > private.b64
base64 -w0 public.pem > public.b64
# 初始化数据库
pnpm db:migrate
pnpm db:seed
# 启动服务
pnpm dev
# 运行测试
pnpm test
在部署认证系统前,让Claude Code帮你做最终安全审查:
请对 src/ 目录下所有认证相关代码进行安全审计,检查以下项目:
1. Token存储:确认所有Token通过HttpOnly Secure Cookie传递
2. 密码处理:确认使用argon2/bcrypt,无明文日志
3. CORS:确认无通配符,白名单正确
4. 限流:确认登录、注册、Token刷新有速率限制
5. 输入验证:确认所有用户输入经过验证
6. SQL注入:确认使用参数化查询
7. 密钥管理:确认无硬编码密钥,所有密钥从环境变量读取
8. 审计日志:确认敏感操作有日志记录
输出格式:每项PASS或FAIL + 具体文件和行号。
flowchart LR
A[代码完成] --> B[安全审计]
B --> C{PASS?}
C -->|全部通过| D[部署到Staging]
C -->|存在FAIL| E[修复问题]
E --> B
D --> F[渗透测试]
F --> G{通过?}
G -->|是| H[部署到Production]
G -->|否| E
H --> I[监控告警]
I --> J[定期Token轮换<br/>密钥轮换]
| 训练营类型 | 适用性 | 预计时长 | 说明 |
|---|---|---|---|
| 全栈开发训练营 | ★★★★★ | 4-6小时 | 认证是全栈必学内容,可拆为2-3个Session |
| 安全工程师训练营 | ★★★★☆ | 3-4小时 | 侧重安全审计和攻击防护部分 |
| 企业级应用训练营 | ★★★★★ | 6-8小时 | 多租户+RBAC是企业级核心需求 |
| 前端开发训练营 | ★★★☆☆ | 2-3小时 | 侧重OAuth集成和Token管理,跳过后端实现 |
| 运维/DevOps训练营 | ★★☆☆☆ | 1-2小时 | 侧重部署检查清单和密钥管理 |
推荐教学路径:先完成Part 1-3的JWT基础(2小时),然后根据学员方向选择OAuth集成(前端方向)或RBAC设计(后端方向),最后用Part 6的完整案例做综合练习。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
