Skip to content

前端常见痛点:当后端说"跨域问题前端自己处理"

问题背景

"接口报错了,Network面板显示跨域问题。"

"这是前端的问题,你们自己处理一下。"

"但这是后端配置的问题啊..."

"MDN文档上说前端可以解决的,你看看吧。"

作为前端开发者,你是否曾遇到过这样的对话?跨域问题是前端开发中最常见的痛点之一,而且往往伴随着前后端之间的扯皮。今天,我们就来彻底理解这个问题,搞清楚什么是跨域,为什么会有跨域限制,以及谁应该(以及如何)解决这个问题。

同源策略:浏览器的安全基石

什么是同源策略?

同源策略(Same-Origin Policy)是浏览器的一项核心安全机制,它限制了来自一个源的文档或脚本如何与另一个源的资源进行交互。所谓"同源",指的是相同的协议、域名和端口号。

例如:

  • https://example.com/page1https://example.com/page2 是同源的
  • https://example.comhttp://example.com 不同源(协议不同)
  • https://example.comhttps://api.example.com 不同源(域名不同)
  • https://example.comhttps://example.com:8080 不同源(端口不同)

为什么浏览器要实施同源策略?

想象一下,如果没有同源策略:

  1. 你登录了银行网站,获得了认证 Cookie
  2. 同时你不小心访问了恶意网站
  3. 恶意网站的JavaScript可以直接向你的银行网站发送请求,并带上你的Cookie
  4. 恶意网站就可以以你的身份执行转账等操作

同源策略正是防止了这类攻击,保护用户的安全和隐私。

同源策略具体限制了什么?

  • Cookie、LocalStorage 和 IndexDB 的读取受限
  • DOM 的访问受限
  • AJAX 请求的发送受限(最常见的跨域问题)

项目中的典型跨域场景

场景一:前端与API服务部署在不同域名下

前端: https://www.myapp.com
API: https://api.myapp.com

这是企业应用中最常见的架构,前端静态资源和API服务分开部署,必然面临跨域问题。

场景二:本地开发环境连接测试服务器

前端: http://localhost:3000
API: https://test-api.myapp.com

开发时,前端运行在本地服务器,而API可能部署在测试环境,这也会导致跨域问题。

场景三:微前端或多系统集成

主应用: https://main.myapp.com
子应用1: https://app1.myapp.com
子应用2: https://app2.myapp.com

在微前端架构中,不同子应用可能部署在不同域名下,它们之间的通信也会遇到跨域问题。

解决跨域问题的常见方法

1. CORS(跨域资源共享)- 后端实现

跨域资源共享(Cross-Origin Resource Sharing,CORS)是最标准、最推荐的跨域解决方案。它通过添加HTTP头部信息,使服务器能够声明哪些源可以访问其资源。

实现方式:服务端在HTTP响应头中添加 Access-Control-Allow-Origin 等字段。

javascript
// Node.js Express 示例
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://www.myapp.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');
  
  // 处理预检请求
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  
  next();
});

其他后端框架的CORS配置

  • Spring Boot (Java):

    java
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("https://www.myapp.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE")
                    .allowedHeaders("*")
                    .allowCredentials(true);
        }
    }
  • Django (Python):

    python
    # settings.py
    CORS_ALLOWED_ORIGINS = [
        "https://www.myapp.com",
    ]
    CORS_ALLOW_METHODS = [
        "GET",
        "POST",
        "PUT",
        "DELETE",
        "OPTIONS",
    ]
    CORS_ALLOW_CREDENTIALS = True

2. 代理服务器 - 前端开发环境或生产环境

当你无法修改后端代码时,可以通过代理服务器转发请求,绕过同源策略。

开发环境代理

  • Webpack Dev Server:

    javascript
    // webpack.config.js
    module.exports = {
      devServer: {
        proxy: {
          '/api': {
            target: 'https://api.myapp.com',
            changeOrigin: true,
            pathRewrite: { '^/api': '' }
          }
        }
      }
    };
  • Vite:

    javascript
    // vite.config.js
    export default {
      server: {
        proxy: {
          '/api': {
            target: 'https://api.myapp.com',
            changeOrigin: true,
            rewrite: (path) => path.replace(/^\/api/, '')
          }
        }
      }
    }

