文章目录
前言:跨域问题的本质
同源策略(Same-Origin Policy)详解
浏览器的安全机制,要求请求的 协议、域名、端口 必须完全一致,否则视为跨域。例如:
http://a.com:8080
请求http://a.com:8080/api
→ 同源http://a.com:8080
请求http://a.com:8090/api
→ 跨域(端口不同)http://a.com
请求https://a.com/api
→ 跨域(协议不同)
跨域请求的触发场景
- AJAX 请求:
XMLHttpRequest
或fetch
调用外部接口 - WebSocket:与非同源服务器建立连接
- Cookie 与凭证:携带
Cookie
的请求需显式配置withCredentials
一、解决方案一:CORS 标准实现(推荐首选)
1.1 CORS 原理
CORS(跨域资源共享)通过 服务器响应头 告知浏览器允许的跨域来源。浏览器会根据这些头信息判断是否允许请求。
CORS 请求分类
-
简单请求(Simple Request):
- 方法:
GET
,POST
,HEAD
- 头信息:仅限
Accept
,Accept-Language
,Content-Language
,Content-Type
(且Content-Type
仅限application/x-www-form-urlencoded
,multipart/form-data
,text/plain
) - 浏览器自动添加
Origin
头。
- 方法:
-
预检请求(Preflight Request):
- 当请求为 非简单请求(如
PUT
,DELETE
或自定义头字段)时,浏览器会先发送OPTIONS
请求,询问服务器是否允许该请求。 - 服务器需在
OPTIONS
响应中返回允许的Methods
,Headers
和Origin
。
- 当请求为 非简单请求(如
1.2 实现步骤与代码示例
1.2.1 Spring Boot 实现
// 单接口配置
@RestController
public class UserController {
@CrossOrigin(
origins = "http://client.example.com",
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = "Authorization, Content-Type",
exposedHeaders = "X-Custom-Header",
allowCredentials = true,
maxAge = 3600 // 预检缓存时间(秒)
)
@GetMapping("/user")
public User getUser() {
return new User("John Doe", 30);
}
}
// 全局配置(推荐)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配所有路径
.allowedOrigins("http://client.example.com", "https://another-domain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
.exposedHeaders("X-Total-Count", "X-RateLimit-Limit")
.allowCredentials(true)
.maxAge(3600);
}
}
1.2.2 Node.js/Express.js 实现
const express = require('express');
const cors = require('cors');
const app = express();
// 动态验证来源
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = ['http://client.example.com', 'https://another-domain.com'];
if (allowedOrigins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
exposedHeaders: ['X-Total-Count'],
maxAge: 86400 // 预检缓存24小时
};
app.use(cors(corsOptions));
// 处理OPTIONS请求
app.options('/api/*', cors(corsOptions));
1.3 安全加固
- 禁止
*
与credentials
同时使用:Access-Control-Allow-Origin
为*
时,无法携带Cookie
。 - 白名单机制:仅允许可信域名。
- 防CSRF攻击:结合
XSRF-TOKEN
和SameSite
属性。
二、解决方案二:JSONP(仅限GET请求)
2.1 JSONP 原理
利用 <script>
标签的跨域特性,通过动态注入脚本实现数据回传。服务端需将数据封装到前端定义的回调函数中。
关键点:
- 只能处理GET请求:
<script>
标签仅支持GET请求。 - 易受XSS攻击:需严格验证回调函数名。
2.2 实现步骤与代码示例
2.2.1 前端代码(动态注入)
function handleResponse(data) {
console.log("Received data:", data);
}
// 动态生成script标签
const script = document.createElement('script');
script.src = `http://api.example.com/data?callback=handleResponse`;
document.head.appendChild(script);
2.2.2 后端代码(Java示例)
@RestController
public class JsonpController {
@GetMapping("/data")
public String handleJsonp(
@RequestParam String callback // 接收回调函数名
) {
User user = new User("Alice", 25);
// 防XSS攻击:验证回调函数名格式
if (!callback.matches("^[a-zA-Z0-9_]+$")) {
throw new IllegalArgumentException("Invalid callback parameter");
}
// 封装数据到回调函数
return callback + "(" + new Gson().toJson(user) + ")";
}
}
2.3 安全加固
- 严格验证回调函数名:防止注入攻击。
- 数据校验:确保返回数据不包含恶意代码。
三、解决方案三:Nginx 反向代理
3.1 反向代理原理
通过Nginx将前端请求转发到后端服务,使浏览器认为请求与当前页面同源。
核心配置步骤:
- 代理转发:将请求转发到后端服务。
- CORS头配置:动态设置
Access-Control-Allow-Origin
。 - WebSocket支持:处理
Upgrade
和Connection
头。
3.2 配置示例(含WebSocket)
server {
listen 443 ssl;
server_name frontend.example.com;
# SSL配置
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location /api/ {
# 反向代理到后端服务
proxy_pass http://backend.example.com:3000;
# 传递请求头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# CORS配置
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Allow-Credentials "true";
# 处理预检请求(OPTIONS)
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Max-Age' 1728000;
return 204;
}
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
3.3 安全加固
- 动态验证来源:
map $http_origin $allowed_origin { default ''; ~^(http://client\.example\.com|https://another-domain\.com)$ $http_origin; } add_header Access-Control-Allow-Origin $allowed_origin; if ($allowed_origin = '') { return 403; }
- 限制请求方法:仅允许白名单内的方法。
四、解决方案四:API网关统一处理(微服务场景)
4.1 API网关原理
作为微服务的统一入口,API网关负责 路由、认证、限流、CORS配置 等功能,降低后端服务的复杂度。
优势:
- 集中管理:统一配置CORS、鉴权、日志等。
- 动态路由:根据请求路径动态转发到后端服务。
4.2 Spring Cloud Gateway 实现
// 全局CORS配置
@Component
public class GlobalCorsFilter implements GlobalCorsProperties {
@Override
public CorsConfiguration getCorsConfiguration() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(
List.of("http://client.example.com", "https://another-domain.com")
);
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(
List.of("Authorization", "Content-Type", "X-Requested-With")
);
config.setExposedHeaders(List.of("X-Total-Count"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
return config;
}
}
// 动态白名单配置(从数据库读取)
@Component
public class DynamicCorsConfig implements WebFilter {
@Autowired
private CorsProperties corsProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String origin = exchange.getRequest().getHeaders().getOrigin();
// 验证来源
if (!corsProperties.getAllowedOrigins().contains(origin)) {
return exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN).then();
}
// 设置CORS头
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setAccessControlAllowOrigin(origin);
response.getHeaders().setAccessControlAllowMethods(corsProperties.getAllowedMethods());
response.getHeaders().setAccessControlAllowHeaders(corsProperties.getAllowedHeaders());
return chain.filter(exchange);
}
}
4.3 安全加固
- 限流与熔断:结合
Resilience4j
或Sentinel
。 - 请求签名:对敏感接口进行签名验证。
五、解决方案五:代理服务器(Node.js示例)
5.1 代理服务器原理
通过中间代理服务器转发请求,代理服务器与前后端同源,避免浏览器拦截。
优势:
- 开发环境快速配置。
- 支持复杂路由规则。
5.2 Node.js 实现(http-proxy-middleware)
// proxy.config.js
module.exports = {
'/api': {
target: 'http://backend.example.com:3000',
changeOrigin: true,
pathRewrite: { '^/api': '' },
headers: {
Host: 'backend.example.com'
},
onProxyReq: (proxyReq, req, res) => {
// 动态修改请求头
proxyReq.setHeader('X-Forwarded-Proto', req.protocol);
proxyReq.setHeader('X-Real-IP', req.ip);
},
onProxyRes: (proxyRes, req, res) => {
// 处理响应头
proxyRes.headers['Access-Control-Expose-Headers'] = 'X-Total-Count';
}
}
};
5.3 安全加固
- HTTPS强制跳转:代理服务器仅允许HTTPS请求。
- 速率限制:使用
express-rate-limit
模块。
六、解决方案六:服务器端渲染(SSR)
6.1 SSR 原理
在服务器端直接渲染页面,避免浏览器发起跨域请求。例如:
- Next.js:预渲染页面并返回完整HTML。
- Nuxt.js:服务端渲染Vue应用。
优势:
- SEO友好:搜索引擎可直接抓取渲染后的HTML。
- 首屏加载快:服务器返回完整页面。
6.2 Next.js 实现示例
// pages/index.js
export async function getServerSideProps() {
const res = await fetch('http://api.example.com/data', {
headers: {
Authorization: 'Bearer YOUR_TOKEN' // 可携带凭证
}
});
const data = await res.json();
return { props: { data } };
}
export default function Home({ data }) {
return <div>{JSON.stringify(data)}</div>;
}
6.3 安全加固
- 防CSRF:在服务端验证请求来源。
- 数据过滤:对渲染内容进行XSS过滤。
七、方案选择决策树
场景 | 推荐方案 | 原因 | 技术栈 |
---|---|---|---|
单页应用(SPA)开发 | Nginx反向代理 / 代理服务器 | 开发与生产环境统一配置,避免前后端分离的复杂性 | Node.js, Nginx |
微服务架构 | API网关统一处理 | 集中式管理,支持动态路由与权限控制 | Spring Cloud Gateway, Kong |
旧项目兼容第三方API | JSONP | 无需后端改造,快速集成 | Vanilla JS |
需要严格安全控制 | CORS标准实现 + 白名单 | 细粒度配置,支持所有HTTP方法 | Spring Boot, Express.js |
WebSocket跨域 | Nginx反向代理 + WebSocket支持 | 需要处理Upgrade头和Connection头 | Nginx |
服务端渲染(SSR) | 服务器端直接请求 | 避免浏览器发起跨域请求 | Next.js, Nuxt.js |
八、常见问题与最佳实践
8.1 预检请求(OPTIONS)的深度处理
- 问题:当请求包含自定义头或使用非简单方法(如PUT/DELETE)时,浏览器会先发送OPTIONS请求。
- 解决方案:
- 在后端显式返回
Access-Control-Allow-Methods
和Access-Control-Allow-Headers
- 对OPTIONS请求返回204 No Content状态码
- 在后端显式返回
Spring Boot示例:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With");
}
}
8.2 安全性建议
- 避免使用
*
与allowCredentials
同时开启:// 错误配置 app.use(cors({ origin: '*', credentials: true }));
- 限制
allowedOrigins
为可信域名列表:allowedOrigins: ["http://client.example.com", "https://another-domain.com"]
- 对敏感接口启用CSRF防护:
app.use(csrf()); app.use((req, res, next) => { res.cookie('XSRF-TOKEN', req.csrfToken()); next(); });
九、扩展知识点
9.1 WebSocket跨域解决方案
通过Nginx配置支持WebSocket:
location /ws/ {
proxy_pass http://backend-ws.example.com;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
add_header Access-Control-Allow-Origin $http_origin;
}
9.2 跨域Cookie处理
- 前端设置:
fetch('http://api.example.com', { credentials: 'include' // 允许携带Cookie });
- 后端配置:
add_header Set-Cookie "SameSite=None; Secure"; // HTTPS下强制跨域Cookie
十、总结
跨域问题的解决需要结合项目架构、安全需求与开发效率综合考量。CORS作为标准方案应优先采用,而Nginx、API网关等则适用于复杂场景。