Spring Boot中WebSocket连接403 Forbidden问题及STOMP协议后续请求认证方案咨询
一、先解决你遇到的WebSocket握手403 Forbidden问题
你已经在SecurityConfiguration里给/ws**设了permitAll(),但还是403,大概率是因为Spring Security对WebSocket有独立的安全配置链,仅靠HttpSecurity的配置不够。你需要额外配置WebSocket的安全规则,具体可以通过实现SecurityWebSocketMessageBrokerConfigurer来完成:
@Configuration public class WebSocketSecurityConfig implements SecurityWebSocketMessageBrokerConfigurer { @Override public void configureInbound(MessageSecurityMetadataSourceRegistry messages) { // 结合HttpSecurity的握手permitAll配置,这里设置消息层面的权限规则 messages // 允许所有订阅/发送到/user/**的消息(对应你的Broker前缀) .simpSubscribeDestMatchers("/user/**").permitAll() // 要求所有/app/**的消息必须认证(对应你的@MessageMapping接口路径) .simpDestMatchers("/app/**").authenticated() // 拒绝其他所有STOMP消息 .anyMessage().denyAll(); } }
另外检查下CORS配置的一致性:你的WebSocketConfig里setAllowedOriginPatterns("http://localhost:49322")要和Spring Security的CORS配置完全匹配,避免跨域预检失败导致的403。
二、STOMP协议后续请求的认证方案
根据你提到的两种业务场景,分别对应不同的实现方式:
场景1:握手阶段就携带JWT令牌(推荐方案)
你现在的客户端已经在握手时传递了Authorization头:
this.stompClient.connect({"Authorization" : `Bearer ${this.token}`}, () => { ... })
因为WebSocket握手是HTTP请求,你的JwtAuthFilter会自动处理这个请求头,验证JWT并将Authentication对象绑定到WebSocket会话中。后续所有通过该会话发送的STOMP消息,都会自动继承这个会话的Principal身份信息。
这种情况下,你只需要通过WebSocketSecurityConfig的configureInbound方法配置消息权限即可(比如要求/app/**必须认证),不用再为每个消息单独验证令牌,@AuthenticationPrincipal UserDetails user可以直接在@MessageMapping方法中获取当前用户。
场景2:握手允许匿名,仅STOMP消息携带令牌
如果业务需要握手阶段匿名,仅在发送具体消息时携带令牌(比如你示例中的stompClient.send带Authorization头),那你需要自定义Channel拦截器,从STOMP消息的请求头中提取JWT并验证,然后将认证信息绑定到当前消息上下文:
@Component public class JwtStompInterceptor implements ChannelInterceptor { private final JwtAuthFilter jwtAuthFilter; private final AuthenticationManager authenticationManager; public JwtStompInterceptor(JwtAuthFilter jwtAuthFilter, AuthenticationManager authenticationManager) { this.jwtAuthFilter = jwtAuthFilter; this.authenticationManager = authenticationManager; } @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); assert accessor != null; // 仅处理客户端发送消息的请求(对应stompClient.send方法) if (StompCommand.SEND.equals(accessor.getCommand())) { String authHeader = accessor.getFirstNativeHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); // 复用已有JWT过滤器的验证逻辑 try { UsernamePasswordAuthenticationToken authToken = jwtAuthFilter.getAuthentication(token); Authentication authenticated = authenticationManager.authenticate(authToken); // 将认证信息绑定到当前消息 accessor.setUser(authenticated); } catch (AuthenticationException e) { throw new AccessDeniedException("无效的JWT令牌"); } } } return message; } }
然后将该拦截器配置到WebSocket的入站通道中,在WebSocketSecurityConfig中添加:
@Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(jwtStompInterceptor); }
配置完成后,客户端每次发送消息时携带的Authorization头会被自动验证,@AuthenticationPrincipal依然可以正常获取用户信息。
三、你的疑问解答
为什么HttpSecurity的permitAll()没生效?
Spring Security的WebSocket安全模块会对握手请求做独立检查,仅靠HttpSecurity的配置无法覆盖WebSocket的安全规则,必须配合SecurityWebSocketMessageBrokerConfigurer的配置才能生效。握手带JWT后,后续STOMP消息为什么能自动认证?
WebSocket会话与初始握手的HTTP会话绑定,握手时验证JWT后设置的Principal会被保存在WebSocket会话中,后续所有通过该会话发送的STOMP消息都会自动携带这个身份信息,无需重复验证。要不要用AbstractSecurityWebSocketMessageBrokerConfigurer?
该抽象类已被标记为Deprecated,直接实现SecurityWebSocketMessageBrokerConfigurer是更推荐的方式,功能完全一致。
备注:内容来源于stack exchange,提问作者Abas jama




