1. 前言

在日常开发中,跨域资源共享(CORS)已成为前端与后端分离架构的必备能力。正确配置 CORS 是后端安全边界的重要一环。但安全与便利的天平往往难以把握——过于宽松的配置可能导致敏感数据泄露,过于严格的策略又会影响正常业务交互。本文我们继续《Spring Security 实战教程》,讲解 CORS 安全配置。


2. 背景与原理概述

跨域请求是指浏览器在一个域(Origin A)加载的脚本访问另一个域(Origin B)的资源。为保护用户数据,浏览器默认禁止跨域访问,除非服务器在响应头中明确允许。CORS 机制通过 Access-Control-Allow-* 系列头部,告诉浏览器何种跨域请求被允许,从而实现安全的跨域通信。

2.1 预检请求全流程

当发起“简单请求”时(如 GET/POST 且只有简单头部),浏览器会直接带上 Cookie 等凭证;对复杂请求(如 PUT/DELETE、携带自定义头部)则先发起一次预检(OPTIONS)请求,确认服务器同意后才发送实际请求。
预检请求流程示意图

2.2 关键响应头说明

Access-Control-Allow-* 响应头主要包括以下几个:

响应头 安全意义 示例值
Access-Control-Allow-Origin 允许的源列表 https://your-domain.com
Access-Control-Allow-Methods 允许的HTTP方法 GET,POST,PUT
Access-Control-Allow-Headers 允许的请求头 Content-Type,Authorization
Access-Control-Max-Age 预检结果缓存时间(秒) 86400

3. Spring MVC 中的 CORS 支持

Spring Framework 自 4.2 起在 MVC 层面引入了对 CORS 的原生支持,可通过全局配置或注解方式开启。

全局配置(WebMvcConfigurer)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration  
public class CorsGlobalConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 应用到所有 /api 路径
.allowedOrigins("https://your-domain.com") // 允许的源
.allowedMethods("GET", "POST", "PUT") // 允许的方法
.allowedHeaders("Content-Type", "Authorization")
.exposedHeaders("X-Total-Count")
.allowCredentials(true) // 允许携带 Cookie
.maxAge(3600); // 预检结果缓存 1 小时
}
}

注解方式(@CrossOrigin)

1
2
3
4
5
6
7
8
9
10
11
12
@RestController  
@RequestMapping("/users")
@CrossOrigin(
origins = "https://your-domain.com",
methods = {RequestMethod.GET, RequestMethod.POST},
allowCredentials = "true",
maxAge = 1800
)
public class UserController {
@GetMapping
public List<User> list() { … }
}

4. Spring Security 安全配置方案

尽管 Spring MVC 已处理了 CORS,但如果启用了 Spring Security必须在 Spring Security 配置中显式开启 CORS 支持,否则安全过滤器会在 MVC 之前拦截预检请求,导致跨域失败。

全局安全配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration  
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors
.configurationSource(corsConfigurationSource())
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
// 其他安全配置...
return http.build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://your-domain.com"));
config.setAllowedMethods(List.of("GET", "POST"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setExposedHeaders(List.of("X-Custom-Header"));
config.setMaxAge(3600L);
config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}

5. 安全强化最佳实践

5.1 动态源配置方案

很多时候我们并不希望相关配置是写死在代码里的,可以构建 CorsConfigurationSource 来实现动态源配置方案,这里以读取环境变量举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean  
CorsConfigurationSource corsConfigurationSource(Environment env) {
CorsConfiguration config = new CorsConfiguration();

// 从环境变量读取允许的源
String allowedOrigins = env.getProperty("cors.allowed.origins", "");
config.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));

config.applyPermitDefaultValues();
config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}

5.2 安全头信息增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean  
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(withDefaults())
.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(63072000)
)
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
)
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'")
)
);
return http.build();
}

6. 前后端联合调试方案

假设前端部署在 https://frontend.app,后端 API 接口在 https://your-domain.com

  • 后端全局配置(见第4节 Spring Security 安全配置方案)允许该前端域名。
  • 前端请求示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 带凭证的跨域请求  
fetch('https://your-domain.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
})
.then(response => {
console.log('Allowed Headers:',
response.headers.get('Access-Control-Allow-Headers'));
});

先发起 OPTIONS https://your-domain.com/data,带上 Origin: https://frontend.app

后端返回:

1
2
3
4
Access-Control-Allow-Origin: https://frontend.app  
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization

7. 常见安全隐患与对策

7.1 风险矩阵

错误配置 潜在风险 解决方案
Access-Control-Allow-Origin: * 敏感数据泄露 动态白名单机制
允许全部HTTP方法 未授权操作风险 最小权限原则
暴露敏感响应头 信息泄露风险 严格控制 exposedHeaders
缺失 Vary: Origin 头 缓存投毒攻击 自动添加 Vary 头

7.2 生产环境检查清单

  1. 禁用 allowedOrigins("*")
  2. 设置合理的 maxAge 值(建议 ≤ 1 小时)
  3. 严格限制 allowedHeaders 范围
  4. 启用 allowCredentials 时必须指定具体源
  5. 结合 CSRF 保护关键操作

8. 结语

CORS 配置的本质是在便利性与安全性之间寻找平衡点。通过 Spring Security 的精细化配置,我们可以:

  1. 建立清晰边界:精确控制可交互的客户端范围
  2. 实施最小授权:按需开放必要的 HTTP 方法和头部
  3. 实现动态防御:根据环境变化自动调整安全策略
  4. 构建纵深防御:与 CSRF、CSP 等安全机制形成合力
1
2
3
4
# 生产环境推荐配置示例  
cors.allowed.origins=https://web-client.com,https://mobile-client.com
cors.max-age=3600
cors.exposed-headers=X-Request-ID

通过本文内容,相信小伙伴们已经可以在 Spring Boot + Spring Security 项目中,按需灵活地设定跨域安全边界,既满足前后端分离的开发需求,又确保系统不被非授权域滥用。让您的应用将拥有更健壮的跨域防护能力。