跨域

201 阅读6分钟

总体来说两个解法:

  1. CorsConfig
  2. Nginx 代理

核心概念

什么是跨域问题?

跨域问题是浏览器的安全机制(同源策略),当网页从一个域名向另一个域名发送请求时,浏览器会阻止这种请求。

同源策略判断标准

浏览器检查三个要素:协议 + 域名 + 端口

// 当前页面:https://stplan.example.com:443

// ✅ 同源请求(允许)
'https://stplan.example.com:443/api/data'
'https://stplan.example.com/other-page'
'https://stplan.example.com:443/api/user/123'

// ❌ 跨域请求(被阻止)
'http://stplan.example.com/api'              // 协议不同(http vs https)
'https://qeelin.example.com/api/data'        // 域名不同(stplan vs qeelin)
'https://stplan.example.com:8080/api'        // 端口不同(443 vs 8080)

为什么要限制跨域?

1. 保护用户隐私

// 恶意网站 evil.example.com 的页面
// 如果没有跨域限制,可以:
fetch('https://bank.example.com/account/balance')  // 窃取银行信息
fetch('https://social.example.com/api/messages')   // 窃取私人消息

2. 防止 CSRF 攻击

// 利用用户登录状态执行恶意操作
fetch('https://bank.example.com/transfer', {
  method: 'POST',
  credentials: 'include',
  body: JSON.stringify({
    to: 'hacker-account',
    amount: 10000
  })
});

3. 防止信息泄露

// 探测内网信息
fetch('http://192.168.1.100:8080/admin/users')
  .then(() => console.log('发现内网服务!'))

跨域问题的表现

浏览器报错信息

Access to fetch at 'https://qeelin.example.com/api/plan/109909' 
from origin 'https://stplan.example.com' 
has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

前端代码失败

fetch('https://qeelin.example.com/api/plan/109909')
  .then(response => response.json())
  .catch(error => {
    console.error('跨域请求被阻止!', error);
    // 功能无法正常使用
  });

重要概念澄清

✅ 跨域只存在于浏览器环境

// ❌ 有跨域问题:浏览器中的网页请求
fetch('https://qeelin.example.com/api/plan/109909')

// ✅ 没有跨域问题:服务器端调用
// Java后端调用其他服务
restTemplate.getForObject("https://qeelin.example.com/api/plan/109909", String.class);

✅ 跨域是前端问题,不是后端问题

  • 后端服务之间的调用不受跨域限制
  • 只有浏览器中的 JavaScript 请求才会被跨域策略阻止
  • 后端可以自由调用任何其他服务

实际场景分析

你的项目情况

前端地址:https://stplan.example.com/#/plans
后端地址:https://qeelin.example.com/api/plan/109909

浏览器检查:
  当前页面:stplan.example.com
  请求目标:qeelin.example.com
  ❌ 不同源 → 触发跨域限制

完整的请求流程(有跨域问题)

┌─────────────────────────────────────────────────────────┐
│                     用户浏览器                           │
│  页面:https://stplan.example.com/#/plans               │JavaScript 代码:                                       │
│  fetch('https://qeelin.example.com/api/plan/109909')   │
│                                                         │
│  浏览器同源检查:                                        │
│  stplan.example.com ≠ qeelin.example.com                │
│  ❌ 不同源 → 这是跨域请求!                             │
└─────────────────────────────────────────────────────────┘
                        ↓ OPTIONS 预检请求
                        ↓
┌─────────────────────────────────────────────────────────┐
│                  真实后端 (qeelin)                       │
│  接收 OPTIONS 预检请求                                   │
│  返回 CORS 响应头:                                      │
│  Access-Control-Allow-Origin: ???                       │
│  (如果没有或不匹配,浏览器阻止请求)                   │
└─────────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────────┐
│                     用户浏览器                           │
│  ❌ CORS 检查失败 → 浏览器阻止 JavaScript 访问响应      │
│  错误:No 'Access-Control-Allow-Origin' header          │
└─────────────────────────────────────────────────────────┘

跨域解决方案

方案 1:CORS 配置(推荐 ⭐⭐⭐⭐⭐)

原理

