You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJS混合项目中Socket连接不中断的JWT刷新最佳实践

NestJS混合项目(REST+Socket)无断开刷新JWT令牌最佳实践

针对你遇到的问题,结合NestJS和Socket.io的特性,以下是落地性强的最佳实践方案:

一、刷新令牌的方案选择与分析

你提到的两种方案各有优劣,这里给出明确的推荐和细节:

方案1:通过Socket事件触发刷新

客户端在收到JWT过期错误时,直接通过Socket发送refresh-jwt-token事件并携带refresh_token。这种方案的好处是不用切换通信协议,全程保持Socket连接上下文,但缺点是需要在Socket层额外维护一套刷新逻辑,且refresh_token作为事件参数传输时,必须确保用WSS加密,避免明文泄露。

方案2:通过REST接口刷新(优先推荐)

客户端调用已有的REST刷新接口,将refresh_token放在Authorization头(比如Bearer {refresh_token})获取新的access_token。这种方案更符合JWT刷新的常规设计,能复用现有REST鉴权逻辑,不需要在Socket层重复造轮子,而且HTTP头传输token的方式更规范,便于做日志、拦截等统一处理。

二、新Access Token的正确使用方式

你之前想每次emit事件都带token的做法确实不优雅,这里有两种更合理的实现:

1. 更新Socket连接的上下文元数据

客户端拿到新access_token后,发送一个专门的Socket事件告知服务器更新连接的鉴权信息:

// 客户端代码
socket.emit('update-access-token', newAccessToken);

服务器端监听该事件,验证新token合法后,将其存储到Socket实例的data属性中(NestJS的Socket对象自带data字段,可自定义存储任意数据):

// 服务器端事件处理器
@SubscribeMessage('update-access-token')
async updateAccessToken(
  @MessageBody() newToken: string,
  @ConnectedSocket() socket: Socket
) {
  try {
    // 验证新token的合法性
    const payload = await this.jwtService.verifyAsync(newToken);
    // 更新Socket上下文,后续事件直接从这里取鉴权信息
    socket.data.user = payload.user;
    socket.data.accessToken = newToken;
    return { success: true };
  } catch (error) {
    return { success: false, message: '无效的access token' };
  }
}

之后处理其他Socket事件时,直接从socket.data中读取用户信息或token,无需每次从事件参数提取。

2. 客户端自动在Socket事件的auth字段携带新token

Socket.io支持在emit事件时附带auth选项,客户端可以在拿到新token后,全局配置或每次emit时手动携带:

// 客户端全局设置auth,后续所有emit都会自动带上
socket.io.opts.auth = { token: newAccessToken };

// 或者单次emit时指定
socket.emit('random-event', randomParam, { auth: { token: newAccessToken } });

服务器端可以通过自定义Socket守卫统一处理鉴权,自动提取auth字段的token:

// 自定义Socket守卫
@Injectable()
export class SocketAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const client = context.switchToWs().getClient<Socket>();
    // 优先级:事件auth > 握手auth > 上下文存储的token
    const token = 
      client.handshake.auth.token || 
      (client.handshake.headers?.authorization?.split(' ')[1]) || 
      client.data.accessToken;

    if (!token) {
      client.emit('auth-error', { code: 'NO_TOKEN' });
      return false;
    }

    try {
      const payload = await this.jwtService.verifyAsync(token);
      client.data.user = payload;
      return true;
    } catch (error) {
      // 区分过期和无效错误,给客户端明确提示
      const errorCode = error.name === 'TokenExpiredError' ? 'TOKEN_EXPIRED' : 'INVALID_TOKEN';
      client.emit('auth-error', { code: errorCode });
      return false;
    }
  }
}

在需要鉴权的Socket网关或事件上添加@UseGuards(SocketAuthGuard)即可实现统一拦截。

三、完整流程示例

  1. 首次连接:客户端通过client.handshake.auth.token携带access_token,服务器守卫验证通过后,将用户信息存储到socket.data。
  2. token过期触发:客户端发起Socket事件时,服务器守卫检测到token过期,返回TOKEN_EXPIRED错误。
  3. 刷新token:客户端调用REST刷新接口,携带refresh_token获取新的access_token。
  4. 更新Socket鉴权:客户端发送update-access-token事件或设置全局Socket auth,将新token传递给服务器。
  5. 恢复正常操作:服务器更新socket.data后,后续事件处理自动使用新的鉴权信息,无需断开连接。

四、额外注意事项

  • Refresh Token安全:客户端要将refresh_token存储在HttpOnly Cookie或安全的本地存储中,传输必须用HTTPS/WSS,防止泄露。
  • 主动过期提醒:服务器可以在token即将过期前(比如提前5分钟)主动发送token-expiring事件给客户端,提醒提前刷新,提升用户体验。
  • 统一鉴权逻辑:所有Socket事件的鉴权都通过守卫处理,不要在每个事件处理器里重复写验证代码,保持代码简洁可维护。

内容的提问来源于stack exchange,提问作者Ignacio Miguez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 20:23:17