Spring Security 实战:彻底解决 CORS 跨域凭据问题与 WebSocket 连接失败

在前后端分离架构中,跨域(CORS)问题是开发者绕不开的"坑",尤其是当请求需要携带凭据(如 Cookie、Token)时,稍有配置不当就会触发浏览器拦截。本文结合实际项目中遇到的"CORS 凭据不匹配""WebSocket 403 拒绝访问"等问题,详细讲解如何在 Spring Security 中正确配置跨域,以及避坑要点。

一、问题背景:从报错日志看核心矛盾

项目中先后出现两类跨域相关错误,本质都是配置不兼容导致:

1. CORS 凭据不匹配错误

前端请求携带 withCredentials: true(需传递 Cookie/Token)时,浏览器报错:

复制代码
Access to XMLHttpRequest at 'http://127.0.0.1:8080/api/ws/info' from origin 'null' has been blocked by CORS policy: 
The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.

核心原因 :后端未返回 Access-Control-Allow-Credentials: true 响应头,与前端凭据模式不匹配。

2. WebSocket 连接失败与 403 错误

WebSocket 请求(如 ws://127.0.0.1:8080/api/ws/123/abc/websocket)被拦截,同时出现:

  • WebSocket connection to 'ws://...' failed
  • GET http://.../eventsource 403 (Forbidden)
    核心原因:Spring Security 未放行 WebSocket 相关路径,且 WebSocket 未单独配置跨域。

二、解决方案:Spring Security 跨域配置实战

针对上述问题,需从"HTTP 跨域凭据配置"和"WebSocket 跨域放行"两方面入手,以下是完整可复用的脱敏配置方案(已替换敏感路径、类名,保留核心逻辑)。

1. 核心:HTTP 跨域凭据配置(解决 CORS 凭据不匹配)

Spring Security 中配置跨域的关键是:明确允许凭据传递、确保 CORS 过滤器优先执行、避免配置冲突

完整 Security 配置代码
java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

/**
 * 安全配置类:负责权限控制、跨域配置、过滤器管理
 */
@Configuration
public class AppSecurityConfig {

    // 注入自定义组件(脱敏:替换为项目通用命名,避免暴露业务细节)
    private final AuthFailHandler authFailHandler; // 认证失败处理器
    private final LogoutHandler appLogoutHandler; // 退出成功处理器
    private final TokenAuthFilter tokenAuthFilter; // Token认证过滤器
    private final WhiteListProperties whiteListProps; // 白名单URL配置(从配置文件读取)

    // 构造函数注入(Lombok @RequiredArgsConstructor 可简化代码)
    public AppSecurityConfig(AuthFailHandler authFailHandler,
                            LogoutHandler appLogoutHandler,
                            TokenAuthFilter tokenAuthFilter,
                            WhiteListProperties whiteListProps) {
        this.authFailHandler = authFailHandler;
        this.appLogoutHandler = appLogoutHandler;
        this.tokenAuthFilter = tokenAuthFilter;
        this.whiteListProps = whiteListProps;
    }

    /**
     * 配置Security过滤器链:核心权限与跨域规则
     */
    @Bean
    protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                // 1. 配置CORS:注入自定义跨域配置源(优先级最高)
                .cors(cors -> cors.configurationSource(corsConfigSource()))
                // 2. 禁用CSRF:前后端分离用Token认证,无需依赖Session
                .csrf(csrf -> csrf.disable())
                // 3. 响应头配置:解决X-Frame-Options嵌入限制、缓存问题
                .headers(headers -> headers
                        .cacheControl(cache -> cache.disable()) // 禁用浏览器缓存敏感资源
                        .frameOptions(options -> options.disable()) // 允许跨域页面嵌入(按需调整)
                )
                // 4. 异常处理:认证失败时返回统一格式响应
                .exceptionHandling(ex -> ex
                        .authenticationEntryPoint(authFailHandler)
                )
                // 5. Session配置:无状态模式(Token认证不需要Session)
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                // 6. 授权规则:白名单路径匿名访问,其余需认证
                .authorizeHttpRequests(auth -> {
                    // 放行配置文件中的白名单URL(如第三方回调、公开接口)
                    whiteListProps.getUrls().forEach(url -> auth.antMatchers(url).permitAll());
                    // 基础公开接口:登录、注册、验证码
                    auth.antMatchers("/api/auth/login", "/api/auth/register", "/api/auth/captcha").permitAll()
                            // 静态资源:HTML、CSS、JS、图片等
                            .antMatchers(HttpMethod.GET, 
                                    "/", "/*.html", "/static/**/*.html", 
                                    "/static/**/*.css", "/static/**/*.js", "/static/**/*.png")
                            .permitAll()
                            // 文档资源:Swagger、监控页面(生产环境建议关闭)
                            .antMatchers("/swagger-ui/**", "/swagger-resources/**", 
                                    "/webjars/**", "/v3/api-docs/**", "/monitor/druid/**")
                            .permitAll()
                            // WebSocket相关路径:全部放行(含动态子路径)
                            .antMatchers("/api/ws/**").permitAll()
                            // 其余所有请求:必须认证(Token有效)
                            .anyRequest().authenticated();
                })
                // 7. 退出登录配置:指定退出接口与处理器
                .logout(logout -> logout
                        .logoutUrl("/api/auth/logout")
                        .logoutSuccessHandler(appLogoutHandler)
                        .clearAuthentication(true) // 清除认证信息
                )
                // 8. 注入Token过滤器:在用户名密码过滤器前执行(优先验证Token)
                .addFilterBefore(tokenAuthFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }

    /**
     * 自定义CORS配置源:解决跨域凭据问题的核心配置
     */
    @Bean
    public CorsConfigurationSource corsConfigSource() {
        CorsConfiguration corsConfig = new CorsConfiguration();

        // 1. 允许的来源:生产环境必须替换为具体前端域名(如"https://app.xxx.com")
        // 注意:addAllowedOriginPattern支持通配符且兼容凭据模式,addAllowedOrigin不支持
        corsConfig.addAllowedOriginPattern("*");

        // 2. 允许的请求头:支持所有自定义头(如Token、语言标识)
        corsConfig.addAllowedHeader("*");

        // 3. 允许的请求方法:GET、POST、PUT、DELETE等常用方法
        corsConfig.addAllowedMethod(HttpMethod.GET);
        corsConfig.addAllowedMethod(HttpMethod.POST);
        corsConfig.addAllowedMethod(HttpMethod.PUT);
        corsConfig.addAllowedMethod(HttpMethod.DELETE);
        corsConfig.addAllowedMethod(HttpMethod.OPTIONS); // 允许预检请求

        // 4. 关键配置:允许凭据传递(与前端withCredentials: true匹配)
        corsConfig.setAllowCredentials(true);

        // 5. 预检请求有效期:3600秒(减少浏览器重复发送OPTIONS请求)
        corsConfig.setMaxAge(3600L);

        // 6. 应用范围:所有接口路径都生效
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", corsConfig);

        return configSource;
    }
}

2. 补充:WebSocket 跨域配置(解决连接失败)

若项目使用 WebSocket(如实时通知、消息推送),需单独配置 WebSocket 跨域,确保与 HTTP 跨域规则一致(已脱敏路径与类名):

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;

/**
 * WebSocket配置类:处理实时连接的跨域与路径映射
 */
@Configuration
@EnableWebSocket // 启用WebSocket支持
public class AppWebSocketConfig implements WebSocketConfigurer {

    // 注入自定义WebSocket处理器(脱敏:替换为项目实际处理器)
    private final AppWebSocketHandler webSocketHandler;

    public AppWebSocketConfig(AppWebSocketHandler webSocketHandler) {
        this.webSocketHandler = webSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 1. 注册处理器:映射所有/api/ws/**路径的WebSocket请求
        // 2. 跨域配置:生产环境替换为具体前端域名(如"https://app.xxx.com")
        // 3. SockJS降级:兼容不支持WebSocket的浏览器(按需启用)
        registry.addHandler(webSocketHandler, "/api/ws/**")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}

3. 配套配置类(脱敏:白名单与处理器示例)

为确保配置完整性,补充核心依赖类的脱敏示例(仅展示结构,无业务敏感信息):

(1)白名单配置类(读取配置文件)
java 复制代码
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;

/**
 * 白名单配置:从application.yml读取无需认证的URL
 */
@Component
@ConfigurationProperties(prefix = "app.security.white-list")
public class WhiteListProperties {
    // 白名单URL列表(如:/api/callback/thirdparty, /api/public/notice)
    private List<String> urls;

    // Getter & Setter
    public List<String> getUrls() {
        return urls;
    }

    public void setUrls(List<String> urls) {
        this.urls = urls;
    }
}
(2)认证失败处理器(统一响应格式)
java 复制代码
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 认证失败处理器:返回统一的JSON格式错误响应
 */
public class AuthFailHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, 
                         HttpServletResponse response, 
                         AuthenticationException authException) throws IOException {
        // 统一响应格式(脱敏:替换为项目实际响应工具类)
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"code\":401,\"msg\":\"认证失败,请重新登录\",\"data\":null}");
    }
}

