什么是跨域访问问题,怎么解决?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 浏览器安全机制理解:面试官想了解你是否理解浏览器 "同源策略" 的设计初衷,以及为什么需要这个安全限制。

  2. 方案广度与深度:考察你是否掌握多种跨域解决方案,能否根据实际场景选择合适的方案,而不是只会 "配置 CORS" 这一种。

  3. 生产实践经验:是否了解跨域场景下的 Cookie 携带、预检请求优化、安全风险等实际问题。

核心答案

跨域 是指浏览器出于安全考虑(同源策略),阻止网页向不同源(协议 + 域名 + 端口)发起 AJAX 请求。

常见解决方案有 5 种

方案原理优点缺点适用场景
CORS服务端设置响应头标准方案,功能完整需要服务端配合现代项目首选
代理服务器同源代理转发无需修改后端多一层转发开发环境
JSONP利用 <script> 标签兼容性好只支持 GET,不安全老项目兼容
postMessage跨窗口通信安全可控仅限窗口间iframe 嵌套
WebSocket不受同源限制全双工通信需要建立长连接实时通信场景

一句话总结:生产环境优先使用 CORS,开发环境可用 代理服务器,老旧系统兼容可用 JSONP

深度解析

一、什么是跨域?为什么会有跨域问题?

1. 同源策略

同源策略的核心要点:

  • 什么是同源:URL 的协议(Protocol)、域名(Domain)、端口(Port)三者完全相同才算同源。

  • 限制范围:同源策略主要限制以下操作:

    • AJAX 请求无法发送(主要限制)
    • Cookie、LocalStorage、IndexedDB 无法读取
    • DOM 无法操作(iframe 跨域)
  • 设计初衷:防止恶意网站读取其他网站的数据,保护用户隐私和安全。比如防止恶意网站通过 AJAX 读取你的银行账户信息。

2. 跨域错误示例

跨域错误的几个关键点:

  • 请求是发出去了的:跨域并不是请求发不出去,而是浏览器拦截了响应。

  • 后端不知道跨域:后端正常处理请求并返回数据,根本不知道发生了跨域。

  • 浏览器报错:浏览器检查响应头,如果没有正确的 CORS 头,就拦截响应并报错。

二、方案一:CORS(跨域资源共享)

CORS(Cross-Origin Resource Sharing)是 W3C 标准,也是目前最主流的跨域解决方案。

1. 简单请求 vs 预检请求

CORS 请求分为两种类型:

  • 简单请求:满足特定条件的请求,浏览器直接发送,在响应头中检查 CORS 头即可。

  • 预检请求(Preflight):不满足简单请求条件的请求,浏览器会先发送一个 OPTIONS 请求询问服务器是否允许,然后再发送实际请求。

2. 简单请求流程

简单请求的处理流程:

  • 请求阶段:浏览器在请求头中添加 Origin 字段,表示请求来源。

  • 响应阶段:服务器在响应头中添加 Access-Control-Allow-Origin,表示允许哪些源访问。

  • 验证阶段:浏览器检查响应头,如果 Origin 在允许列表中,则允许读取响应。

3. 预检请求流程

预检请求的处理流程:

  • 第一步(预检):浏览器先发送 OPTIONS 请求,询问服务器是否允许该跨域请求(包括方法、头部等)。

  • 第二步(实际请求):如果预检通过,浏览器才发送实际的请求。

  • 缓存优化Access-Control-Max-Age 可以设置预检结果的缓存时间,避免每次都发送预检请求。

4. 后端配置示例

Spring Boot 配置 CORS

// 方式一:全局配置(推荐)
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")              // 允许跨域的路径
                .allowedOriginPatterns("*")         // 允许的源(SpringBoot 2.4+ 用 patterns)
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")                // 允许的请求头
                .allowCredentials(true)             // 允许携带 Cookie
                .maxAge(3600);                      // 预检请求缓存时间(秒)
    }
}

// 方式二:过滤器方式(更灵活)
@Component
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;

        // 设置允许的源(生产环境应配置具体域名)
        String origin = request.getHeader("Origin");
        if (isAllowedOrigin(origin)) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        }

        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Max-Age", "3600");

        // 预检请求直接返回
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        chain.doFilter(req, res);
    }
}

5. CORS 常见响应头

响应头说明示例
Access-Control-Allow-Origin允许的源*http://localhost:3000
Access-Control-Allow-Methods允许的方法GET, POST, PUT, DELETE
Access-Control-Allow-Headers允许的请求头Content-Type, X-Token
Access-Control-Allow-Credentials是否允许 Cookietrue
Access-Control-Max-Age预检缓存时间3600(秒)
Access-Control-Expose-Headers暴露给前端的响应头X-Total-Count

三、方案二:代理服务器

代理服务器的原理是:同源策略只限制浏览器,不限制服务器。通过同源的服务器代理转发请求,绕过浏览器的跨域限制。

