CORS
跨域资源共享(Cross-Origin Resource Sharing,CORS)是浏览器实施的同源安全策略的放宽机制。Spring Security 通过 CorsFilter 与 CorsConfigurationSource 配合,允许服务器精确声明哪些外部源可以访问本域资源,是前后端分离架构的必备配置。
定义与作用
一句话定义:浏览器默认阻止跨域 AJAX 请求,CORS 通过一组 HTTP 响应头(Access-Control-Allow-*)告知浏览器:服务器允许特定域名的脚本访问本域资源。
| 场景 | 是否跨域 | 示例 |
|---|---|---|
| 同域 | 否 | https://a.com/page → https://a.com/api |
| 不同域名 | 是 | https://app.com → https://api.com |
| 不同端口 | 是 | https://a.com → https://a.com:8080 |
| 不同协议 | 是 | http://a.com → https://a.com |
两类跨域请求:
- 简单请求:GET / HEAD / POST(Content-Type 为
application/x-www-form-urlencoded、multipart/form-data、text/plain),直接发送,浏览器通过响应头判断是否放行; - 预检请求(Preflight):非简单请求(如 PUT / DELETE、自定义 Header、Content-Type 为
application/json)先发送OPTIONS请求,服务器返回许可后,浏览器才发送实际请求。
Spring Security 的 CorsFilter 位于过滤器链第 5 位(在 HeaderWriterFilter 之后、CsrfFilter 之前),负责拦截 CORS 相关请求并写入响应头。
攻击原理与防护流程
核心原理:服务器通过 CorsConfigurationSource 声明允许的源(Origin)、方法(Methods)、头(Headers)、凭证(Credentials)等规则;CorsFilter 根据请求 Origin 头匹配配置,将对应规则写入响应头返回给浏览器;浏览器依据响应头决定是否放行。跨域请求的拦截权在浏览器,而非服务器——服务器始终会收到请求,CORS 防止的是恶意站点读取响应内容。
核心配置:CorsConfigurationSource
1. 基于 URL 映射的精细化配置(推荐)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.configurationSource(corsConfigurationSource())
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/**").authenticated();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("https://app.example.com", "https://admin.example.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
config.setExposedHeaders(Arrays.asList("X-Total-Count")); // 允许前端读取的响应头
config.setAllowCredentials(true); // 允许携带 Cookie / Authorization Header
config.setMaxAge(3600L); // 预检请求缓存 1 小时
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config); // 仅对 API 路径生效
return source;
}
}
2. 使用 CorsRegistry 简化配置(Spring MVC 层,Spring Security 可复用)
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(3600);
}
}
注意:若使用 WebMvcConfigurer 配置,Spring Security 中只需 http.cors() 而不需要显式 configurationSource(),Spring 会自动检测并委托给 MVC 层的 CORS 配置。
完整示例一:前后端分离项目配置精确跨域
场景说明
电商系统前端部署在 https://mall.example.com,后端 API 在 https://api.mall.com。前端需要调用 /api/orders 获取订单数据,且登录态通过 Cookie 维持。要求仅允许该前端域名访问,禁止其他域名的脚本窃取数据。
操作前配置(无 CORS 或配置错误,浏览器报错)
@EnableWebSecurity
public class MallSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 错误:不应禁用,同时前端需要 Cookie
// 未配置 cors(),浏览器默认阻止所有跨域请求
.authorizeRequests()
.antMatchers("/api/orders/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/api/login")
.permitAll();
}
}
浏览器控制台报错:Access to XMLHttpRequest at 'https://api.mall.com/api/orders' from origin 'https://mall.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
前端无法获取任何响应数据,即使后端服务器已正常处理请求(可通过后端日志确认)。
操作后配置(精确源 + 凭证 + 预检缓存)
@EnableWebSecurity
public class MallSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.configurationSource(corsConfigurationSource())
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/orders/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/api/login")
.permitAll();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("https://mall.example.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-CSRF-TOKEN"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
结果分析:前端 axios.get('https://api.mall.com/api/orders') 携带 Origin: https://mall.example.com 和 Cookie: JSESSIONID=...。CorsFilter 匹配到 /api/** 配置,返回 Access-Control-Allow-Origin: https://mall.example.com 和 Access-Control-Allow-Credentials: true。浏览器校验通过,前端成功获取订单 JSON。恶意站点 https://evil.com 发起请求时,Origin 不匹配,响应中无 Access-Control-Allow-Origin,浏览器拒绝其读取响应。
完整示例二:多环境动态 CORS 配置
场景说明
SaaS 平台支持客户自定义前端域名,后端需要根据当前租户白名单动态判断是否允许跨域。不能使用写死的 allowedOrigins,必须从数据库查询当前租户配置的域名。
操作前配置(硬编码多源,维护困难)
@EnableWebSecurity
public class SaasSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
// ❌ 硬编码:每新增一个客户域名都要改代码重启
config.setAllowedOrigins(Arrays.asList(
"https://tenant-a.com",
"https://tenant-b.com",
"https://tenant-c.com"
));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
return config;
})
.and()
.authorizeRequests()
.antMatchers("/api/tenant/**").authenticated();
}
}
操作后配置(动态从数据库读取白名单)
@EnableWebSecurity
public class SaasSecurityConfig extends WebSecurityConfigurerAdapter {
private final TenantDomainService tenantDomainService;
public SaasSecurityConfig(TenantDomainService tenantDomainService) {
this.tenantDomainService = tenantDomainService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.configurationSource(tenantCorsConfigurationSource())
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.antMatchers("/api/tenant/**").authenticated();
}
@Bean
public CorsConfigurationSource tenantCorsConfigurationSource() {
return request -> {
String origin = request.getHeader("Origin");
String tenantId = request.getHeader("X-Tenant-ID");
List<String> allowedDomains = tenantDomainService.findAllowedDomains(tenantId);
CorsConfiguration config = new CorsConfiguration();
if (origin != null && allowedDomains.contains(origin)) {
config.setAllowedOrigins(Collections.singletonList(origin));
}
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Tenant-ID", "X-CSRF-TOKEN"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
return config;
};
}
}
结果分析:每次跨域请求到达时,CorsFilter 调用自定义 CorsConfigurationSource,根据 X-Tenant-ID 查询数据库获取该租户白名单。只有白名单内的域名才会被写入 Access-Control-Allow-Origin。新增客户域名只需在管理后台配置,无需修改代码或重启服务。若攻击者使用伪造 Origin 或未知域名,返回的 CorsConfiguration 不包含该 Origin,浏览器拒绝响应。
易错场景:允许 Credentials 时 Origin 不能为 *
面试高频考点:配置 allowCredentials(true) 时,如果同时设置 allowedOrigins(Collections.singletonList("*")),会导致 CORS 配置冲突,浏览器仍会报错。
错误场景还原:
@Bean
public CorsConfigurationSource brokenCorsSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*")); // ❌ 通配符
config.setAllowedMethods(Arrays.asList("GET", "POST"));
config.setAllowCredentials(true); // ❌ 同时允许凭证
// ...
}
浏览器报错:The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
原因分析:W3C CORS 规范明确规定,当请求携带凭证(Cookie / Authorization Header)时,服务器返回的 Access-Control-Allow-Origin 必须是具体的 Origin 值,不能是通配符 *。这是浏览器的安全限制,防止凭证泄露给任意域。
正确做法:必须显式列出具体域名,或使用 setAllowedOriginPatterns(Spring 5.3+ 支持模式匹配,但仍不能为 * 搭配 Credentials):
config.setAllowedOrigins(Arrays.asList("https://app.example.com", "https://admin.example.com"));
config.setAllowCredentials(true); // ✅ 具体 Origin 与 Credentials 搭配
面试追问:如果确实有任意域名访问需求且需要 Cookie,怎么办?
答:这在安全上是不允许的。若业务确实需要,应放弃 Cookie 凭证机制,改用无状态 Token 认证(如 JWT 放在 Header),此时可以关闭
allowCredentials(false)并使用allowedOrigins("*")。但这样等同于放弃 CORS 的凭证安全屏障,需确保 Token 本身有足够的防盗用措施。
核心类速查
| 类 | 说明 |
|---|---|
CorsFilter | 核心过滤器,拦截 CORS 请求并写入响应头 |
CorsConfiguration | CORS 配置对象,封装 Origin / Methods / Headers / Credentials 等规则 |
CorsConfigurationSource | 配置源接口,按请求返回对应的 CorsConfiguration |
UrlBasedCorsConfigurationSource | 基于 URL 路径映射的配置源实现 |
CorsRegistry | Spring MVC 层简化配置入口,与 WebMvcConfigurer 配合使用 |