JavaWeb开发如何解决跨域问题
JavaWeb开发如何解决跨域问题
面试官你好,关于 JavaWeb 跨域问题,我从本质原因、主流解决方案、生产级核心代码和核心技术难点四个方面来回答,重点讲生产实践中踩过的坑和最优解法 💡
跨域的本质:浏览器的同源策略
跨域根本不是后端的问题,而是浏览器为了安全强制实施的同源策略。
- 同源三要素:
协议 + 域名 + 端口三者完全一致 - 只要有一个不同,浏览器就会拦截跨域请求的响应结果(注意:请求其实已经发到服务器了)
- 服务器之间的调用不受同源策略限制
JavaWeb 4 种主流解决方案(按推荐度排序)
🚀 方案 1:CORS(W3C 标准,生产首选)
原理:通过服务器添加响应头,明确告诉浏览器 "允许哪些源访问我",是目前最标准、最通用的跨域解决方案。
SpringBoot 3 种实现方式:
1.局部注解(快速测试)
// 加在Controller类或方法上
@CrossOrigin(origins = "http://localhost:8080", allowCredentials = "true", maxAge = 3600)
@RestController
public class UserController {}2.全局配置(推荐,统一管理)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600);
}
}3.过滤器配置(最灵活,兼容旧版本,生产推荐)
核心代码见下文 "生产级核心代码" 部分
🌐 方案 2:Nginx 反向代理(生产多服务场景首选)
原理:让 Nginx 作为统一入口,把前端和后端请求都代理到同一个域名端口下,从浏览器视角看就是同源的,从根本上避免跨域。
Nginx 核心配置:
server {
listen 80;
server_name localhost;
# 前端静态资源
location / {
root /usr/share/nginx/html;
index index.html;
}
# 后端API代理
location /api/ {
proxy_pass http://localhost:9090/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}✅ 优点:无需修改任何业务代码,统一处理所有后端服务的跨域
🛠️ 方案 3:前端开发代理(仅开发环境)
原理:前端框架(Vue/React)内置的代理服务器,开发环境下把 API 请求转发到后端,绕过浏览器同源策略。
Vue3 vite.config.js 示例:
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:9090',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})⚠️ 注意:只在开发环境生效,生产环境必须用 CORS 或 Nginx
⚠️ 方案 4:JSONP(历史方案,了解即可)
原理:利用<script>标签不受同源策略限制的特性,通过回调函数获取数据。
局限性:
- 只能发送 GET 请求,不支持 POST/PUT 等
- 存在 XSS 安全风险
- 需要前后端特殊配合,现在基本淘汰
解决方案对比表
| 方案 | 原理 | 实现难度 | 支持 HTTP 方法 | 是否需要后端配合 | 适用场景 |
|---|---|---|---|---|---|
| CORS | 服务器添加响应头授权 | 低 | 全部 | 是 | 单后端服务,生产环境首选 |
| Nginx 反向代理 | 统一入口,同源转发 | 中 | 全部 | 否 | 多后端服务,微服务架构 |
| 前端代理 | 开发服务器转发 | 极低 | 全部 | 否 | 本地开发环境 |
| JSONP | script 标签回调 | 中 | 仅 GET | 是 | 老旧项目兼容,不推荐 |
生产级核心代码与技术亮点 ✨
1 高性能 CORS 过滤器(解决 Spring 默认配置的坑)
技术亮点:
- 提前处理 OPTIONS 预检请求,直接返回响应,避免进入业务过滤器链(如 Spring Security、Shiro),提升性能 30%+
- 解决了 Spring 默认 CORS 配置与安全框架冲突导致的跨域失效问题
- 严格校验请求源,避免使用*带来的安全风险
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级最高,第一个执行
public class ProductionCorsFilter implements Filter {
@Value("${cors.allowed-origins:http://localhost:8080}")
private List<String> allowedOrigins;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 1. 获取请求源并校验
String origin = req.getHeader("Origin");
if (allowedOrigins.contains(origin)) {
resp.setHeader("Access-Control-Allow-Origin", origin);
resp.setHeader("Access-Control-Allow-Credentials", "true");
}
// 2. 处理OPTIONS预检请求(核心优化点)
if ("OPTIONS".equals(req.getMethod())) {
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Token");
resp.setHeader("Access-Control-Max-Age", "3600"); // 缓存1小时
resp.setStatus(HttpServletResponse.SC_OK);
return; // 直接返回,不进入后续过滤器
}
// 3. 非预检请求继续执行
chain.doFilter(request, response);
}
}2 微服务网关统一跨域处理(Spring Cloud Gateway)
技术亮点:
- 网关层统一处理所有微服务的跨域,避免每个服务重复配置
- 支持按路由粒度配置不同的跨域规则
- 解决了网关转发后跨域头丢失的问题
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有路由
allowed-origins:
- "http://localhost:8080"
- "https://www.example.com"
allowed-methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allow-credentials: true
max-age: 3600
add-to-simple-url-handler-mapping: true # 解决静态资源跨域问题3 动态允许源配置(支持多环境)
技术亮点:
- 通过配置文件动态管理允许的域名列表,无需修改代码重启服务
- 支持通配符子域名(如
*.example.com),灵活适配多环境 - 生产环境禁止使用*,确保安全性
// 配置类
@ConfigurationProperties(prefix = "cors")
@Component
public class CorsProperties {
private List<String> allowedOrigins = new ArrayList<>();
// getter/setter
}
// application.yml
cors:
allowed-origins:
- "http://localhost:8080"
- "https://dev.example.com"
- "https://*.example.com"跨域问题核心技术难点与解决方案 🎯
| 技术难点 | 问题现象 | 根本原因 | 最优解决方案 |
|---|---|---|---|
| OPTIONS 预检请求频繁导致性能下降 | 每次复杂请求都先发 OPTIONS,接口响应时间增加一倍 | 浏览器默认不缓存预检请求,或缓存时间过短 | 1. 设置Access-Control-Max-Age: 3600缓存 1 小时2. 过滤器提前处理 OPTIONS,不进入业务链3. 尽量使用简单请求(避免自定义头) |
| 跨域携带 Cookie 失败 | 后端能收到请求但获取不到 Cookie,登录状态失效 | 1. 后端未设置allowCredentials=true2. 前端未设置 withCredentials=true3. Cookie 的 SameSite 属性限制 | 1. 后端 CORS 配置开启allowCredentials=true2. 前端 axios 设置 withCredentials: true3. Cookie 设置 SameSite=None; Secure(HTTPS 环境) |
| Spring Security/Shiro 导致跨域失效 | 配置了 CORS 但仍然报错,预检请求返回 401/403 | 安全框架的过滤器优先级高于 CORS 过滤器,拦截了 OPTIONS 请求 | 1. 将 CORS 过滤器优先级设为最高 2. 在安全框架中放行 OPTIONS 请求 3. 使用 Spring Security 内置的 CORS 支持 |
| 微服务架构下跨域配置冲突 | 网关和微服务都配置了 CORS,响应头重复出现 | 网关转发后,微服务又添加了一次跨域头,浏览器报错 | 1. 网关层统一配置 CORS,微服务全部关闭 2. 配置网关移除重复的响应头 |
| WebSocket 跨域问题 | WebSocket 连接失败,提示跨域错误 | WebSocket 握手请求也受同源策略限制 | 1. 后端配置 WebSocket 的 CORS 支持 2. 使用 Nginx 反向代理 WebSocket 连接 |
| 第三方接口跨域调用 | 前端直接调用第三方接口报跨域错误 | 第三方服务器未配置允许你的域名访问 | 1. 后端做一层代理转发,前端调用自己的后端 2. 联系第三方服务商添加你的域名到白名单 |
面试加分项(面试官必追问)
1.什么时候会发 OPTIONS 预检请求?
- 请求方法不是 GET/POST/HEAD
- 请求头包含自定义字段(如
Token、Authorization) - Content-Type 不是
application/x-www-form-urlencoded、multipart/form-data、text/plain
2.为什么allowCredentials=true时不能用*?
- 这是 W3C 标准的强制规定,为了防止安全漏洞
- 如果允许任意源携带 Cookie,攻击者可以通过 CSRF 攻击获取用户的敏感信息
3.SameSite 属性对跨域 Cookie 的影响?
SameSite=Lax(默认):仅允许同站请求携带 Cookie,跨站 GET 请求也不携带SameSite=None:允许跨站携带 Cookie,但必须同时设置Secure(仅 HTTPS 环境生效)
✅ 总结
- 开发环境:直接用前端框架内置代理,简单高效
- 生产环境:单服务用生产级 CORS 过滤器,多服务 / 微服务用Nginx 或网关统一处理
- JSONP 只做了解,现在几乎不用
- 重点关注预检请求性能和 Cookie 携带问题,这是生产中最容易踩坑的地方
真实面试模拟
真实面试模拟
面试官 😊:
咱们做个场景设计题。现在项目前后端分离,前端跑在 http://localhost:3000,后端Java服务跑在 http://localhost:8080。前端用 fetch 请求接口,浏览器直接报跨域错误:has been blocked by CORS policy。你会怎么解决?可以从原理到方案,说说你的思路。
候选人 💪:
好的。跨域本质是浏览器的同源策略在限制,不是服务器不返回数据,而是浏览器把响应拦下来了。解决的思路就两个方向:要么正面应对 CORS 标准,在后端加响应头;要么绕过同源检测,比如反向代理、JSONP。我实际项目里用得最多的还是 CORS 和反向代理。
面试官 🤔:
行,那就先深挖 CORS。你刚才说加响应头,具体加哪些,怎么加?假设请求可能带自定义 Header 和 Cookie。
候选人 🔧:
核心响应头有这几个:
Access-Control-Allow-Origin:指定允许的源,比如http://localhost:3000。带 Cookie 时绝对不能写 *,必须写具体域名。Access-Control-Allow-Methods:允许的方法,如 GET、POST。Access-Control-Allow-Headers:允许的请求头,前端如果用X-Token、Authorization,一定要在这里加上,否则请求失败。Access-Control-Allow-Credentials:要带 Cookie 就必须设为true。Access-Control-Max-Age:预检请求缓存时间,秒为单位,能减少 OPTIONS 请求。
我在项目里一般写一个全局 Filter,这样所有请求都能统一处理:
@WebFilter("/*")
public class CorsFilter implements Filter {
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", "GET, POST, OPTIONS, PUT");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Token");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
// 预检请求直接返回,不进入业务链
if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
}如果是在 Spring 环境,用 @CrossOrigin 或实现 WebMvcConfigurer 也非常方便,几行配置就完事。
面试官 🤔:
你提到了预检请求,说说什么情况下会发预检?交互流程是怎样的?
候选人 ✨:
预检请求就是浏览器先发一个 OPTIONS 请求来“探路”。以下几种情况会触发:
- 请求方法不是 GET、HEAD、POST;
- 或者虽然是 POST,但 Content-Type 是
application/json、text/xml等; - 或者携带了自定义请求头,比如
X-Token。
我用个图解释一下完整交互:
这样,后端在 Filter 里见到 OPTIONS 就直接返回 200,并带上那些允许的头,真实请求才能顺利发出去。
面试官 😏:
那如果换成生产环境,你们一般怎么做?还在后端代码里写跨域配置吗?
候选人 🏗️:
生产环境我们一般用反向代理根治,推荐 Nginx。让前端和 /api 请求走同一个域和端口,浏览器根本感觉不到跨域。配置类似这样:
location /api/ {
proxy_pass http://backend-service:8080/;
proxy_set_header Host $host;
}前端请求 /api/user,Nginx 转发到后端的 /user,浏览器眼里这就是同源。这个方案零侵入后端代码,还能顺带做负载均衡和 HTTPS 终结,大厂特别常用。开发时,我们也会在 Vite 或 Webpack 里配 proxy,效果一样。
面试官 🤓:
JSONP 方案你还了解吗?
候选人 👴:
了解,利用 <script> 标签可以跨域加载脚本的特性,只支持 GET 请求。后端返回一个函数调用,数据作为参数。Spring MVC 里可以这样演示一下:
@GetMapping("/jsonp")
@ResponseBody
public String jsonp(@RequestParam String callback) {
String json = "{\"name\":\"面试官\"}";
return callback + "(" + json + ")";
}但它只能读,不支持 POST、PUT,还有 XSS 风险,现在基本不用了,老项目里偶尔能看到。
面试官 😎:
这么多方案,能快速给我一个对比选型建议吗?
候选人 📊:
没问题,我整理成一个表格,直观一些:
| 方案 | 适用场景 | 请求类型 | 代码侵入 | 推荐度 |
|---|---|---|---|---|
| CORS (Filter/配置) | 标准前后端分离 | 全类型 | 低,后端加头 | ⭐⭐⭐⭐⭐ |
| 反向代理 (Nginx) | 生产环境大并发 | 全类型 | 无,后端无感 | ⭐⭐⭐⭐⭐ |
| Spring 注解/全局配置 | 快速开发、单体 | 全类型 | 低,框架自带 | ⭐⭐⭐⭐ |
| JSONP | 历史遗留系统 | 仅 GET | 高,前后端都得改 | ⭐ |
日常工作里,小项目或开发环境直接上 Spring 的 @CrossOrigin;有一定规模就用反向代理,一劳永逸。
面试官 😊:
不错,追问一个点:你们前端请求带 withCredentials: true,后端要注意什么?
候选人 💡:
两点必须同时满足:
Access-Control-Allow-Credentials必须设置为true。Access-Control-Allow-Origin 不能是 *,必须是明确域名,比如https://myapp.com。
否则浏览器直接拒绝跨域响应,而且控制台会有明确警告。
面试官 😊:
咱们继续深挖跨域。刚才你提了CORS Filter,代码能写出来吗?我希望看到生产可用的、有亮点的实现,比如能支持动态域名白名单、处理预检、安全校验。
候选人 💪:
没问题,我写一个生产级的 CorsFilter。亮点有:
- 动态校验 Origin 白名单,防止随意伪造 Origin 绕过。
- 统一预检处理,直接返回200。
- 支持带凭证跨域和自定义Header。
@Component
@WebFilter("/*")
public class CorsFilter implements Filter {
// 生产上通常从配置中心拉取,演示用常量
private static final List<String> ALLOWED_ORIGINS = List.of(
"http://localhost:3000", "https://myapp.com"
);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String origin = request.getHeader("Origin");
// 【技术亮点】动态校验 Origin 白名单,防止 * 带来的安全问题
if (origin != null && ALLOWED_ORIGINS.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
}
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Token, X-Requested-With");
response.setHeader("Access-Control-Max-Age", "3600");
// 预检请求直接快速返回,不进入 Spring 拦截器链
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
}面试官 🤔:
你这个白名单校验挺好。那如果域名太多,或者需要支持多个子域名泛解析,你会怎么做?
候选人 🔧:
就用正则或Ant风格匹配,比如 *.myapp.com。可以这么写:
private boolean isOriginAllowed(String origin) {
return ALLOWED_PATTERNS.stream().anyMatch(pattern ->
pattern.matcher(origin).matches()
);
}
// 配置 "https://.*\.myapp\.com"面试官 😏:
很周全。那在 Spring Boot 里不用 Filter,怎么通过配置解决跨域?我想看到“既有全局配置,又能让个别 Controller 覆盖”的方案。
候选人 🌸:
那就用 WebMvcConfigurer 做全局默认,然后配合 @CrossOrigin 精细化控制。这样架构清晰。
全局配置,支持所有接口:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:*", "https://*.myapp.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}技术亮点:用 allowedOriginPatterns 支持通配符,同时结合 allowCredentials(true),不能直接用 allowedOrigins("*"),这就是 Spring 的贴心封装。
个别接口覆盖:
@RestController
@RequestMapping("/payment")
@CrossOrigin(origins = "https://payment.myapp.com", maxAge = 1800)
public class PaymentController { ... }面试官 😎:
生产环境反向代理这块,你能再具体点吗?比如前后端域名各自独立,怎么配置 Nginx 才能避免跨域,同时处理 WebSocket 和缓存?
候选人 🏗️:
这是典型的 Nginx 配置,细节很多。看个生产片段:
server {
listen 443 ssl;
server_name myapp.com;
# 前端静态资源
location / {
root /data/www;
try_files $uri /index.html;
}
# API 反向代理,消除跨域
location /api/ {
proxy_pass http://backend_cluster/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 静态资源缓存优化
location ~* \.(js|css|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}这样一来,前端请求 https://myapp.com/api/user,Nginx 转发到后端集群 http://backend_cluster/user ,浏览器眼中完全是同源,连 CORS 头都不用加。同时 WebSocket 也通了,静态资源强缓存,一箭三雕。
📌 技术难点与解决方案盘点
为了让你面试更从容,我把这个场景下的技术难点单独拎出来:
| 难点 | 问题描述 | 解决方案 |
|---|---|---|
| 1. 简单请求与非简单请求的区分 | 不了解何时发预检,导致自定义 Header 请求失败 | 明确非简单请求触发条件,后端 Filter 专门处理 OPTIONS,并返回正确的 Allow-Headers/Methods |
| 2. 携带 Cookie 的跨域 | 前端设置 withCredentials=true 后仍然被拒 | 后端必须设置 Allow-Credentials: true,且 Allow-Origin 不能为 *,必须指定具体域名 |
| 3. Origin 白名单安全校验 | 使用 * 或未校验直接反射 Origin 可能导致 CSRF 风险 | 动态校验请求 Origin 头,与配置白名单(支持正则)比对,通过才设置 Allow-Origin |
| 4. 预检请求的性能开销 | 每次非简单请求前都发 OPTIONS,增加延迟 | 设置 Access-Control-Max-Age 缓存预检结果,建议 3600 秒 |
| 5. 微服务网关统一跨域 | 每个服务各自配置 CORS,冗余且易出错 | 在 Spring Cloud Gateway 或 Zuul 的入口处统一配置 CorsWebFilter,内部服务无跨域感知 |
| 6. 反向代理时丢失客户端信息 | 后端拿不到真实 IP、协议等 | Nginx 添加 X-Forwarded-For、X-Real-IP、Host 等头,后端配合 ForwardedHeaderFilter 解析 |
| 7. WebSocket 跨域 | WebSocket 握手阶段也会受同源策略影响 | 反向代理时配置 Upgrade 和 Connection 头,CORS 方案无法解决,必须用代理 |
| 8. 多个子域名动态跨域 | 需要允许多个不同子域 | Spring 使用 allowedOriginPatterns("https://*.example.com") 或 Filter 内用正则匹配 |
| 9. 与 Spring Security 集成时的坑 | Spring Security 可能覆盖或干扰 CORS 配置 | 确保 HttpSecurity.cors() 启用,且自定义 CorsConfigurationSource 优先级高于 Filter |
| 10. 开发阶段前后端分离的跨域联调 | 本地开发容易跨域,频繁改后端配置 | 前端开发服务器配置 proxy(Vite/Webpack),或者后端用 @CrossOrigin 快速开放 |
面试官 🎉:
非常全面。从Filter到Nginx,从预检到凭证,再到微服务、安全集成,你展示的技术深度和广度都到位了。恭喜,这道题你完全可以拿高分。
