JavaWeb开发如何解决跨域问题

0 阅读9分钟

JavaWeb开发如何解决跨域问题

跨域的本质:浏览器的同源策略

跨域根本不是后端的问题,而是浏览器为了安全强制实施的同源策略

  • 同源三要素:协议 + 域名 + 端口 三者完全一致
  • 只要有一个不同,浏览器就会拦截跨域请求的响应结果(注意:请求其实已经发到服务器了)
  • 服务器之间的调用不受同源策略限制

image.png

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.过滤器配置(最灵活,兼容旧版本,生产推荐)

核心代码见下文 "生产级核心代码" 部分


整理了面试真题、每日技术知识点、系统学习路线,都汇总在个人网站 [www.javadashen.com] 有需要的同学自取。

公众号「Rain的Java大神之路」每天拆一个知识点,陪你悄悄变强,有空来坐坐。


🌐 方案 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 反向代理统一入口,同源转发全部多后端服务,微服务架构
前端代理开发服务器转发极低全部本地开发环境
JSONPscript 标签回调仅 GET老旧项目兼容,不推荐

生产级核心代码与技术亮点 ✨

1 高性能 CORS 过滤器(解决 Spring 默认配置的坑)

技术亮点

  1. 提前处理 OPTIONS 预检请求,直接返回响应,避免进入业务过滤器链(如 Spring Security、Shiro),提升性能 30%+
  2. 解决了 Spring 默认 CORS 配置与安全框架冲突导致的跨域失效问题
  3. 严格校验请求源,避免使用*带来的安全风险
@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)

技术亮点

  1. 网关层统一处理所有微服务的跨域,避免每个服务重复配置
  2. 支持按路由粒度配置不同的跨域规则
  3. 解决了网关转发后跨域头丢失的问题
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 动态允许源配置(支持多环境)

技术亮点

  1. 通过配置文件动态管理允许的域名列表,无需修改代码重启服务
  2. 支持通配符子域名(如*.example.com),灵活适配多环境
  3. 生产环境禁止使用*,确保安全性
// 配置类
@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
  • 请求头包含自定义字段(如TokenAuthorization
  • Content-Type 不是application/x-www-form-urlencodedmultipart/form-datatext/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-TokenAuthorization,一定要在这里加上,否则请求失败。
  • 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/jsontext/xml 等;
  • 或者携带了自定义请求头,比如 X-Token

我用个图解释一下完整交互:

image.png

这样,后端在 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,后端要注意什么?

候选人 💡:

两点必须同时满足:

  1. Access-Control-Allow-Credentials 必须设置为 true
  2. 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-ForX-Real-IPHost 等头,后端配合 ForwardedHeaderFilter 解析
7. WebSocket 跨域WebSocket 握手阶段也会受同源策略影响反向代理时配置 UpgradeConnection 头,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,从预检到凭证,再到微服务、安全集成,你展示的技术深度和广度都到位了。恭喜,这道题你完全可以拿高分。

别走,交个朋友

我是 Rain ,一个喜欢把复杂技术讲透、让代码落地的实践者。

如果你厌倦了四处收集碎片化的八股文,想看看一线项目里真实的架构决策和踩坑记录,欢迎来我的公众号「Rain 的 Java 大神之路」坐坐。 在这里,我会把每一次技术复盘、每一个项目的设计源码,毫无保留地分享给你。

🔥 技术这条路很酷,我们结伴同行。
🚀Keep Coding, Keep Loving —— 与所有 Java 同路人共勉。

image.png