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 连接失败等问题,同时兼顾安全性与灵活性,适配大多数前后端分离项目场景。

相关推荐
旺仔小拳头..20 分钟前
Maven相关
java·maven
要一起看日出22 分钟前
数据结构---------红黑树
java·数据结构·红黑树
程序定小飞30 分钟前
基于springboot的民宿在线预定平台开发与设计
java·开发语言·spring boot·后端·spring
FREE技术44 分钟前
山区农产品售卖系统
java·spring boot
星光一影1 小时前
Java医院管理系统HIS源码带小程序和安装教程
java·开发语言·小程序
YA3332 小时前
java设计模式七、代理模式
java·设计模式·代理模式
helloworddm2 小时前
Orleans 自定义二进制协议在 TCP 上层实现的完整过程
java·网络协议·tcp/ip
超级大只老咪3 小时前
蓝桥杯知识点大纲(JavaC组)
java·算法·蓝桥杯
Yiii_x3 小时前
如何使用IntelliJ IDEA进行Java编程
java·课程设计·ai编程
阿杰AJie3 小时前
如何在程序中避免出现大量if和case
java·后端