
1. 跨域问题的本质与产生场景第一次在Java项目中遇到跨域问题时我盯着浏览器控制台的红色报错信息足足愣了三分钟。控制台里赫然显示着Access to XMLHttpRequest at http://api.example.com/data from origin http://localhost:8080 has been blocked by CORS policy——这就是典型的跨域错误。作为Java开发者理解跨域机制是前后端分离架构下的必修课。跨域Cross-Origin Resource Sharing本质是浏览器实施的安全策略。当你的前端页面如运行在localhost:8080尝试通过AJAX请求访问另一个源如api.example.com的资源时浏览器会检查响应头中的CORS相关字段。如果服务端没有明确声明允许该源访问浏览器就会拦截这次请求。注意这是浏览器的行为不是服务端拒绝响应——实际上请求可能已经到达服务端并完成了处理。常见的跨域场景包括开发环境前端运行在localhost:3000后端API在localhost:8080生产环境前端部署在www.domain.comAPI服务在api.domain.com第三方集成需要调用外部API如微信支付、地图服务等关键理解跨域限制是浏览器对JavaScript的安全限制使用Postman等工具直接请求API不会触发CORS检查。这也是为什么开发者经常困惑为什么Postman能请求成功而浏览器不行。2. Java服务端的CORS解决方案2.1 最简方案手动设置响应头对于简单的GET请求可以在Java控制器中直接添加响应头。以Spring Boot为例RestController public class ApiController { GetMapping(/data) public ResponseEntityString getData() { HttpHeaders headers new HttpHeaders(); headers.add(Access-Control-Allow-Origin, *); return new ResponseEntity(Hello CORS, headers, HttpStatus.OK); } }这种方式的缺点是需要每个接口单独处理无法处理非简单请求如Content-Type为application/json的POST请求星号(*)通配符不允许携带凭证如cookies2.2 过滤器统一处理更优雅的方式是使用Filter统一处理public class CorsFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) res; response.setHeader(Access-Control-Allow-Origin, http://localhost:3000); response.setHeader(Access-Control-Allow-Methods, POST, GET, OPTIONS, DELETE); response.setHeader(Access-Control-Max-Age, 3600); response.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization); response.setHeader(Access-Control-Allow-Credentials, true); chain.doFilter(req, res); } }然后在配置类中注册Configuration public class AppConfig { Bean public FilterRegistrationBeanCorsFilter corsFilter() { FilterRegistrationBeanCorsFilter registration new FilterRegistrationBean(); registration.setFilter(new CorsFilter()); registration.addUrlPatterns(/*); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); return registration; } }2.3 Spring Security集成方案如果项目使用了Spring Security需要在安全配置中明确允许OPTIONS请求EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS).permitAll() // 其他安全配置... } Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList(http://localhost:3000)); configuration.setAllowedMethods(Arrays.asList(GET,POST,PUT,DELETE)); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, configuration); return source; } }3. 预检请求Preflight深度解析当请求满足以下任一条件时浏览器会先发送OPTIONS预检请求使用了非简单方法PUT、DELETE等设置了非简单头部如AuthorizationContent-Type不是application/x-www-form-urlencoded、multipart/form-data或text/plain预检请求的完整交互过程浏览器发送OPTIONS请求携带Origin: 当前页面源Access-Control-Request-Method: 实际请求的方法Access-Control-Request-Headers: 自定义头部列表服务端响应必须包含Access-Control-Allow-Origin: 允许的源Access-Control-Allow-Methods: 允许的方法Access-Control-Allow-Headers: 允许的头部Access-Control-Max-Age: 预检结果缓存时间秒浏览器确认通过后才发送实际请求在Java中处理预检请求的常见坑点忘记配置Spring Security放行OPTIONS方法Access-Control-Allow-Headers未包含所有需要的请求头没有正确处理带凭证的请求需要设置Allow-Credentials4. 生产环境最佳实践4.1 动态允许源配置硬编码允许的源在生产环境不灵活建议改为动态配置Value(${cors.allowed-origins}) private String[] allowedOrigins; Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(allowedOrigins) .allowedMethods(*) .allowCredentials(true) .maxAge(3600); } }; }然后在application.properties中配置cors.allowed-originshttps://www.yourdomain.com,https://staging.yourdomain.com4.2 网关层统一处理在微服务架构中更推荐在API网关层统一处理CORS而不是每个服务单独配置。以Spring Cloud Gateway为例spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowedOrigins: https://www.yourdomain.com allowedMethods: * allowedHeaders: * allowCredentials: true maxAge: 36004.3 安全加固建议不要使用Access-Control-Allow-Origin: *与Allow-Credentials: true组合严格限制允许的方法如只允许GET、POST对Origin头进行验证防止伪造敏感接口建议结合CSRF保护5. 常见问题排查指南5.1 为什么设置了CORS仍然报错检查清单响应头是否确实出现在HTTP响应中有些框架会覆盖你的设置预检请求是否被正确处理特别是OPTIONS方法前端请求是否设置了withCredentials但服务端未设置Allow-Credentials是否有多层代理如Nginx覆盖了CORS头5.2 如何调试预检请求使用Chrome开发者工具打开Network面板勾选Preserve log查找类型为OPTIONS的请求检查请求头和响应头是否匹配5.3 特殊场景处理文件上传跨域// 需要显式声明允许的Headers configuration.addAllowedHeader(Content-Type); configuration.addAllowedHeader(X-Requested-With);WebSocket跨域// 在WebSocket配置中处理 registry.addHandler(webSocketHandler, /ws) .setAllowedOrigins(http://allowed.domain.com);6. 进阶CORS与缓存问题当服务端正确配置CORS后浏览器会缓存预检响应根据Max-Age。这可能导致修改CORS配置后不立即生效。解决方法开发阶段设置较短的Max-Age如60秒强制刷新缓存CtrlF5使用匿名模式测试在URL中添加随机参数绕过缓存对于CDN场景需要确保CDN不会缓存OPTIONS响应通常需要配置Cache-Control: no-store, max-age07. 替代方案JSONP与代理虽然CORS是现代推荐方案但了解替代方案也有价值JSONP仅限GET请求// 前端 function handleResponse(data) { console.log(data); } const script document.createElement(script); script.src http://api.example.com/data?callbackhandleResponse; document.body.appendChild(script); // Java端 GetMapping(/data) public void getData(RequestParam String callback, HttpServletResponse response) { String jsonData {\key\:\value\}; String jsonp callback ( jsonData ); response.getWriter().write(jsonp); }服务端代理方案RestController RequestMapping(/proxy) public class ProxyController { GetMapping(/external-api) public ResponseEntityString proxyExternalApi() { RestTemplate restTemplate new RestTemplate(); String result restTemplate.getForObject(http://external.api.com/data, String.class); return ResponseEntity.ok(result); } }实际经验我曾在一个老项目中遇到必须支持IE8的情况最终采用Nginx反向代理方案解决跨域问题。现代项目除非有特殊兼容性要求否则都应优先使用标准CORS方案。