在后端响应头中添加 Access-Control-Allow-Origin,告诉浏览器允许这个跨域请求。

实现方式

全局 CORS 配置(推荐)

package com.meituan.stplan.platform.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")  // 匹配 /api 下的所有路径
                .allowedOrigins("https://stplan.example.com")  // 允许的前端域名
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")  // 允许的 HTTP 方法
                .allowedHeaders("*")  // 允许所有请求头
                .allowCredentials(true)  // 允许携带 Cookie
                .maxAge(3600);  // 预检请求缓存时间(秒)
    }
}

单个接口配置

@RestController
@RequestMapping("/api")
public class PlanController {
    
    @CrossOrigin(origins = "https://stplan.example.com")
    @GetMapping("/plan/{id}")
    public ResponseEntity<?> getPlan(@PathVariable Long id) {
        // 业务逻辑
        return ResponseEntity.ok(data);
    }
}

完整的请求流程(CORS 解决后)

┌─────────────────────────────────────────────────────────┐
│                     用户浏览器                           │
│  页面:https://stplan.example.com/#/plans               │
│  请求:https://qeelin.example.com/api/plan/109909       │
│                                                         │
│  浏览器同源检查:                                        │
│  stplan.example.com ≠ qeelin.example.com                │
│  ❌ 不同源 → 发送 OPTIONS 预检请求                      │
└─────────────────────────────────────────────────────────┘
                        ↓ OPTIONS 预检请求
                        ↓
┌─────────────────────────────────────────────────────────┐
│                  真实后端 (qeelin)                       │
│  接收 OPTIONS 预检请求                                   │
│  返回 CORS 响应头:                                      │
│  HTTP/1.1 200 OK                                        │
│  Access-Control-Allow-Origin: https://stplan.example.com
│  Access-Control-Allow-Methods: GET, POST, PUT, DELETE  │
│  Access-Control-Allow-Headers: *                        │
│  Access-Control-Max-Age: 3600                           │
└─────────────────────────────────────────────────────────┘
                        ↓ CORS 检查通过
                        ↓
┌─────────────────────────────────────────────────────────┐
│                     用户浏览器                           │
│  ✅ CORS 检查通过 → 发送真实请求                        │
│  GET /api/plan/109909 HTTP/1.1                          │
└─────────────────────────────────────────────────────────┘
                        ↓ GET 请求
                        ↓
┌─────────────────────────────────────────────────────────┐
│                  真实后端 (qeelin)                       │
│  处理请求,返回数据:                                    │
│  {                                                      │
│    "id": 109909,                                        │
│    "name": "演练计划",                                  │
│    "status": "进行中"                                   │
│  }                                                      │
└─────────────────────────────────────────────────────────┘
                        ↓ 响应数据
                        ↓
┌─────────────────────────────────────────────────────────┐
│                     用户浏览器                           │
│  ✅ JavaScript 可以访问响应数据                         │
│  页面更新,显示数据                                      │
└─────────────────────────────────────────────────────────┘

优点

  • ✅ 前端代码不需要改
  • ✅ 改动最少(只改后端)
  • ✅ 实施最快(几分钟搞定)
  • ✅ 不需要运维改 Nginx
  • ✅ 维护简单

缺点

  • ❌ 后端需要暴露 CORS 配置
  • ❌ 如果有多个前端域名,需要都加到白名单

方案 2:Nginx 反向代理(推荐 ⭐⭐⭐)

原理

通过 Nginx 在同一域名下代理请求,浏览器认为前后端在同一域名,不会触发跨域限制。

实现方式

Nginx 配置

