NestJS混合项目中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)即可实现统一拦截。
三、完整流程示例
- 首次连接:客户端通过
client.handshake.auth.token携带access_token,服务器守卫验证通过后,将用户信息存储到socket.data。 - token过期触发:客户端发起Socket事件时,服务器守卫检测到token过期,返回
TOKEN_EXPIRED错误。 - 刷新token:客户端调用REST刷新接口,携带refresh_token获取新的access_token。
- 更新Socket鉴权:客户端发送
update-access-token事件或设置全局Socket auth,将新token传递给服务器。 - 恢复正常操作:服务器更新
socket.data后,后续事件处理自动使用新的鉴权信息,无需断开连接。
四、额外注意事项
- Refresh Token安全:客户端要将refresh_token存储在HttpOnly Cookie或安全的本地存储中,传输必须用HTTPS/WSS,防止泄露。
- 主动过期提醒:服务器可以在token即将过期前(比如提前5分钟)主动发送
token-expiring事件给客户端,提醒提前刷新,提升用户体验。 - 统一鉴权逻辑:所有Socket事件的鉴权都通过守卫处理,不要在每个事件处理器里重复写验证代码,保持代码简洁可维护。
内容的提问来源于stack exchange,提问作者Ignacio Miguez