三、关键配置解析:避坑要点

很多开发者配置跨域后仍报错,本质是没理解以下核心规则:

1. allowCredentials: true 的强制约束

corsConfig.setAllowCredentials(true) 时,浏览器禁止 Access-Control-Allow-Origin* ,因此必须用 addAllowedOriginPattern("*") 替代 addAllowedOrigin("*")(Spring 5.3+ 支持)。

  • 错误用法:corsConfig.addAllowedOrigin("*") → 浏览器拦截响应(安全限制)
  • 正确用法:corsConfig.addAllowedOriginPattern("*") → 兼容凭据模式,支持通配符

2. CORS 过滤器的执行顺序

Spring Security 链中,CORS 过滤器必须优先于认证过滤器(如 Token 过滤器) ,否则请求会先被认证拦截,导致 CORS 头未返回。

本文通过 .cors(cors -> cors.configurationSource(...)) 配置,Spring 会自动将 CORS 过滤器加入到 Security 链最前端,避免手动 addFilterBefore 导致的顺序冲突。

3. WebSocket 路径的放行规则

WebSocket 请求路径通常包含动态参数(如 /api/ws/123/abc/websocket),因此必须用通配符 /** 放行所有子路径:

  • 错误:.antMatchers("/api/ws", "/api/ws/info").permitAll() → 仅放行固定路径
  • 正确:.antMatchers("/api/ws/**").permitAll() → 放行所有 /api/ws 下的子路径

4. 前端配合配置(脱敏示例)

跨域是前后端协同问题,前端需确保请求启用凭据模式(以 Axios 和 WebSocket 为例):

javascript 复制代码
// 1. Axios HTTP请求(携带Token凭据)
axios({
  url: "http://127.0.0.1:8080/api/ws/info",
  method: "GET",
  withCredentials: true, // 启用凭据传递(必须与后端匹配)
  headers: {
    "Authorization": "Bearer " + token // 示例:Token放在请求头
  }
});

// 2. WebSocket连接(实时通信)
const ws = new WebSocket("ws://127.0.0.1:8080/api/ws/123/abc/websocket");
// 若用SockJS降级(兼容低版本浏览器)
const sock = new SockJS("http://127.0.0.1:8080/api/ws", null, {
  withCredentials: true
});

四、验证与排错

配置完成后,可通过浏览器"Network"面板验证,或使用 Postman 模拟跨域请求:

1. 验证核心响应头

成功请求应包含以下头信息(可在"Response Headers"中查看):

复制代码
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: 前端实际域名(如 http://localhost:8081)
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Max-Age: 3600

2. 常见错误排错步骤

错误现象 可能原因 解决方案
403 Forbidden 1. 目标路径未加入白名单 2. Token过滤器拦截了WebSocket请求 1. 检查 authorizeHttpRequests 中是否放行路径 2. 在Token过滤器中排除WebSocket路径(如 /api/ws/**
WebSocket连接失败 1. WebSocket跨域未配置 2. 路径未放行 1. 检查 AppWebSocketConfigsetAllowedOrigins 配置 2. 确认 .antMatchers("/api/ws/**").permitAll() 已添加
X-Frame-Options错误 frameOptions 配置为 sameOrigin headers.frameOptions(options -> options.disable()) 启用

五、生产环境优化建议

  1. 限制允许的来源 :将 addAllowedOriginPattern("*") 替换为具体前端域名(如 https://app.xxx.com),避免任意域名跨域,降低安全风险。
  2. 缩小请求方法范围 :若接口仅支持 GET/POST,可删除 PUT/DELETE 方法配置,减少攻击面(如 corsConfig.addAllowedMethod(HttpMethod.GET))。
  3. 关闭不必要功能:生产环境建议关闭 Swagger、Druid 等文档/监控页面,或通过 IP 白名单限制访问。
  4. 日志排查 :开启 Security debug 日志(logging.level.org.springframework.security=DEBUG),快速定位拦截原因(生产环境需关闭)。

总结

跨域问题的核心是"前后端配置匹配",尤其是凭据模式下,需确保:

  • 后端返回 Access-Control-Allow-Credentials: true 响应头
  • 允许的来源用 addAllowedOriginPattern 而非 addAllowedOrigin
  • WebSocket 路径单独放行并配置跨域
  • CORS 过滤器在 Security 链中优先执行

按照本文脱敏配置,可彻底解决 CORS 凭据不匹配、WebSocket 连接失败等问题,同时兼顾安全性与灵活性,适配大多数前后端分离项目场景。

相关推荐
winrisef2 小时前
删除无限递归文件夹
java·ide·python·pycharm·系统安全
悦悦子a啊2 小时前
Java面向对象练习:Person类继承与排序
java·开发语言·python
不会算法的小灰2 小时前
Spring Boot 实现邮件发送功能:整合 JavaMailSender 与 FreeMarker 模板
java·spring boot·后端
come112343 小时前
深入理解 Java和Go语法和使用场景(指南十一)
java·开发语言·golang
李贺梖梖9 小时前
DAY23 单例设计模式、多例设计模式、枚举、工厂设计模式、动态代理
java
武昌库里写JAVA9 小时前
Java设计模式之工厂模式
java·vue.js·spring boot·后端·sql
赛姐在努力.11 小时前
SpringMVC中的常用注解及使用方法
java·spring
让我上个超影吧12 小时前
黑马点评秒杀优化和场景补充
java
寻星探路12 小时前
Java EE初阶启程记06---synchronized关键字
java·java-ee