Spring Security集成StompJS/SockJS WebSocket时的CORS问题
WebSocket集成时的CORS错误解决方案
问题场景
为Web项目添加WebSocket功能时触发CORS错误,排查后怀疑是Spring Security与WebSocket配置规则冲突导致,相关代码如下:
WebSocketConfig
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/message"); config.setApplicationDestinationPrefixes("/app"); } }
Spring SecurityConfig
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsService userDetailsService; private final BCryptPasswordEncoder bCryptPasswordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); } @Override protected void configure(HttpSecurity http) throws Exception { CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean()); customAuthenticationFilter.setFilterProcessesUrl("/api/login"); http.csrf().disable().cors().configurationSource(request -> { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); configuration.addAllowedHeader("*"); return configuration; }).and() .authorizeRequests().anyRequest().permitAll().and() .addFilter(customAuthenticationFilter) .addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
CustomAuthorizationFilter
public class CustomAuthorizationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/token/refresh")) { filterChain.doFilter(request, response); } else { String authorizationHeader = request.getHeader(AUTHORIZATION); if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { try { String token = authorizationHeader.substring("Bearer ".length()); Algorithm algorithm = Algorithm.HMAC256("secret".getBytes()); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT decodedJWT = verifier.verify(token); String username = decodedJWT.getSubject(); String[] roles = decodedJWT.getClaim("roles").asArray(String.class); Collection<SimpleGrantedAuthority> authorities = new ArrayList<>(); Arrays.stream(roles).forEach(role -> { authorities.add(new SimpleGrantedAuthority(role)); }); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( username, null, authorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } catch (Exception exception) { log.error("Error logging in: {}", exception.getMessage()); response.setHeader("error", exception.getMessage()); response.setStatus(FORBIDDEN.value()); Map<String, String> error = new HashMap<>(); error.put("error_message", exception.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); new ObjectMapper().writeValue(response.getOutputStream(), error); } } else { filterChain.doFilter(request, response); } } } }
Angular端TokenInterceptor
@Injectable() export class TokenInterceptor implements HttpInterceptor { constructor(private auth: AuthService, private router: Router) { } intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> | any { const myToken = this.auth.getToken(); if (myToken) { request = request.clone({ setHeaders: { Authorization: `Bearer ${myToken}` }, }); } return next.handle(request).pipe( catchError((err: any) => { if (err instanceof HttpErrorResponse) { if (err.status === 401) { return this.handleUnAuthorizedError(request, next) } } return throwError(() => new Error("Some other error occured")) }) ); } handleUnAuthorizedError(req: HttpRequest<any>, next: HttpHandler) { let tokenApiModel = new TokenApiModel(); tokenApiModel.access_token = this.auth.getToken()!; tokenApiModel.refresh_token = this.auth.getRefreshToken()!; return this.auth.renewToken(tokenApiModel) .pipe( switchMap((data: TokenApiModel) => { this.auth.storeRefreshToken(data.refresh_token) this.auth.storeToken(data.access_token) req = req.clone({ setHeaders: { Authorization: `Bearer ${data.access_token}` }, withCredentials: false }); return next.handle(req) }), catchError((err) => { return throwError(() => { alert("Token is expired, login again") this.router.navigate(["login"]) }) }) ) } }
Angular端SocketService
@Injectable({ providedIn: 'root' }) export class SocketService { public stompClient: any; public msg: Array<any> = []; constructor() { const serverUrl = 'http://localhost:8080/socket'; const ws = new SockJS(serverUrl); this.stompClient = Stomp.over(ws); } initializeWebSocketConnection() { const that = this; console.log(this.stompClient); // It work until here this.stompClient.connect({}, function (frame: any) { that.stompClient.subscribe('/message', (message: any) => { console.log(message); if (message.body) { that.msg.push(message.body); } }); that.stompClient.subscribe('/messages', (message: any) => { console.log(message); if (message.body) { that.msg.push(message.body); } }); }); } sendMessage(message: any) { this.stompClient.send('/app/send/message', {}, message); } }
解决方案
1. 统一CORS规则,避免WebSocket与Security配置不一致
WebSocketConfig中setAllowedOrigins("*")和Security里的允许源冲突,修改WebSocketConfig,将允许源和Security保持一致:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/socket") .setAllowedOrigins("http://localhost:4200") // 和Security配置匹配 .withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/message"); config.setApplicationDestinationPrefixes("/app"); } }
2. 更新Security配置,放行WebSocket请求与OPTIONS预检
SockJS握手前会发送OPTIONS预检请求,需要在Security中明确放行该方法,同时确保WebSocket端点不被拦截:
@Override protected void configure(HttpSecurity http) throws Exception { CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean()); customAuthenticationFilter.setFilterProcessesUrl("/api/login"); http.csrf().disable() .cors().configurationSource(request -> { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 添加OPTIONS方法 configuration.addAllowedHeader("*"); configuration.setAllowCredentials(true); // 允许携带凭证(可选,根据需求) return configuration; }).and() .authorizeRequests() .antMatchers("/socket/**", "/api/login/**", "/api/token/refresh/**", "/api/user/save/**").permitAll() // 明确放行WebSocket端点 .anyRequest().permitAll().and() .addFilter(customAuthenticationFilter) .addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); }
3. 调整CustomAuthorizationFilter,跳过WebSocket请求
当前过滤器会拦截除登录、刷新token外的所有请求,包括WebSocket握手,需要添加跳过规则:
public class CustomAuthorizationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String servletPath = request.getServletPath(); // 跳过WebSocket、登录、刷新token的请求 if (servletPath.equals("/api/login") || servletPath.equals("/api/token/refresh") || servletPath.startsWith("/socket/")) { filterChain.doFilter(request, response); } else { // 原有认证逻辑保持不变 String authorizationHeader = request.getHeader(AUTHORIZATION); if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { try { String token = authorizationHeader.substring("Bearer ".length()); Algorithm algorithm = Algorithm.HMAC256("secret".getBytes()); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT decodedJWT = verifier.verify(token); String username = decodedJWT.getSubject(); String[] roles = decodedJWT.getClaim("roles").asArray(String.class); Collection<SimpleGrantedAuthority> authorities = new ArrayList<>(); Arrays.stream(roles).forEach(role -> { authorities.add(new SimpleGrantedAuthority(role)); }); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( username, null, authorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } catch (Exception exception) { log.error("Error logging in: {}", exception.getMessage()); response.setHeader("error", exception.getMessage()); response.setStatus(FORBIDDEN.value()); Map<String, String> error = new HashMap<>(); error.put("error_message", exception.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); new ObjectMapper().writeValue(response.getOutputStream(), error); } } else { filterChain.doFilter(request, response); } } } }
4. Angular端SocketService优化(可选)
添加错误回调方便排查问题,同时根据后端配置开启凭证携带:
@Injectable({ providedIn: 'root' }) export class SocketService { public stompClient: any; public msg: Array<any> = []; constructor() { const serverUrl = 'http://localhost:8080/socket'; const ws = new SockJS(serverUrl, null, { withCredentials: true // 后端允许的话开启 }); this.stompClient = Stomp.over(ws); } initializeWebSocketConnection() { const that = this; this.stompClient.connect({}, function (frame: any) { that.stompClient.subscribe('/message', (message: any) => { console.log(message); if (message.body) { that.msg.push(message.body); } }); that.stompClient.subscribe('/messages', (message: any) => { console.log(message); if (message.body) { that.msg.push(message.body); } }); }, function(error: any) { console.error('WebSocket连接失败:', error); // 添加错误回调 }); } sendMessage(message: any) { this.stompClient.send('/app/send/message', {}, message); } }
内容的提问来源于stack exchange,提问作者Tuan Le Anh
相关产品推荐
相关产品推荐