代理服务器的工作原理:

  • 浏览器视角:请求发送到 localhost:3000,同源,无跨域问题。

  • 代理服务器:前端开发服务器(如 Vite、Webpack)将 /api/* 的请求代理到后端服务器。

  • 服务器间通信:服务器之间通信不受同源策略限制。

Vite 配置代理示例

// vite.config.js
export default {
  server: {
    proxy: {
      // 将 /api 请求代理到后端
      '/api': {
        target: 'http://localhost:8080',  // 后端地址
        changeOrigin: true,               // 修改 Origin 头
        rewrite: (path) => path.replace(/^\/api/, '')  // 路径重写
      }
    }
  }
}

Nginx 反向代理配置

# 生产环境可用 Nginx 做反向代理
server {
    listen 80;
    server_name example.com;

    # 前端静态资源
    location / {
        root /var/www/html;
        index index.html;
    }

    # API 代理
    location /api/ {
        proxy_pass http://backend-server:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

四、方案三:JSONP

JSONP(JSON with Padding)是早期常用的跨域方案,利用 <script> 标签不受同源限制的特性。

JSONP 的工作原理:

  • 利用 <script> 标签<script> 标签的 src 属性不受同源策略限制。

  • 回调函数机制:前端通过 URL 参数传递回调函数名,后端返回调用该函数的 JS 代码。

  • 自动执行:浏览器加载并执行返回的 JS 代码,触发回调函数。

JSONP 实现代码

// 前端实现
function jsonp(url, callback) {
    const callbackName = 'jsonp_' + Date.now();

    // 创建全局回调函数
    window[callbackName] = function(data) {
        callback(data);
        delete window[callbackName];           // 清理全局函数
        document.body.removeChild(script);     // 移除 script 标签
    };

    // 创建 script 标签
    const script = document.createElement('script');
    script.src = `${url}?callback=${callbackName}`;
    document.body.appendChild(script);
}

// 使用
jsonp('http://api.example.com/user', function(data) {
    console.log(data);
});

JSONP 的局限性

  • 只支持 GET 请求:因为是通过 URL 传参,无法发送 POST 等请求。

  • 存在安全风险:如果后端被劫持,可以返回恶意代码执行。

  • 错误处理困难:无法通过 HTTP 状态码判断请求是否成功。

五、方案对比与选择

六、携带 Cookie 的跨域问题

当需要跨域携带 Cookie 时,需要额外配置:

// 前端:fetch 需要设置 credentials
fetch('http://api.example.com/user', {
    credentials: 'include'  // 携带 Cookie
});

// 前端:axios 需要设置 withCredentials
axios.get('http://api.example.com/user', {
    withCredentials: true
});
// 后端:CORS 配置
// 注意:allowCredentials(true) 时,allowOrigin 不能为 "*"
response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");  // 具体域名
response.setHeader("Access-Control-Allow-Credentials", "true");

重要注意事项

  • Access-Control-Allow-Credentials: true 时,Access-Control-Allow-Origin 不能 设置为 *,必须指定具体域名。

  • Cookie 也受同源策略限制,跨域 Cookie 需要设置 SameSite=None; Secure(仅 HTTPS)。

面试高频追问

  1. 追问一:为什么表单提交、图片加载不受跨域限制,但 AJAX 会受限制?

    回答思路:跨域限制的目的是保护用户数据安全。表单提交会跳转页面,不会泄露数据给原页面;图片加载只能显示,无法读取内容。而 AJAX 可以读取响应内容,如果恶意网站能通过 AJAX 访问你的银行账户,就能窃取数据,所以需要限制。

  2. 追问二:CORS 的预检请求会对性能有影响吗?如何优化?

    回答思路:预检请求会增加一次网络往返,有性能开销。优化方式:一是设置 Access-Control-Max-Age 缓存预检结果;二是尽量避免使用触发预检的请求方式(如 Content-Type: application/json 改为 text/plain,但不太推荐);三是服务端对 OPTIONS 请求快速响应,不进行业务处理。

  3. 追问三:WebSocket 为什么不受跨域限制?

    回答思路:WebSocket 使用 ws://wss:// 协议,建立连接时虽然也发送 Origin 头,但它的安全模型与 AJAX 不同。WebSocket 连接需要服务端显式验证 Origin,而不是浏览器拦截。实际上服务端应该校验 Origin,避免恶意网站建立 WebSocket 连接。

常见面试变体

  • 变体一:介绍一下 CORS 的工作原理?
  • 变体二:简单请求和预检请求有什么区别?
  • 变体三:开发环境和生产环境分别如何解决跨域问题?
  • 变体四:跨域请求能携带 Cookie 吗?需要注意什么?

记忆口诀

同源策略三要素:协议域名端口要一致。

解决方案记四招:CORS 标准、代理转发、JSONP 兼容、postMessage 通信。

CORS 流程:简单请求直接发,预检先问 OPTIONS,响应头部要配置全。

总结

跨域是浏览器 同源策略(协议 + 域名 + 端口)导致的安全限制。主流解决方案是 CORS(后端设置 Access-Control-* 响应头),开发环境可用 代理服务器 绕过,老旧系统可用 JSONP(仅 GET)。生产环境推荐 Nginx 反向代理 + CORS 组合方案,注意携带 Cookie 时 Allow-Origin 不能为 *