Node.js JWT 认证实战:Express + Passport 的登录鉴权与权限控制

详解 Node.js 后端 JWT 认证的完整实现:Token 签发与验证、Refresh Token 机制、Password 中间件、Role-Based 权限控制、Rate Limiting 防暴力破解。含 Express 完整代码。

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 Token15 分钟API 请求鉴权
Refresh Token7-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 CookieLocalStorage
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 的泄露等同于攻破整个认证体系。生产环境应遵循以下原则:

  1. 永不硬编码:使用环境变量或密钥管理服务(AWS Secrets Manager、Azure Key Vault、HashiCorp Vault)。
  2. 长度充足:HS256 的 Secret 至少 256 位(32 字节),建议使用 crypto.randomBytes(64).toString('hex') 生成。
  3. 定期轮换:RS256 场景下定期更新密钥对,旧公钥保留缓存期以兼容已签发 Token;HS256 场景下通过双写过渡期实现零停机轮换。
  4. 分离密钥: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 的稳定性。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. GraphQL BFF 与微前端:多前端团队的 Schema 分片与协作模式
  2. 前端 GraphQL 客户端集成:Apollo Client、Relay 与 urql 选型
  3. 前端性能调试实战:从首屏加载到运行时瓶颈