NestJS Passport策略中如何将新生成JWT设置到响应头
NestJS JWT令牌刷新逻辑实现方案
问题场景
- 基于NestJS + Passport实现身份认证
- JWT规则配置:总有效期14天,payload携带
refreshTime字段,阈值设为15分钟:用户15分钟内发起请求则正常通过认证,超过15分钟触发令牌刷新;JWT签名单独存储在独立数据表,校验时对应签名存在才签发新令牌,不存在直接抛出未认证异常 - 现有实现将刷新逻辑写在
JwtStrategy的validate方法中,无法直接将新令牌写入响应头,不确定该逻辑应该放在Strategy、拦截器还是AuthGuard中
现有实现代码
// jwt.strategy.ts import { ConfigService } from '@nestjs/config'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { AuthService } from '../auth.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private configService: ConfigService, private authService: AuthService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET'), passReqToCallback: true, }); } async validate(req: any, payload: any) { const { refreshTime, sub, phoneNumber } = payload; const tokenSignature = req.get('authorization').split('.').reverse()[0]; if (refreshTime < Date.now()) { console.log('REFRESH'); this.authService.refreshToken(tokenSignature); // **Set new token to response header** } return { userId: sub, phoneNumber, tokenSignature, }; } }
实现结论
不要在JwtStrategy的validate方法中处理令牌刷新和响应头写入。
Passport Strategy的validate方法职责单一,仅负责校验令牌合法性、返回需要挂载到req.user的用户信息,该生命周期阶段无法稳定操作响应对象,硬编码写入会出现响应已发送、生命周期不匹配的问题。
最优实现位置是自定义AuthGuard,不推荐用拦截器,原因如下:
- AuthGuard属于NestJS认证流程的原生扩展点,执行时机早于拦截器,在路由逻辑运行前完成认证处理,符合安全逻辑的执行优先级
- 可以直接拿到完整的请求、响应对象引用,操作响应头没有阻碍
- 拦截器执行时机在Guard之后,如果令牌校验失败直接抛出401异常,拦截器根本不会触发,不适合承载认证核心逻辑
具体实现代码
第一步:改造JwtStrategy,仅保留校验逻辑
把刷新判断的标记通过返回值传给Guard,不在Strategy里做刷新操作:
// jwt.strategy.ts import { ConfigService } from '@nestjs/config'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from '../auth.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private configService: ConfigService, private authService: AuthService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET'), passReqToCallback: true, }); } async validate(req: any, payload: any) { const { refreshTime, sub, phoneNumber } = payload; // 注意要先去掉Bearer 前缀再拆分签名,原实现这里有bug const rawToken = req.get('authorization').replace('Bearer ', ''); const tokenSignature = rawToken.split('.').reverse()[0]; // 校验签名是否存在于数据表,不存在直接抛401 const isSignatureValid = await this.authService.checkTokenExists(tokenSignature); if (!isSignatureValid) { throw new UnauthorizedException('登录状态已失效'); } // 返回用户信息+是否需要刷新的标记,后续由Guard处理 return { userId: sub, phoneNumber, tokenSignature, needRefresh: refreshTime < Date.now() }; } }
第二步:自定义JwtAuthGuard,处理令牌刷新和响应头写入
// jwt-auth.guard.ts import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from '../auth.service'; import { Response } from 'express'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor(private authService: AuthService) { super(); } async canActivate(context: ExecutionContext): Promise<boolean> { // 先执行原生Passport校验逻辑,校验失败直接返回401 const passAuth = await super.canActivate(context); if (!passAuth) return false; const req = context.switchToHttp().getRequest(); const res: Response = context.switchToHttp().getResponse(); const userInfo = req.user; if (userInfo.needRefresh) { // 调用刷新方法生成新令牌,注意刷新后要作废旧签名 const newToken = await this.authService.refreshToken(userInfo.tokenSignature, { sub: userInfo.userId, phoneNumber: userInfo.phoneNumber }); // 将新令牌写入响应头 res.setHeader('Authorization', `Bearer ${newToken}`); // 跨域场景下需要暴露该响应头,否则前端无法读取 // res.setHeader('Access-Control-Expose-Headers', 'Authorization'); } return true; } }
注意事项
- 刷新令牌时必须将旧令牌对应的签名从数据表中删除或标记为已使用,避免旧令牌被重复用于刷新,引发安全问题
- 原代码中拆分token签名的逻辑没有去掉
Bearer前缀,会导致拿到的签名值错误,校验、作废旧token时会出现匹配失败的问题 - 如果项目使用GraphQL,只需要修改Guard中从上下文获取req、res的方式,核心刷新逻辑不需要调整
内容的提问来源于stack exchange,提问作者soroush madani
相关产品推荐
相关产品推荐