server {
    listen 443 ssl;
    server_name stplan.example.com;
    
    # SSL 证书配置
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    # 前端静态资源
    location / {
        root /var/www/stplan;
        try_files $uri $uri/ /index.html;
    }
    
    # API 请求代理到 qeelin
    location /api/ {
        proxy_pass https://qeelin.example.com/;
        
        # 保留原始请求头
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

前端代码改动

// ❌ 原来(会跨域)
fetch('https://qeelin.example.com/api/plan/109909')

// ✅ 改成(通过 Nginx 代理)
fetch('https://stplan.example.com/api/plan/109909')

完整的请求流程(Nginx 反向代理)

┌─────────────────────────────────────────────────────────┐
│                     用户浏览器                           │
│  页面:https://stplan.example.com/#/plans               │
│  请求:https://stplan.example.com/api/plan/109909       │
│                                                         │
│  浏览器同源检查:                                        │
│  stplan.example.com === stplan.example.com              │
│  ✅ 同源 → 不是跨域 → 直接发送请求                      │
└─────────────────────────────────────────────────────────┘
                        ↓ 同源请求(没有跨域检查)
                        ↓
┌─────────────────────────────────────────────────────────┐
│                    Nginx 服务器                          │
│  (浏览器认为这就是 stplan)                               │
│                                                         │
│  接收请求:/api/plan/109909                             │
│  匹配 location /api/ 规则                                │
│  转发到:https://qeelin.example.com/api/plan/109909     │
│  (浏览器看不到这一步)                                    │
└─────────────────────────────────────────────────────────┘
                        ↓ 后台转发(浏览器看不到)
                        ↓
┌─────────────────────────────────────────────────────────┐
│                  真实后端 (qeelin)                       │
│  处理请求,返回数据:                                    │
│  {                                                      │
│    "id": 109909,                                        │
│    "name": "演练计划",                                  │
│    "status": "进行中"                                   │
│  }                                                      │
└─────────────────────────────────────────────────────────┘
                        ↓ 返回给 Nginx
                        ↓
┌─────────────────────────────────────────────────────────┐
│                    Nginx 服务器                          │
│  返回响应给浏览器                                        │
└─────────────────────────────────────────────────────────┘
                        ↓ 响应来自 stplan
                        ↓
┌─────────────────────────────────────────────────────────┐
│                     用户浏览器                           │
│  接收响应(认为来自 stplan)                             │
│  ✅ 没有 CORS 错误                                       │
│  JavaScript 可以访问响应数据                             │
└─────────────────────────────────────────────────────────┘

优点

  • ✅ 后端代码不需要改
  • ✅ 更安全(后端不暴露 CORS)
  • ✅ 性能更好(Nginx 层面处理)
  • ✅ 可以统一管理多个后端服务

缺点

  • ❌ 需要改前端代码(所有请求 qeelin 的地方改成请求 stplan)
  • ❌ 需要改 Nginx 配置
  • ❌ 改动范围大

方案 3:后端代理(不推荐)

原理

在前端同域的后端添加代理接口,由后端调用其他服务(后端调用没有跨域限制)。

实现方式

@RestController
@RequestMapping("/api")
public class ProxyController {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping("/proxy/plan/{id}")
    public ResponseEntity<?> proxyGetPlan(@PathVariable Long id) {
        // 后端调用其他服务,没有跨域问题
        String result = restTemplate.getForObject(
            "https://qeelin.example.com/api/plan/" + id, 
            String.class
        );
        return ResponseEntity.ok(result);
    }
}

前端代码

// 请求同域的后端代理接口
fetch('https://stplan.example.com/api/proxy/plan/109909')
  .then(res => res.json())
  .then(data => console.log(data))

缺点

  • ❌ 增加后端负担
  • ❌ 需要为每个接口写代理
  • ❌ 性能下降(多一层转发)

方案对比

方案改前端改后端改 Nginx难度推荐度实施时间
CORS 配置❌ 不需要✅ 需要❌ 不需要⭐ 简单⭐⭐⭐⭐⭐5 分钟
Nginx 反向代理✅ 需要❌ 不需要✅ 需要⭐⭐ 中等⭐⭐⭐30 分钟
后端代理❌ 不需要✅ 需要❌ 不需要⭐⭐⭐ 复杂⭐ 不推荐1 小时+

总结

概念说明
🔒 跨域是什么浏览器的安全机制,保护用户免受恶意攻击
🌐 跨域的范围只影响浏览器中的请求,服务器间调用不受限制
🛠️ 主流解决方案CORS 配置 + Nginx 反向代理
最佳实践使用 CORS 配置,简单快速
🚫 避免完全关闭跨域限制,会带来安全风险

参考资源