
1. 跨域问题与CORS基础概念第一次在Spring项目中遇到跨域问题时我盯着浏览器控制台那个鲜红的Access-Control-Allow-Origin错误提示足足愣了五分钟。作为后端开发者我们常常只关注服务端逻辑却忽略了现代Web应用最基本的安全约束——同源策略。同源策略要求协议、域名和端口三者完全相同才算同源。举个例子https://example.com/app和https://api.example.com/data不同源子域名不同http://localhost:8080和https://localhost:8080不同源协议不同http://localhost:8080和http://localhost:3000不同源端口不同CORS跨源资源共享机制就是在这种安全约束下开的一道后门。它通过HTTP头部来协商哪些跨域请求是被允许的。一个典型的CORS请求会包含这些关键头部Origin: https://frontend.com Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type而服务端的响应头部则形如Access-Control-Allow-Origin: https://frontend.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: Content-Type Access-Control-Max-Age: 3600在Spring生态中我们主要有三种方式实现CORS方法级注解CrossOriginWebMvcConfigurer全局配置自定义Filter方案前两种方案简单但不够灵活当我们需要精细控制跨域策略时自定义Filter才是王道。2. 自定义CORS Filter设计原理为什么需要自定义Filter去年我们项目就遇到一个典型案例需要根据不同的客户端动态返回不同的Access-Control-Allow-Origin值。Spring默认的CORS配置无法满足这种动态需求这时自定义Filter的优势就显现出来了。一个健壮的CORS Filter应该处理以下核心逻辑请求来源验证检查Origin头是否在允许列表中预检请求处理对OPTIONS方法做出正确响应头部注入为响应添加必要的CORS头部缓存优化合理设置Access-Control-Max-Age安全加固防范CORS滥用攻击下面是Filter的核心处理流程图----------------- | 收到HTTP请求 | ---------------- | --------v-------- | 是预检(OPTIONS)请求? | ---------------- | ---------------------- | | -----v----- -----v----- |处理预检请求 | |处理普通请求 | |设置CORS头部 | |设置CORS头部 | |返回204状态 | |继续过滤器链 | ----------- -----------在Spring中实现时我们要特别注意Filter的执行顺序。CORS Filter应该位于安全过滤器如Spring Security的过滤器链之后但要在业务逻辑之前执行。错误的位置可能导致CORS头部被后续过滤器覆盖或安全校验失败。3. Spring CORS Filter完整实现下面是我在生产环境中验证过的CORS Filter实现方案。这个版本支持动态Origin检测、可配置的HTTP方法和头部白名单以及灵活的凭证控制。3.1 基础Filter类结构Component Order(Ordered.HIGHEST_PRECEDENCE) public class CustomCorsFilter implements Filter { private final ListString allowedOrigins Arrays.asList( https://production.com, https://staging.com, http://localhost:3000 ); private final ListString allowedMethods Arrays.asList( GET, POST, PUT, DELETE, OPTIONS ); private final ListString allowedHeaders Arrays.asList( Authorization, Content-Type, X-Requested-With ); Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 实现细节在下文展开 } // 其他方法省略... }关键点说明Order注解确保Filter在合适的位置执行使用Component让Spring管理生命周期白名单配置采用硬编码方式实际项目应该从配置中心读取3.2 预检请求处理逻辑HttpServletResponse response (HttpServletResponse) res; HttpServletRequest request (HttpServletRequest) req; String origin request.getHeader(Origin); // 检查Origin是否合法 if (origin ! null allowedOrigins.contains(origin)) { response.setHeader(Access-Control-Allow-Origin, origin); response.setHeader(Vary, Origin); // 处理预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { response.setHeader(Access-Control-Allow-Methods, String.join(, , allowedMethods)); response.setHeader(Access-Control-Allow-Headers, String.join(, , allowedHeaders)); response.setHeader(Access-Control-Max-Age, 3600); response.setHeader(Access-Control-Allow-Credentials, true); response.setStatus(HttpServletResponse.SC_NO_CONTENT); return; } } chain.doFilter(req, res);这里有几个值得注意的细节动态设置Access-Control-Allow-Origin而不是使用通配符*添加Vary: Origin头部防止CDN缓存问题预检请求直接返回204状态码不继续过滤器链显式设置Allow-Credentials为true以支持带凭证的请求3.3 响应头增强处理对于非预检请求我们还需要确保响应包含必要的CORS头部if (origin ! null allowedOrigins.contains(origin)) { response.setHeader(Access-Control-Expose-Headers, X-Custom-Header); response.setHeader(Access-Control-Allow-Credentials, true); // 防止缓存普通请求的CORS响应 response.setHeader(Cache-Control, no-store); } chain.doFilter(req, res);特别注意Expose-Headers用于暴露自定义头部给前端禁用缓存确保每次请求都携带最新CORS策略凭证模式需要特别注意安全性下文会专门讨论4. 高级配置与安全考量4.1 动态Origin管理硬编码的Origin列表显然不够灵活。我们可以通过以下方式改进Value(${cors.allowed-origins}) private String[] allowedOrigins; // 或者从数据库读取 Autowired private CorsConfigRepository configRepo; private boolean isOriginAllowed(String origin) { // 支持通配符子域名如 *.example.com if (origin null) return false; return Arrays.stream(allowedOrigins) .anyMatch(allowed - allowed.equals(origin) || (allowed.startsWith(*.) origin.endsWith(allowed.substring(1)))); }警告使用通配符时要特别小心避免过于宽松的设置如*或*.com4.2 安全加固措施CSRF防护启用Spring Security的CSRF保护时需要排除CORS预检请求http.csrf().disable(); // 简单方案 // 或 http.csrf().ignoringAntMatchers(HttpMethod.OPTIONS, /**); // 精确控制头部注入防护确保只添加必要的头部避免信息泄露// 禁用不必要的头部 response.setHeader(X-Content-Type-Options, nosniff); response.setHeader(X-Frame-Options, DENY); response.setHeader(X-XSS-Protection, 1; modeblock);速率限制对预检请求实施限流防止DDoS攻击Bean public FilterRegistrationBeanCustomCorsFilter corsFilterRegistration() { FilterRegistrationBeanCustomCorsFilter registration new FilterRegistrationBean(); registration.setFilter(new CustomCorsFilter()); registration.addUrlPatterns(/*); registration.setName(customCorsFilter); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); return registration; }5. 生产环境问题排查指南5.1 常见问题速查表问题现象可能原因解决方案预检请求返回403CSRF保护拦截配置csrf().ignoringAntMatchers(HttpMethod.OPTIONS, /**)响应缺少CORS头部Filter顺序不对调整Order值为最高优先级带凭证的请求失败未设置Allow-Credentials添加response.setHeader(Access-Control-Allow-Credentials, true)多个Origin被缓存缺少Vary头部添加response.setHeader(Vary, Origin)自定义头部不可见未暴露头部设置Access-Control-Expose-Headers5.2 浏览器调试技巧查看完整CORS流程在Chrome开发者工具的Network标签中勾选Preserve log过滤OPTIONS请求检查请求头中的Origin和响应头中的CORS相关头部强制刷新CORS缓存// 在控制台执行 fetch(https://api.example.com/data, { method: GET, cache: reload, credentials: include });测试不同Origin# 使用curl模拟跨域请求 curl -H Origin: http://test.com \ -H Access-Control-Request-Method: GET \ -H Access-Control-Request-Headers: Content-Type \ -X OPTIONS http://localhost:8080/api6. 性能优化实践6.1 预检请求缓存通过合理设置Access-Control-Max-Age可以减少预检请求次数// 生产环境建议值秒 response.setHeader(Access-Control-Max-Age, 86400); // 24小时但要注意浏览器实际缓存时间可能小于设置值修改CORS策略后需要等待缓存过期或强制刷新6.2 条件性CORS处理对于纯API项目可以只对特定路径启用CORSif (request.getRequestURI().startsWith(/api/)) { // 应用CORS逻辑 } else { chain.doFilter(req, res); return; }6.3 响应压缩CORS头部会增加响应大小启用压缩可以减轻影响# application.properties server.compression.enabledtrue server.compression.mime-typestext/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json server.compression.min-response-size10247. 替代方案比较7.1 Spring Security CORS如果你已经使用Spring Security可以考虑其内置的CORS支持EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.cors().configurationSource(corsConfigurationSource()); // 其他配置... } Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); configuration.setAllowedOrigins(allowedOrigins); configuration.setAllowedMethods(allowedMethods); configuration.setAllowedHeaders(allowedHeaders); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, configuration); return source; } }优势与安全配置集中管理支持方法级细粒度控制劣势不够灵活难以实现动态Origin检测配置复杂度高7.2 网关层CORS在微服务架构中更推荐在API网关层统一处理CORS# Spring Cloud Gateway配置示例 spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowed-origins: https://frontend.com allowed-methods: * allowed-headers: * allow-credentials: true max-age: 3600网关方案的优点统一入口一处配置多处生效减少下游服务性能开销便于集中管理安全策略8. 版本兼容性注意事项不同Spring版本对CORS的支持有所差异Spring Boot版本特点2.4.x支持allowedOriginPatterns替代allowedOrigins2.2.x引入CorsWebFilter响应式支持1.5.x需要手动注册FilterRegistrationBean对于传统Spring MVC应用建议的最低版本是2.1.x它提供了最稳定的CORS支持。如果你使用的是更早版本考虑升级或使用第三方库如com.thetransactioncompany:cors-filter。在Spring WebFlux响应式应用中需要使用CorsWebFilterBean public CorsWebFilter corsWebFilter() { CorsConfiguration config new CorsConfiguration(); // 配置同上... UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsWebFilter(source); }9. 测试策略建议完善的CORS测试应该包括单元测试验证Filter逻辑Test public void testPreflightRequest() throws Exception { MockHttpServletRequest request new MockHttpServletRequest(); request.setMethod(OPTIONS); request.addHeader(Origin, https://allowed.com); MockHttpServletResponse response new MockHttpServletResponse(); filter.doFilter(request, response, (req, res) - {}); assertEquals(204, response.getStatus()); assertEquals(https://allowed.com, response.getHeader(Access-Control-Allow-Origin)); }集成测试验证完整请求流程Test public void testActualRequestWithCors() throws Exception { mockMvc.perform(get(/api/data) .header(Origin, https://allowed.com)) .andExpect(status().isOk()) .andExpect(header().exists(Access-Control-Allow-Origin)); }安全测试验证不合法Origin被拒绝Test public void testInvalidOriginRejected() throws Exception { mockMvc.perform(get(/api/data) .header(Origin, https://hacker.com)) .andExpect(header().doesNotExist(Access-Control-Allow-Origin)); }性能测试评估预检请求对系统的影响10. 实际项目经验分享在电商平台项目中我们遇到了几个典型的CORS相关挑战多环境配置开发环境允许所有本地端口测试环境允许测试团队域名生产环境严格限制为已知域名解决方案是通过Spring Profile动态加载配置Configuration public class CorsConfig { Bean Profile(dev) public FilterRegistrationBeanCorsFilter devCorsFilter() { // 宽松配置 } Bean Profile(prod) public FilterRegistrationBeanCorsFilter prodCorsFilter() { // 严格配置 } }第三方集成 当需要与第三方合作伙伴系统集成时我们实现了动态Origin注册接口RestController RequestMapping(/api/cors-config) public class CorsConfigController { PostMapping public ResponseEntity? addAllowedOrigin(RequestBody OriginRequest request) { // 验证请求合法性 // 将新Origin添加到数据库 // 触发配置热更新 return ResponseEntity.ok().build(); } }移动端特殊处理 某些旧版移动APP无法正确处理CORS我们为此添加了特殊路径前缀/legacy-api/这些接口禁用CORS加强CSRF防护增加请求签名验证这个方案帮助我们在保证安全性的同时兼容老旧客户端。