JWT (JSON Web Token)是现代 Web 应用的标准认证方案:无状态、自包含、可跨域。本文详解 Node.js 后端 JWT 认证的完整实现。
一、JWT 基础
JWT 由三部分组成:Header.Payload.Signature。每次客户端携带 Token 访问受保护资源时,服务端无需查询数据库即可完成身份校验,这是 JWT 无状态特性的核心优势。
Header: 算法类型(HS256/RS256)
Payload: 声明(用户ID、角色、过期时间)
Signature: 签名防篡改
Token 结构深度解析
Header 通常包含 alg(签名算法)和 typ(令牌类型)。Payload 中的标准声明包括 sub(主题/用户ID)、iat(签发时间)、exp(过期时间)和 iss(签发者)。开发者还可添加自定义声明如 role、permissions。Signature 的生成过程为:HMACSHA256(base64Url(Header) + "." + base64Url(Payload), Secret)。这个签名确保了 Header 和 Payload 在传输过程中未被篡改,因为任何修改都会导致签名验证失败。
HS256 vs RS256 选型对比
| 维度 | HS256(HMAC + SHA-256) | RS256(RSA + SHA-256) |
|---|---|---|
| 密钥类型 | 对称密钥(单个 Secret) | 非对称密钥对(私钥签名 / 公钥验证) |
| 安全性 | 密钥泄露则完全失效 | 私钥仅服务端持有,公钥可公开分发 |
| 性能 | 计算快,适合高并发 | 签名略慢,验证可通过公钥在边缘节点完成 |
| 适用场景 | 单体服务、微服务内部通信 | 分布式系统、第三方接入、单点登录(SSO) |
| 密钥轮换 | 需同步更新所有节点 | 只替换私钥,公钥分发即可 |
选型建议:单体 Express 应用使用 HS256 足够;若涉及多个服务或需要第三方验证 Token,优先选择 RS256。使用 RS256 时,私钥应存储在 KMS(Key Management Service)或 HashiCorp Vault 中,公钥可配置在 API Gateway 或 CDN 边缘节点,实现分布式的 Token 验证。
JWT 安全漏洞与防御
None 算法绕过:JWT Header 中 alg 字段可被攻击者篡改为 "none",某些早期库在解析时会跳过签名验证。防御方案是:在验证时显式指定 algorithms: ['HS256'],拒绝接受 none 算法。
jwt.verify(token, SECRET, { algorithms: ['HS256'] });
密钥混淆攻击(Key Confusion):当服务端同时支持 HS256 和 RS256 时,攻击者可能将使用公钥作为 HMAC Secret 签名的 Token 提交给服务端。服务端若未严格检查算法类型,就会用公钥验证 HMAC 签名,导致绕过。防御方法是:解析 Header 后严格校验 alg 字段,拒绝与配置不符的算法。
双 Token 策略
| Token | 有效期 | 用途 |
|---|---|---|
| Access Token | 15 分钟 | API 请求鉴权 |
| Refresh Token | 7-30 天 | 刷新 Access Token |
双 Token 策略的核心思想是缩短 Access Token 的生命周期以降低泄露风险,同时通过长期有效的 Refresh Token 减少用户频繁登录的挫败感。Refresh Token 的设计必须更加安全,通常应存储在 HttpOnly Cookie 中,并配合 Refresh Token Rotation 机制使用,详见第五节。
二、Express 实现
npm install jsonwebtoken bcryptjs express-rate-limit
npm install -D @types/jsonwebtoken @types/bcryptjs
bcrypt 配置详解
bcrypt.hash(password, saltRounds) 中的 saltRounds 决定了计算强度。每增加一轮,哈希时间约翻倍。推荐值为 12(单次哈希耗时约 250ms),在 Express 单线程模型中,耗时过长的哈希会阻塞事件循环,因此生产环境建议配合 bcrypt.hash 的 Promise 版本并将 CPU 密集型操作放入 Worker Threads 或使用集群模式。若使用 Argon2(更现代的密码哈希方案),可调整 memoryCost 和 timeCost 参数抵抗 GPU 暴力破解。
// auth.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
export function generateTokens(userId: string, role: string, jti?: string) {
const accessToken = jwt.sign({ sub: userId, role }, ACCESS_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ sub: userId, jti }, REFRESH_SECRET, { expiresIn: '7d' });
return { accessToken, refreshToken };
}
// 验证中间件
export function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const payload = jwt.verify(token, ACCESS_SECRET, { algorithms: ['HS256'] }) as { sub: string; role: string };
req.user = payload;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
完整 Express 路由实现
以下代码展示了注册、登录、Token 刷新和受保护资源的完整路由。注册时先检查邮箱唯一性,然后对密码进行哈希处理;登录时先校验邮箱是否存在,再使用 bcrypt.compare 验证密码;成功后签发双 Token 并返回给客户端。
import { Router } from 'express';
import { hashPassword, verifyPassword, generateTokens, authenticate } from './auth';
const router = Router();
// 注册
router.post('/register', async (req, res) => {
const { email, password, role = 'user' } = req.body;
const existing = await User.findOne({ email });
if (existing) return res.status(409).json({ error: 'Email already registered' });
const passwordHash = await hashPassword(password);
const user = await User.create({ email, passwordHash, role });
res.status(201).json({ id: user._id, email: user.email });
});
// 登录
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const valid = await verifyPassword(password, user.passwordHash);
if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
const { accessToken, refreshToken } = generateTokens(user._id.toString(), user.role);
res.json({ accessToken, refreshToken });
});
// 刷新 Token
router.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) return res.status(401).json({ error: 'Refresh token required' });
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET) as { sub: string };
const tokens = generateTokens(payload.sub, 'user');
res.json(tokens);
} catch {
res.status(403).json({ error: 'Invalid refresh token' });
}
});
// 受保护路由
router.get('/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
export default router;
Token 存储模式对比(Cookie vs LocalStorage)
| 维度 | HttpOnly Cookie | LocalStorage |
|---|---|---|
| XSS 防护 | 无法被 JavaScript 读取,天然免疫 XSS | 易受 XSS 攻击,脚本可直接读取 Token |
| CSRF 防护 | 需配合 CSRF Token 或 SameSite Cookie | 不受 CSRF 影响(手动 Header 发送) |
| 跨域支持 | SameSite=None; Secure 支持跨域 | 天然支持跨域读取 |
| SSR / 移动端 | Cookie 自动随请求携带 | 需手动写入请求 Header |
| 带宽消耗 | 每次请求自动携带 | 仅 API 请求携带 |
安全建议:Refresh Token 务必使用 HttpOnly + Secure + SameSite=Strict Cookie 存储;Access Token 可存入内存(Memory)并在页面刷新后通过 Refresh Token 重新获取。这种分层存储策略将最严重的 Token 泄露风险(长期有效的 Refresh Token)隔离在 Cookie 安全沙箱中,即使发生 XSS 攻击,攻击者也无法窃取 Refresh Token。
三、Rate Limiting 防暴力破解
暴力破解攻击通过自动化脚本尝试大量用户名和密码组合进行登录。Rate Limiting 是抵御此类攻击的第一道防线。
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5, // 最多 5 次尝试
message: { error: 'Too many login attempts' },
standardHeaders: true, // 返回 RateLimit-* 响应头
legacyHeaders: false, // 禁用 X-RateLimit-* 旧头
});
app.post('/login', loginLimiter, async (req, res) => {
// 登录逻辑
});
Redis 分布式限流
在集群部署或多实例场景下,内存限流器(如默认的 MemoryStore)无法共享计数状态,攻击者可轮询不同实例突破限制。引入 Redis 作为统一存储后端可解决此问题。
import Redis from 'ioredis';
import { RateLimiterRedis } from 'rate-limiter-flexible';
const redisClient = new Redis(process.env.REDIS_URL!);
const limiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail',
points: 5, // 允许 5 次
duration: 900, // 每 15 分钟
blockDuration: 3600, // 超限后封禁 1 小时
});
async function distributedLoginLimiter(req: Request, res: Response, next: NextFunction) {
const key = req.ip || req.body.email;
try {
await limiter.consume(key, 1);
next();
} catch {
res.status(429).json({ error: 'Too many requests, please try again later' });
}
}
rate-limiter-flexible 库支持滑动窗口计数,比固定窗口更精确;blockDuration 可在超限后增加冷却期,有效阻止持续性暴力破解。生产环境还应根据账号粒度(而非仅 IP 粒度)限制,因为攻击者可能使用大量代理 IP。
验证码 + 限流组合策略
仅靠限流无法完全阻止攻击者(攻击者可放慢速率绕过限制)。引入验证码(CAPTCHA)作为更高安全层级的校验手段,在检测到异常行为时触发。
// 策略:失败 3 次后要求验证码,失败 5 次后封禁 IP
async function adaptiveLoginProtection(req: Request, res: Response, next: NextFunction) {
const key = `fail:${req.body.email}`;
const fails = await redisClient.get(key);
const failCount = parseInt(fails || '0', 10);
if (failCount >= 3) {
const { captchaToken, captchaAnswer } = req.body;
const valid = await verifyCaptcha(captchaToken, captchaAnswer);
if (!valid) return res.status(400).json({ error: 'Invalid captcha' });
}
if (failCount >= 5) {
return res.status(429).json({ error: 'Account temporarily locked' });
}
next();
}
这种自适应策略兼顾了正常用户体验和系统安全:普通用户在短期内输错密码不超过三次时无需额外操作;当攻击者持续尝试时,系统逐步升级防御等级,先引入验证码增加攻击成本,最终封禁账号 login 尝试。验证码服务可使用 Google reCAPTCHA v3(无感知评分)或 hCaptcha(隐私友好)。
四、Role-Based 权限
基于角色的访问控制(RBAC)将权限授予角色而非直接授予用户,简化了大型系统的权限管理。用户与角色是多对多关系,角色与权限也是多对多关系,这种分层设计使得权限变更只需调整角色配置,无需逐个修改用户。
export function authorize(roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// 使用
app.delete('/users/:id', authenticate, authorize(['admin']), deleteUser);
基于 Resource-Based Access Control(RBAC + 资源权限)
简单的角色判断在资源归属场景下不足够。例如,普通用户应能修改自己的资料但不能修改他人的资料,管理员可以修改任何人的资料。此时需要引入资源级别的权限校验,通常表现为 “自己拥有的资源可操作,他人的资源禁止操作”。
export function authorizeResource(
allowedRoles: string[],
getResourceOwnerId?: (req: Request) => Promise<string | null>
) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as { sub: string; role: string };
if (!user) return res.status(401).json({ error: 'Unauthorized' });
// 管理员直接放行
if (allowedRoles.includes(user.role)) return next();
// 资源所有者校验
if (getResourceOwnerId) {
const ownerId = await getResourceOwnerId(req);
if (ownerId === user.sub) return next();
}
return res.status(403).json({ error: 'Forbidden' });
};
}
// 使用示例:用户只能更新自己的资料
app.put('/users/:id', authenticate, authorizeResource(['admin'], async (req) => {
const user = await User.findById(req.params.id);
return user ? user._id.toString() : null;
}), updateUser);
这种模式将 “角色权限” 和 “资源所有权” 两个维度解耦,实现了最小权限原则(Principle of Least Privilege)。在更复杂的系统中,可进一步引入 Casbin 或 Oso 策略引擎,使用 PBAC(Policy-Based Access Control)实现细粒度的动态权限控制。
权限中间件 Nest.js 适配版
Nest.js 流行的原因之一是其优雅的装饰器体系。以下展示如何将上述 RBAC + 资源权限逻辑封装为 Nest.js 守卫(Guard)和装饰器。
import { CanActivate, ExecutionContext, Injectable, SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
使用方法如下:
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('dashboard')
@Roles('admin', 'super_admin')
getDashboard() {
return { stats: '...' };
}
}
Nest.js 的 JwtAuthGuard 负责解析和验证 JWT,RolesGuard 负责角色校验,两者通过 ExecutionContext 共享请求上下文,职责清晰且高度可复用。配合 CASL(Nest.js 官方的授权库),还可以实现基于条件(Condition)的动态权限判断。
五、Refresh Token 轮换机制(Rotation)与 Token 黑名单
Refresh Token 轮换是一种安全机制:每次客户端使用 Refresh Token 获取新的 Access Token 时,服务端同时生成一组新的 Access Token 和 Refresh Token,旧的 Refresh Token 即刻失效。该机制能有效防止 Refresh Token 被盗后的长期滥用:即使攻击者窃取了 Refresh Token A,只要合法用户先于攻击者使用了 Token A,攻击者手中的 Token A 就会因轮换而失效。
// Refresh Token 存储模型
interface RefreshTokenRecord {
jti: string; // 唯一标识
userId: string;
tokenFamily: string; // 同一登录会话的 Token 家族
createdAt: Date;
revokedAt?: Date;
}
async function rotateRefreshToken(oldJti: string, userId: string) {
const oldRecord = await RefreshToken.findOne({ jti: oldJti });
if (!oldRecord || oldRecord.revokedAt) {
// 检测到重放攻击:旧 Token 已使用或已撤销
await revokeTokenFamily(oldRecord?.tokenFamily);
throw new Error('Token reuse detected');
}
// 标记旧 Token 已使用
oldRecord.revokedAt = new Date();
await oldRecord.save();
// 签发新 Token 对
const newTokens = generateTokens(userId, 'user');
await RefreshToken.create({
jti: extractJti(newTokens.refreshToken),
userId,
tokenFamily: oldRecord.tokenFamily,
});
return newTokens;
}
Token 家族(Token Family)机制追踪同一登录会话中的所有 Refresh Token。当检测到序列外使用(如已轮换过的旧 Token 被再次提交),系统立即撤销整个家族的所有 Token,强制用户重新登录。这种设计在检测到泄露时能提供即时响应。
Token 黑名单
Access Token 设计为无状态且短期有效(如 15 分钟),正常情况下无需撤销。但在用户登出、账号被封禁或检测到异常活动时,必须立即使 Token 失效。此时引入 Token 黑名单(Denylist),在 Redis 中存储已撤销的 Token jti 及失效时间。
async function revokeToken(jti: string, exp: number) {
const ttl = exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redisClient.setex(`token:blacklist:${jti}`, ttl, 'revoked');
}
}
async function isTokenRevoked(jti: string): Promise<boolean> {
const exists = await redisClient.get(`token:blacklist:${jti}`);
return exists !== null;
}
// 在 authenticate 中间件中校验
export async function authenticateWithDenylist(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const payload = jwt.decode(token) as { jti: string };
if (await isTokenRevoked(payload.jti)) {
return res.status(401).json({ error: 'Token revoked' });
}
jwt.verify(token, ACCESS_SECRET, { algorithms: ['HS256'] });
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
Redis 的 TTL 机制确保已过期的 Token 自动从黑名单中清除,避免了数据无限增长。对于高并发场景,可在 API Gateway 层进行黑名单预检,减轻应用服务器压力。
六、OAuth2 / OIDC 集成(Passport.js Google/GitHub 策略)
OAuth2 允许用户通过第三方身份提供商(IdP)登录,无需在应用内注册密码。OIDC(OpenID Connect)建立在 OAuth2 之上,标准化了身份信息的返回格式(ID Token)。Passport.js 是 Node.js 最常用的认证中间件框架,通过策略(Strategy)模式支持数百种身份提供商。
npm install passport passport-google-oauth20 passport-github2 express-session
Google OAuth2 策略配置
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: '/auth/google/callback',
}, async (accessToken, refreshToken, profile, done) => {
const email = profile.emails?.[0]?.value;
if (!email) return done(new Error('No email from Google'), false);
let user = await User.findOne({ email });
if (!user) {
user = await User.create({
email,
provider: 'google',
providerId: profile.id,
name: profile.displayName,
});
}
done(null, user);
}));
passport.serializeUser((user: any, done) => done(null, user._id));
passport.deserializeUser((id, done) => User.findById(id).then(user => done(null, user)));
GitHub OAuth2 策略配置
import { Strategy as GitHubStrategy } from 'passport-github2';
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: '/auth/github/callback',
}, async (accessToken, refreshToken, profile, done) => {
const email = profile.emails?.[0]?.value || `${profile.username}@users.noreply.github.com`;
let user = await User.findOne({ providerId: profile.id, provider: 'github' });
if (!user) {
user = await User.create({
email,
provider: 'github',
providerId: profile.id,
name: profile.displayName || profile.username,
});
}
done(null, user);
}));
Express 路由集成
import session from 'express-session';
app.use(session({ secret: process.env.SESSION_SECRET!, resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
const tokens = generateTokens(req.user!._id.toString(), req.user!.role);
res.redirect(`/auth/success?accessToken=${tokens.accessToken}`);
}
);
安全问题与建议:OAuth2 中的 state 参数用于防御 CSRF 攻击,Passport.js 默认自动处理;回调地址必须在 IdP 控制台中严格白名单化,防止开放重定向(Open Redirect)漏洞;从第三方获取的用户 email 不可信任为已验证,应要求用户首次登录时补全信息或验证邮箱。
七、JWT 安全最佳实践
短有效期与 Token 刷新
Access Token 的有效期应尽可能短(5-15 分钟),这是减少泄露后影响窗口的根本手段。短有效期并不意味着用户体验下降,因为 Refresh Token 机制允许在用户无感知的情况下静默刷新。对于高敏感操作(如转账、修改密码),可引入二次验证或一次性 Token(Step-Up Authentication)。
密钥管理
JWT Secret 的泄露等同于攻破整个认证体系。生产环境应遵循以下原则:
- 永不硬编码:使用环境变量或密钥管理服务(AWS Secrets Manager、Azure Key Vault、HashiCorp Vault)。
- 长度充足:HS256 的 Secret 至少 256 位(32 字节),建议使用
crypto.randomBytes(64).toString('hex')生成。 - 定期轮换:RS256 场景下定期更新密钥对,旧公钥保留缓存期以兼容已签发 Token;HS256 场景下通过双写过渡期实现零停机轮换。
- 分离密钥:Access Token 与 Refresh Token 使用不同的 Secret,避免一处泄露导致全盘失守。
HTTPS 传输
JWT 必须通过 HTTPS 传输。在 TLS 未启用的情况下,中间人攻击者可以轻易拦截并篡改 Token。使用 express-sslify 强制 HTTPS 重定向:
import enforce from 'express-sslify';
app.use(enforce.HTTPS({ trustProtoHeader: true }));
内容安全策略(CSP)
配合前端 CSP 头部,限制内联脚本执行,降低 XSS 风险:
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
next();
});
对于使用 LocalStorage 存储 Access Token 的方案,CSP 和严格的输入输出编码(使用 DOMPurify)是防御 XSS 窃取 Token 的关键防线。建议优先考虑将 Refresh Token 存入 HttpOnly Cookie,Access Token 短期存入内存的混合策略。
八、测试策略:Jest + Supertest 完整认证测试用例
认证系统的测试应覆盖正常流程(注册、登录、访问受保护资源)和异常流程(无效 Token、过期 Token、权限不足、暴力破解防护触发)。以下使用 Jest 和 Supertest 编写集成测试。
npm install --save-dev jest @types/jest supertest @types/supertest
import request from 'supertest';
import app from '../app';
import { User } from '../models/User';
import redisClient from '../redis';
beforeAll(async () => {
await User.deleteMany({});
await redisClient.flushdb();
});
afterAll(async () => {
await redisClient.quit();
});
describe('Authentication Flow', () => {
let accessToken: string;
let refreshToken: string;
it('should register a new user', async () => {
const res = await request(app)
.post('/register')
.send({ email: 'test@example.com', password: 'SecurePass123!' });
expect(res.status).toBe(201);
expect(res.body).toHaveProperty('id');
});
it('should reject duplicate registration', async () => {
const res = await request(app)
.post('/register')
.send({ email: 'test@example.com', password: 'SecurePass123!' });
expect(res.status).toBe(409);
});
it('should login and return tokens', async () => {
const res = await request(app)
.post('/login')
.send({ email: 'test@example.com', password: 'SecurePass123!' });
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('accessToken');
expect(res.body).toHaveProperty('refreshToken');
accessToken = res.body.accessToken;
refreshToken = res.body.refreshToken;
});
it('should reject invalid password', async () => {
const res = await request(app)
.post('/login')
.send({ email: 'test@example.com', password: 'WrongPass!' });
expect(res.status).toBe(401);
});
it('should access protected route with valid token', async () => {
const res = await request(app)
.get('/profile')
.set('Authorization', `Bearer ${accessToken}`);
expect(res.status).toBe(200);
expect(res.body.user).toHaveProperty('sub');
});
it('should reject access without token', async () => {
const res = await request(app).get('/profile');
expect(res.status).toBe(401);
});
it('should reject access with invalid token', async () => {
const res = await request(app)
.get('/profile')
.set('Authorization', 'Bearer invalid.token.here');
expect(res.status).toBe(401);
});
it('should refresh tokens with valid refresh token', async () => {
const res = await request(app)
.post('/refresh')
.send({ refreshToken });
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('accessToken');
});
it('should enforce rate limiting after multiple failures', async () => {
for (let i = 0; i < 5; i++) {
await request(app)
.post('/login')
.send({ email: 'test@example.com', password: 'WrongPass!' });
}
const res = await request(app)
.post('/login')
.send({ email: 'test@example.com', password: 'WrongPass!' });
expect(res.status).toBe(429);
});
it('should enforce admin authorization', async () => {
const res = await request(app)
.delete('/users/some-id')
.set('Authorization', `Bearer ${accessToken}`);
expect(res.status).toBe(403);
});
});
测试覆盖率建议
- 单元测试:覆盖
hashPassword、verifyPassword、generateTokens、authorize等纯函数。 - 集成测试:使用
supertest+ 内存数据库(mongodb-memory-server)测试完整请求生命周期。 - 安全测试:使用 OWASP ZAP 或 Burp Suite 扫描 JWT 相关的安全漏洞(如算法混淆、签名绕过)。
- 性能测试:使用 Artillery 或 k6 模拟高并发登录场景,验证限流器和 Redis 的稳定性。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。