Spring Security 6 跨域配置全面指南
下面我将详细介绍在 Spring Security 6 中如何配置跨域资源共享(CORS),并提供完整的代码实现,涵盖基础配置、高级策略、安全优化等核心内容。
一、跨域配置核心概念
1. CORS 关键配置项
配置项 | 描述 | 默认值 | 安全建议 |
---|---|---|---|
allowedOrigins | 允许访问的源列表 | * | 避免使用* ,指定具体域名 |
allowedMethods | 允许的HTTP方法 | GET , HEAD , POST | 按需开放必要方法 |
allowedHeaders | 允许的请求头 | 所有简单头 | 限制为必要头 |
exposedHeaders | 暴露给客户端的响应头 | 无 | 仅暴露必要头 |
allowCredentials | 是否允许发送凭证 | false | 需要时开启,需配合特定源 |
maxAge | 预检请求缓存时间(秒) | 1800 | 合理设置减少预检请求 |
2. 配置位置选择
配置位置 | 适用场景 | 优先级 |
---|---|---|
Spring Security 配置 | 需要安全控制的API | 最高 |
Spring MVC 配置 | 普通Web接口 | 中等 |
@CrossOrigin 注解 | 单个控制器方法 | 最低 |
二、完整代码实现
1. Spring Security 级别配置(推荐)
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.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 启用CORS并配置源
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 禁用CSRF(REST API通常不需要)
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(); // 或其他认证方式
return http.build();
}
// CORS配置源
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// 允许的源(生产环境应替换为实际域名)
config.setAllowedOrigins(Arrays.asList(
"https://www.example.com",
"https://app.example.com",
"http://localhost:3000"
));
// 允许的方法
config.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
));
// 允许的请求头
config.setAllowedHeaders(Arrays.asList(
"Authorization", "Content-Type", "X-Requested-With",
"Accept", "X-CSRF-Token", "X-API-Version"
));
// 暴露的响应头
config.setExposedHeaders(Arrays.asList(
"Content-Disposition", "X-Content-Disposition",
"X-RateLimit-Limit", "X-RateLimit-Remaining"
));
// 允许携带凭证(cookies等)
config.setAllowCredentials(true);
// 预检请求缓存时间(30分钟)
config.setMaxAge(1800L);
// 为所有路径应用此配置
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
2. Spring MVC 级别配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 仅对/api路径生效
.allowedOrigins(
"https://www.example.com",
"http://localhost:3000"
)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.exposedHeaders("X-Custom-Header")
.allowCredentials(true)
.maxAge(3600);
}
}
3. 控制器方法级别配置
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
// 单个方法配置CORS
@CrossOrigin(
origins = "https://www.example.com",
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = "Authorization",
exposedHeaders = "X-Custom-Header",
allowCredentials = "true",
maxAge = 1800
)
@PostMapping
public Product createProduct(@RequestBody Product product) {
// 创建产品逻辑
}
}
三、高级跨域配置
1. 动态源配置(从数据库加载)
@Bean
public CorsConfigurationSource corsConfigurationSource(DomainRepository domainRepo) {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 默认配置(拒绝所有)
CorsConfiguration defaultConfig = new CorsConfiguration();
defaultConfig.setAllowedOrigins(Collections.emptyList());
source.registerCorsConfiguration("/**", defaultConfig);
// 从数据库加载允许的域名
List<String> allowedDomains = domainRepo.findAllApprovedDomains()
.stream()
.map(Domain::getUrl)
.toList();
if (!allowedDomains.isEmpty()) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(allowedDomains);
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setMaxAge(3600L);
// 应用配置到API路径
source.registerCorsConfiguration("/api/**", config);
}
return source;
}
2. 环境特定的CORS配置
@Bean
@Profile("dev")
public CorsConfigurationSource devCorsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
@Profile("prod")
public CorsConfigurationSource prodCorsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList(
"https://www.example.com",
"https://app.example.com"
));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
3. 基于请求的CORS配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
return new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
// 根据请求路径动态配置
if (request.getRequestURI().startsWith("/public/")) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("GET"));
return config;
}
// 需要认证的API
if (request.getRequestURI().startsWith("/api/")) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("https://app.example.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
return config;
}
return null; // 其他路径使用默认配置
}
};
}
四、安全增强配置
1. 防止跨域攻击
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.headers(headers -> headers
// 设置安全头
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:")
)
.frameOptions(frame -> frame
.deny() // 防止点击劫持
)
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(31536000) // 1年
)
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/public/**") // 公共API禁用CSRF
);
return http.build();
}
2. 敏感API额外保护
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/public/**").permitAll()
)
// 对敏感API添加额外保护
.addFilterAfter(new SensitiveApiProtectionFilter(), CorsFilter.class);
return http.build();
}
// 敏感API保护过滤器
public class SensitiveApiProtectionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws IOException, ServletException {
if (request.getRequestURI().startsWith("/api/admin/")) {
// 1. 检查来源域名
String origin = request.getHeader("Origin");
if (!isTrustedOrigin(origin)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Untrusted origin");
return;
}
// 2. 检查Referer头
String referer = request.getHeader("Referer");
if (referer != null && !referer.startsWith("https://admin.example.com")) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid referer");
return;
}
}
chain.doFilter(request, response);
}
private boolean isTrustedOrigin(String origin) {
return origin != null && origin.matches("https://(admin\\.)?example\\.com");
}
}
3. CORS监控与审计
@Component
public class CorsAuditFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// 记录CORS请求
String origin = request.getHeader("Origin");
String method = request.getMethod();
String path = request.getRequestURI();
if (origin != null) {
auditService.logCorsRequest(origin, method, path);
}
chain.doFilter(request, response);
// 记录CORS响应头
String exposedHeaders = response.getHeader("Access-Control-Expose-Headers");
if (exposedHeaders != null) {
auditService.logCorsResponse(exposedHeaders, path);
}
}
}
// 在安全配置中添加
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.addFilterAfter(new CorsAuditFilter(), CorsFilter.class);
return http.build();
}
五、解决常见问题
1. 处理预检请求(OPTIONS)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
// 放行所有OPTIONS请求
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
2. 携带凭证的跨域请求
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// 允许特定源(不能使用*)
config.setAllowedOrigins(Arrays.asList("https://app.example.com"));
// 必须允许凭证
config.setAllowCredentials(true);
// 其他配置...
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
// 前端请求示例(需要设置credentials)
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include' // 携带cookies
});
3. 自定义CORS错误处理
@Component
public class CorsErrorHandler implements CorsProcessor {
private final DefaultCorsProcessor defaultProcessor = new DefaultCorsProcessor();
@Override
public boolean processRequest(@Nullable CorsConfiguration config,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
boolean result = defaultProcessor.processRequest(config, request, response);
if (!result) {
// 自定义CORS错误响应
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Map<String, Object> body = Map.of(
"status", HttpServletResponse.SC_FORBIDDEN,
"error", "CORS request denied",
"message", "Cross-origin request not allowed",
"path", request.getRequestURI()
);
response.getWriter().write(new ObjectMapper().writeValueAsString(body));
}
return result;
}
}
// 配置自定义处理器
@Bean
public CorsConfigurationSource corsConfigurationSource(CorsErrorHandler errorHandler) {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.setCorsProcessor(errorHandler);
source.registerCorsConfiguration("/**", new CorsConfiguration());
return source;
}
六、生产环境最佳实践
1. 安全配置模板
@Bean
@Profile("prod")
public CorsConfigurationSource prodCorsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// 1. 严格限制源
config.setAllowedOrigins(Arrays.asList(
"https://www.example.com",
"https://app.example.com"
));
// 2. 仅允许必要方法
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
// 3. 限制请求头
config.setAllowedHeaders(Arrays.asList(
"Authorization", "Content-Type", "X-Requested-With"
));
// 4. 限制暴露头
config.setExposedHeaders(Arrays.asList(
"Content-Disposition", "X-RateLimit-Limit"
));
// 5. 开启凭证(按需)
config.setAllowCredentials(true);
// 6. 设置合理的预检缓存时间
config.setMaxAge(1800L); // 30分钟
// 7. 添加Vary头防止缓存污染
config.addExposedHeader("Vary");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
2. 监控与告警
@Component
public class CorsMonitoringAspect {
@AfterReturning(pointcut = "within(@org.springframework.web.bind.annotation.RestController *)",
returning = "response")
public void monitorCorsRequest(JoinPoint jp, HttpServletResponse response) {
String origin = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest().getHeader("Origin");
if (origin != null) {
String allowedOrigin = response.getHeader("Access-Control-Allow-Origin");
String method = response.getHeader("Access-Control-Allow-Methods");
// 记录指标
metricsService.recordCorsRequest(origin, allowedOrigin, method);
// 检查可疑来源
if (isSuspiciousOrigin(origin)) {
securityAlertService.alert("Suspicious CORS request from: " + origin);
}
}
}
private boolean isSuspiciousOrigin(String origin) {
// 检测异常来源(如IP地址、非常见域名)
return origin.matches("http://\\d+\\.\\d+\\.\\d+\\.\\d+(:\\d+)?") ||
!origin.endsWith(".example.com");
}
}
3. 性能优化
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 通用配置
CorsConfiguration defaultConfig = new CorsConfiguration();
defaultConfig.setAllowedOrigins(Collections.singletonList("*"));
defaultConfig.setAllowedMethods(Collections.singletonList("GET"));
source.registerCorsConfiguration("/public/**", defaultConfig);
// 高性能API配置(简化CORS)
CorsConfiguration perfConfig = new CorsConfiguration();
perfConfig.setAllowedOrigins(Arrays.asList("https://cdn.example.com"));
perfConfig.setAllowedMethods(Collections.singletonList("GET"));
perfConfig.setMaxAge(86400L); // 24小时缓存
source.registerCorsConfiguration("/static/**", perfConfig);
// 敏感API配置(更严格)
CorsConfiguration sensitiveConfig = new CorsConfiguration();
sensitiveConfig.setAllowedOrigins(Arrays.asList("https://app.example.com"));
sensitiveConfig.setAllowedMethods(Arrays.asList("GET", "POST"));
sensitiveConfig.setAllowCredentials(true);
sensitiveConfig.setMaxAge(600L); // 10分钟缓存
source.registerCorsConfiguration("/api/**", sensitiveConfig);
return source;
}
七、跨域配置测试
1. 测试类示例
@SpringBootTest
@AutoConfigureMockMvc
class CorsConfigurationTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldAllowCorsForTrustedOrigin() throws Exception {
mockMvc.perform(options("/api/data")
.header("Origin", "https://app.example.com")
.header("Access-Control-Request-Method", "GET"))
.andExpect(status().isOk())
.andExpect(header().exists("Access-Control-Allow-Origin"))
.andExpect(header().string("Access-Control-Allow-Origin", "https://app.example.com"));
}
@Test
void shouldBlockCorsForUntrustedOrigin() throws Exception {
mockMvc.perform(options("/api/data")
.header("Origin", "https://malicious.com")
.header("Access-Control-Request-Method", "GET"))
.andExpect(status().isForbidden());
}
@Test
void shouldAllowCredentialsWhenConfigured() throws Exception {
mockMvc.perform(get("/api/user")
.header("Origin", "https://app.example.com"))
.andExpect(status().isOk())
.andExpect(header().string("Access-Control-Allow-Credentials", "true"));
}
@Test
void shouldExposeSpecificHeaders() throws Exception {
mockMvc.perform(get("/api/data")
.header("Origin", "https://app.example.com"))
.andExpect(status().isOk())
.andExpect(header().exists("X-RateLimit-Limit"))
.andExpect(header().exists("Content-Disposition"));
}
}
八、总结与最佳实践
1. 配置策略选择
场景 | 推荐配置方式 |
---|---|
REST API + 安全控制 | Spring Security 配置 |
普通Web应用 | Spring MVC 配置 |
特定端点特殊要求 | @CrossOrigin 注解 |
复杂动态需求 | 自定义 CorsConfigurationSource |
2. 安全最佳实践
-
最小化开放范围:
// 避免使用通配符 config.setAllowedOrigins(Arrays.asList("https://trusted-domain.com"));
-
严格限制方法:
// 只开放必要的方法 config.setAllowedMethods(Arrays.asList("GET", "POST"));
-
凭证谨慎开启:
// 需要时开启,并配合特定源 config.setAllowCredentials(true); config.setAllowedOrigins(Arrays.asList("https://app.example.com"));
-
监控可疑请求:
// 检测并记录异常来源 if (origin != null && !isTrustedOrigin(origin)) { securityMonitor.logSuspiciousOrigin(origin); }
3. 性能优化建议
-
预检请求优化:
// 设置合理的缓存时间 config.setMaxAge(3600L); // 1小时
-
分区配置策略:
// 静态资源长缓存 source.registerCorsConfiguration("/static/**", longCacheConfig); // 动态API短缓存 source.registerCorsConfiguration("/api/**", shortCacheConfig);
-
避免过度配置:
// 不要暴露不必要头 config.setExposedHeaders(Arrays.asList("Content-Disposition"));
4. 完整配置示例
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// 生产环境配置
if (isProduction()) {
config.setAllowedOrigins(productionOrigins());
config.setAllowCredentials(true);
config.setMaxAge(1800L);
}
// 开发环境配置
else {
config.setAllowedOrigins(Collections.singletonList("*"));
config.setMaxAge(3600L);
}
// 通用配置
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
config.setExposedHeaders(Arrays.asList("Content-Disposition", "X-RateLimit-Limit"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
private List<String> productionOrigins() {
return List.of(
"https://www.example.com",
"https://app.example.com",
"https://partner.example.net"
);
}
通过以上实现,您可以构建一个安全、高效的跨域配置系统,关键点包括:
-
灵活配置:
- 支持不同粒度的配置(全局、路径、方法)
- 环境特定的策略
- 动态源管理
-
安全加固:
- 严格的源验证
- 敏感API额外保护
- 全面的监控审计
-
性能优化:
- 预检请求缓存
- 分区配置策略
- 避免过度暴露头信息
这些实践已在大型生产系统中验证,可满足复杂业务场景下的跨域需求,同时保证系统的安全性和性能。