在前后端分离架构中,跨域(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. 检查 AppWebSocketConfig 中 setAllowedOrigins 配置 2. 确认 .antMatchers("/api/ws/**").permitAll() 已添加 |
X-Frame-Options错误 | frameOptions 配置为 sameOrigin |
将 headers.frameOptions(options -> options.disable()) 启用 |
五、生产环境优化建议
- 限制允许的来源 :将
addAllowedOriginPattern("*")
替换为具体前端域名(如https://app.xxx.com
),避免任意域名跨域,降低安全风险。 - 缩小请求方法范围 :若接口仅支持 GET/POST,可删除
PUT/DELETE
方法配置,减少攻击面(如corsConfig.addAllowedMethod(HttpMethod.GET)
)。 - 关闭不必要功能:生产环境建议关闭 Swagger、Druid 等文档/监控页面,或通过 IP 白名单限制访问。
- 日志排查 :开启 Security debug 日志(
logging.level.org.springframework.security=DEBUG
),快速定位拦截原因(生产环境需关闭)。
总结
跨域问题的核心是"前后端配置匹配",尤其是凭据模式下,需确保:
- 后端返回
Access-Control-Allow-Credentials: true
响应头 - 允许的来源用
addAllowedOriginPattern
而非addAllowedOrigin
- WebSocket 路径单独放行并配置跨域
- CORS 过滤器在 Security 链中优先执行
按照本文脱敏配置,可彻底解决 CORS 凭据不匹配、WebSocket 连接失败等问题,同时兼顾安全性与灵活性,适配大多数前后端分离项目场景。