生产环境代理

  • Nginx:
    nginx
    server {
      listen 80;
      server_name www.myapp.com;
      
      location /api/ {
        proxy_pass https://api.myapp.com/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
      }
      
      location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
      }
    }

3. JSONP - 古老但仍有效的方法(仅支持GET请求)

JSONP利用<script>标签没有跨域限制的特性,通过动态创建script标签,从服务器请求数据。

javascript
function jsonp(url, callback) {
  const script = document.createElement('script');
  const callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
  
  window[callbackName] = function(data) {
    callback(data);
    document.body.removeChild(script);
    delete window[callbackName];
  };
  
  script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${callbackName}`;
  document.body.appendChild(script);
}

// 使用示例
jsonp('https://api.myapp.com/data', function(data) {
  console.log('Data received:', data);
});

后端实现(以Node.js为例):

javascript
app.get('/data', (req, res) => {
  const data = { name: 'John', age: 30 };
  const callback = req.query.callback;
  
  if (callback) {
    res.send(`${callback}(${JSON.stringify(data)})`);
  } else {
    res.json(data);
  }
});

4. 跨域资源共享的高级配置

处理带凭证的请求(Cookies)

如果需要在跨域请求中发送Cookie,前端和后端都需要额外配置:

前端

javascript
fetch('https://api.myapp.com/data', {
  credentials: 'include'  // 发送凭证
})

后端

javascript
// 必须指定具体的源,不能使用*通配符
res.header('Access-Control-Allow-Origin', 'https://www.myapp.com');
// 必须设置为true
res.header('Access-Control-Allow-Credentials', 'true');

处理预检请求(Preflight)

对于非简单请求(如使用PUT、DELETE方法,或包含自定义头的请求),浏览器会先发送一个OPTIONS请求(称为"预检请求")。

配置预检请求缓存

javascript
// 预检请求的结果可以缓存多长时间(秒)
res.header('Access-Control-Max-Age', '86400'); // 24小时

谁应该解决跨域问题?

现在我们回到最初的问题:当项目中遇到跨域问题时,到底应该谁来解决?

正确的答案:主要是后端的责任

  1. CORS是在服务器端实现的标准:W3C设计CORS的初衷就是让服务器声明谁可以访问其资源。

  2. 安全控制应由资源所有者决定:后端作为API的提供者和数据的守护者,有责任决定哪些客户端可以访问其资源。

  3. 前端代理只是一种变通方法:虽然前端可以通过代理解决跨域问题,但这只是在后端不配合的情况下的临时解决方案,不是最佳实践。

前后端合作解决方案

最理想的情况是前后端共同协作:

  1. 开发环境:前端使用代理服务器(如webpack-dev-server或vite的proxy功能)解决本地开发时的跨域问题。

  2. 测试和生产环境:后端正确配置CORS头,允许前端域名访问API。

  3. 部署架构优化:如果可能,考虑将前端和API部署在同一个域名下,通过路径区分(如 /api/),从根本上避免跨域问题。

与后端沟通的技巧

当你需要与后端开发者沟通解决跨域问题时,以下是一些有用的技巧:

  1. 解释而非指责:不要简单地说"这是后端问题",而是解释同源策略和CORS的工作原理。

  2. 提供具体的解决方案:准备好对应后端框架的CORS配置示例代码。

  3. 解释安全考量:说明为什么后端需要控制资源访问权限,以及为什么这是一个安全最佳实践。

  4. 提供临时解决方案:如果后端团队短期内无法解决,提出前端代理或其他临时解决方案。

总结

跨域问题是由浏览器的同源策略安全机制引起的,它限制了来自不同源的资源访问。虽然前端可以通过代理等方式临时解决跨域问题,但标准的解决方案是由后端实现CORS。

理解同源策略和CORS的工作原理,能帮助前端开发者更好地与后端团队沟通,共同解决跨域问题。在实际项目中,前后端应该共同协作,采用最合适的解决方案,既保证应用的安全性,又提供良好的开发体验。

下次当你再听到"跨域问题前端自己处理"这样的话时,你就可以自信地解释为什么这主要是一个后端配置问题,并提供具体的解决方案。

用知识点燃技术的火山