基于令牌的验证 (Token Authentication)
虽然基于 Cookie 的会话在传统 Web 应用中很常见,但在不维护服务器端会话的应用程序或通用标头认证的移动应用中,通常优先使用令牌验证(如 JWT)。
浏览器客户端的局限性
WebSocket 协议 (RFC 6455) 并没有规定服务器在握手期间验证客户端的特定方式。在实践中,浏览器端存在以下限制:
- 无法自定义标头: 浏览器在进行 WebSocket 握手时,无法通过 JavaScript 设置自定义的 HTTP 标头(只能用标准的 Cookie 或 Basic Auth)。
- SockJS 限制:
sockjs-client也不支持发送自定义 HTTP 标头。虽然可以使用查询参数,但这存在令牌泄露到日志中的安全风险。
TIP
上述局限性仅限浏览器。Spring 的 Java STOMP 客户端完全支持在 WebSocket 和 SockJS 请求中发送自定义标头。
在 STOMP 协议级别验证
如果你希望避免使用 Cookie,可以在 STOMP 协议内部进行验证。这通常分为两步:
- 客户端: 在
CONNECT帧时传递认证标头。 - 服务器端: 使用
ChannelInterceptor处理这些标头。
服务器端实现示例
你可以注册一个自定义的拦截器,在 CONNECT 命令到达时提取令牌并设置用户信息:
java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// 1. 提取 Authorization 标头
// 2. 验证令牌(如 JWT)
// 3. 设置 Principal 用户对象
// accessor.setUser(user);
}
return message;
}
});
}
}kotlin
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfiguration : WebSocketMessageBrokerConfigurer {
override fun configureClientInboundChannel(registration: ChannelRegistration) {
registration.interceptors(object : ChannelInterceptor {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> {
val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (StompCommand.CONNECT == accessor?.command) {
// 提取认证标头并调用 accessor.setUser(user)
}
return message
}
})
}
}一旦在 CONNECT 帧中设置了用户,Spring 会记住该认证状态,并将其与同一会话中后续的所有消息关联起来。
补充教学
1. 拦截器的顺序逻辑
如果你也使用了 Spring Security 的消息授权,必须确保这个自定义认证拦截器的优先级更高。建议将其定义在标记有 @Order(Ordered.HIGHEST_PRECEDENCE + 99) 的独立配置类中。
2. 不要重复验证
验证逻辑只需要在 CONNECT 帧执行一次。因为一旦 WebSocket 连接成功,底层是一个长连接,除非连接断开,否则用户的 Principal 身份会一直保留在 Session 属性中。
3. JWT 的典型做法
前端通常会在连接成功前的 CONNECT 参数中传入 Authorization: Bearer <JWT>。拦截器校验 JWT 的有效性和过期时间,然后将解析出的用户信息构造出 UsernamePasswordAuthenticationToken 设置给 accessor。