
👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕Nginx这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- Nginx 解决跨域问题:Nginx 的 CORS 跨域配置实战 🌐🔧
-
- 🧠 一、CORS 是什么?为什么需要它?
-
- 1.1 同源策略的诞生
- 1.2 CORS 的作用
- 1.3 CORS 请求类型
-
- ✅ 简单请求(Simple Request)
- 🚧 复杂请求(Preflighted Request)
- 🛠️ 二、Nginx 作为 CORS 网关:为什么选它?
- 🧩 三、Nginx CORS 配置核心指令详解
-
- ✅ 基础配置模板(推荐)
- 🎯 四、实战场景一:单域名前端 + 单后端服务
-
- 4.1 Nginx 配置文件(/etc/nginx/sites-available/api-proxy)
- 4.2 Java 后端:Spring Boot API 示例
- 4.3 前端 JavaScript 调用示例
- 🌐 五、实战场景二:多前端域名授权(白名单机制)
-
- 5.1 需求
- 5.2 Nginx 实现方案:使用 map + 变量
- 5.3 测试验证
- 🛡️ 六、安全最佳实践:避免常见陷阱
-
- ❌ 错误做法 1:通配符 `*` + `credentials: true`
- ✅ 正确做法:动态匹配 Origin
- ❌ 错误做法 2:不处理 OPTIONS 请求
- ✅ 正确做法:显式处理 OPTIONS
- ❌ 错误做法 3:暴露敏感响应头
- ✅ 推荐暴露头(仅限前端需要的)
- 📊 七、CORS 请求流程图(Mermaid)
- 🧪 八、Java 后端配合:是否需要 CORS 配置?
-
- 为什么?
- ✅ 正确做法:**只在 Nginx 层统一处理 CORS**
- 🔄 九、Nginx + Java 实战:完整部署流程
-
- Step 1:准备 Java 后端(Spring Boot)
- Step 2:编译并运行 Java 服务
- Step 3:配置 Nginx
- Step 4:启用站点并重启 Nginx
- Step 5:前端测试页面(部署在 admin.mycompany.com)
- 🔍 十、调试技巧:如何验证 CORS 是否生效?
-
- 方法 1:Chrome DevTools
- 方法 2:curl 模拟 OPTIONS 预检
- 方法 3:使用在线 CORS 测试工具
- 🚀 十一、进阶:使用 Lua + OpenResty 实现动态 CORS(可选)
- 📈 十二、性能与缓存优化
-
- 1. `Access-Control-Max-Age` 设置建议
- 2. 避免重复响应头
- 3. 启用 Gzip 压缩(非 CORS,但重要)
- 🧭 十三、常见错误排查清单
- 🌍 十四、真实案例:某电商平台的 CORS 架构
- 💡 十五、总结:Nginx CORS 配置黄金法则
- 📚 十六、扩展阅读:CORS 与安全
- ✅ 最终建议:你的 Nginx CORS 配置模板(可直接复制)
- 🎉 结语:跨域不是问题,是架构的试金石
- 📌 附录:常用 Origin 正则表达式参考
- ✅ 你已经掌握了:
Nginx 解决跨域问题:Nginx 的 CORS 跨域配置实战 🌐🔧
在现代 Web 开发中,前后端分离架构已成为主流。前端应用通常部署在独立的域名或端口上(如 https://frontend.example.com),而后端 API 服务则运行在另一个域(如 https://api.example.com)。当浏览器加载前端页面并尝试通过 JavaScript 调用跨域 API 时,会触发 同源策略(Same-Origin Policy) 的限制,导致请求被拦截,控制台报错:
Access to fetch at 'https://api.example.com/users' from origin 'https://frontend.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这就是著名的 CORS(Cross-Origin Resource Sharing) 问题。而 Nginx,作为高性能的反向代理服务器和 Web 服务器,是解决这一问题最常用、最高效的工具之一。本篇将带你从零开始,深入理解 CORS 原理,掌握 Nginx 中 CORS 的完整配置方案,并结合 Java 后端实战案例,构建一个安全、灵活、可扩展的跨域解决方案。
🧠 一、CORS 是什么?为什么需要它?
1.1 同源策略的诞生
浏览器的同源策略是 Web 安全的基石之一。它规定:只有当协议、域名、端口三者完全相同时,页面脚本才能访问另一个页面的资源。
例如:
| https://example.com | ✅ 同源 | — |
| https://example.com/api | ✅ 同源 | 路径不同不影响 |
| http://example.com | ❌ 不同源 | 协议不同(http vs https) |
| https://api.example.com | ❌ 不同源 | 子域名不同 |
| https://example.com:8080 | ❌ 不同源 | 端口不同 |
这种限制虽然保护了用户免受 XSS、CSRF 等攻击,但也给现代应用开发带来了挑战 —— 前后端分离架构下,前端与后端必然不在同一源。
1.2 CORS 的作用
CORS 是 W3C 制定的标准,允许服务器明确声明哪些外部源可以访问其资源。它通过在 HTTP 响应头中添加特定字段,告知浏览器:“你可以信任这个来源,允许它发起跨域请求”。
🌟 CORS 不是浏览器的特性,而是服务器的权限声明机制。
1.3 CORS 请求类型
CORS 请求分为两类:
✅ 简单请求(Simple Request)
满足以下条件即为简单请求:
- 方法:GET、POST、HEAD
- 头部:仅限 Accept、Accept-Language、Content-Language、Content-Type(仅限 application/x-www-form-urlencoded、multipart/form-data、text/plain)
示例:
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Alice' })
})
⚠️ 注意:Content-Type: application/json 不是简单请求!它是复杂请求。
🚧 复杂请求(Preflighted Request)
不满足上述条件的请求,浏览器会先发送一个 OPTIONS 预检请求(Preflight Request),询问服务器是否允许该跨域请求。只有预检通过后,才会发送真实请求。
常见触发场景:
- 使用 PUT、DELETE、PATCH 方法
- 自定义请求头(如 Authorization、X-API-Key)
- Content-Type: application/json、application/xml
预检请求示例:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, X-API-Key
服务器响应必须包含:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, X-API-Key
Access-Control-Max-Age: 86400
只有当这些头信息匹配时,浏览器才允许后续的 PUT 请求。
🛠️ 二、Nginx 作为 CORS 网关:为什么选它?
在微服务架构中,前端通常通过 Nginx 作为统一入口,代理多个后端服务。Nginx 拥有以下优势:
| 🚀 性能极高 | 基于事件驱动,单进程处理高并发,内存占用低 |
| 🔧 配置灵活 | 支持基于 location、if、map 等精细控制 |
| 🔄 反向代理 | 可统一代理多个后端服务,集中处理 CORS |
| 🔒 安全可控 | 可在入口层统一拦截非法请求,减少后端负担 |
| 📦 无依赖 | 不需修改后端代码,实现零侵入式跨域管理 |
💡 最佳实践:将 CORS 配置放在 Nginx 层,而非后端(如 Spring Boot)。这样可以避免每个服务重复配置,提升维护性。
🧩 三、Nginx CORS 配置核心指令详解
Nginx 通过 add_header 指令设置响应头,从而实现 CORS 控制。以下是关键指令的详细说明:
| add_header Access-Control-Allow-Origin | 允许的源 | add_header Access-Control-Allow-Origin "https://frontend.example.com"; |
| add_header Access-Control-Allow-Credentials | 是否允许携带凭证(Cookie、Authorization) | add_header Access-Control-Allow-Credentials "true"; |
| add_header Access-Control-Allow-Methods | 允许的 HTTP 方法 | add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; |
| add_header Access-Control-Allow-Headers | 允许的请求头 | add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-API-Key"; |
| add_header Access-Control-Expose-Headers | 允许前端 JS 访问的响应头 | add_header Access-Control-Expose-Headers "X-Total-Count, X-Request-ID"; |
| add_header Access-Control-Max-Age | 预检请求缓存时间(秒) | add_header Access-Control-Max-Age 86400; |
| if ($request_method = 'OPTIONS') | 处理预检请求,避免重复处理 | if ($request_method = 'OPTIONS') { … } |
✅ 基础配置模板(推荐)
location /api/ {
# 代理到后端 Java 服务
proxy_pass http://localhost:8080;
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 "https://frontend.example.com";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
# 预检请求直接返回 204
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "https://frontend.example.com";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
add_header Access-Control-Max-Age 86400;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
✅ 为什么 OPTIONS 请求要单独处理? 因为如果不处理,Nginx 会将 OPTIONS 请求也转发给后端,而大多数 Java 服务(如 Spring Boot)默认不处理 OPTIONS,导致返回 405 或 500,前端报错。
🎯 四、实战场景一:单域名前端 + 单后端服务
假设我们有:
- 前端:https://app.mycompany.com(Nginx 静态站点)
- 后端:http://localhost:8080(Java Spring Boot 应用)
- Nginx 监听 443,代理 /api/* 到后端
4.1 Nginx 配置文件(/etc/nginx/sites-available/api-proxy)
server {
listen 443 ssl http2;
server_name app.mycompany.com;
ssl_certificate /etc/ssl/certs/app.mycompany.com.crt;
ssl_certificate_key /etc/ssl/private/app.mycompany.com.key;
# 静态资源
location / {
root /var/www/frontend;
try_files $uri $uri/ /index.html;
}
# API 代理
location /api/ {
proxy_pass http://localhost:8080;
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;
# CORS 头
add_header Access-Control-Allow-Origin "https://app.mycompany.com";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
# 预检请求处理
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "https://app.mycompany.com";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
add_header Access-Control-Max-Age 86400;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
# 错误页面
error_page 404 /404.html;
location = /404.html {
internal;
root /var/www/frontend;
}
}
4.2 Java 后端:Spring Boot API 示例
package com.example.controller;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.ArrayList;
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public List<User> getAllUsers() {
List<User> users = new ArrayList<>();
users.add(new User(1L, "Alice", "alice@example.com"));
users.add(new User(2L, "Bob", "bob@example.com"));
return users;
}
@PostMapping
public User createUser(@RequestBody User user) {
// 模拟保存
user.setId(System.currentTimeMillis());
return user;
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
// 模拟更新
user.setId(id);
return user;
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
// 模拟删除
System.out.println("Deleted user: " + id);
}
// 内部类:用户模型
static class User {
private Long id;
private String name;
private String email;
public User() {}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
}
4.3 前端 JavaScript 调用示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CORS Demo</title>
</head>
<body>
<h1>前端调用跨域 API</h1>
<button onclick="fetchUsers()">获取用户列表</button>
<button onclick="createUser()">创建用户</button>
<div id="result"></div>
<script>
const API_BASE = 'https://app.mycompany.com/api';
async function fetchUsers() {
try {
const response = await fetch(`${API_BASE}/users`, {
method: 'GET',
credentials: 'include', // 携带 Cookie
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
document.getElementById('result').innerHTML =
`<pre>${JSON.stringify(users, null, 2)}</pre>`;
} catch (error) {
document.getElementById('result').innerHTML =
`<p style="color:red">❌ 错误: ${error.message}</p>`;
}
}
async function createUser() {
try {
const response = await fetch(`${API_BASE}/users`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Charlie',
email: 'charlie@example.com'
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
document.getElementById('result').innerHTML =
`<p>✅ 创建成功: ${user.name} (${user.email})</p>`;
} catch (error) {
document.getElementById('result').innerHTML =
`<p style="color:red">❌ 创建失败: ${error.message}</p>`;
}
}
</script>
</body>
</html>
✅ 关键点:前端必须设置 credentials: 'include',否则浏览器不会发送 Cookie 或 Authorization 头。
🌐 五、实战场景二:多前端域名授权(白名单机制)
在企业级应用中,可能有多个前端项目(如管理后台、移动端 H5、第三方合作伙伴)需要访问同一个 API。
5.1 需求
- 允许 https://admin.mycompany.com
- 允许 https://mobile.mycompany.com
- 允许 https://partner.example.org(第三方合作伙伴)
- 禁止所有其他来源
5.2 Nginx 实现方案:使用 map + 变量
# 定义允许的来源白名单
map $http_origin $allowed_origin {
default "";
"https://admin.mycompany.com" "https://admin.mycompany.com";
"https://mobile.mycompany.com" "https://mobile.mycompany.com";
"https://partner.example.org" "https://partner.example.org";
}
server {
listen 443 ssl http2;
server_name api.mycompany.com;
ssl_certificate /etc/ssl/certs/api.mycompany.com.crt;
ssl_certificate_key /etc/ssl/private/api.mycompany.com.key;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 动态设置 Allow-Origin
add_header Access-Control-Allow-Origin $allowed_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
# 预检请求处理
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $allowed_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
add_header Access-Control-Max-Age 86400;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
}
✅ 为什么用 map? map 指令在 Nginx 启动时编译,性能极高,避免了 if 的重复判断。同时,它能精确匹配,防止恶意伪造 Origin。
5.3 测试验证
使用 curl 模拟不同来源请求:
curl -H "Origin: https://admin.mycompany.com" \\
-X OPTIONS \\
-H "Access-Control-Request-Method: GET" \\
https://api.mycompany.com/users
# ✅ 应返回:
# Access-Control-Allow-Origin: https://admin.mycompany.com
curl -H "Origin: https://evil-site.com" \\
-X OPTIONS \\
-H "Access-Control-Request-Method: GET" \\
https://api.mycompany.com/users
# ❌ 应返回:
# Access-Control-Allow-Origin: (空)
# 浏览器将拦截真实请求
🛡️ 六、安全最佳实践:避免常见陷阱
❌ 错误做法 1:通配符 * + credentials: true
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Credentials "true";
⚠️ 浏览器会拒绝这种组合! 当 Access-Control-Allow-Credentials: true 时,Access-Control-Allow-Origin 不能为 *,必须是明确的源。
✅ 正确做法:动态匹配 Origin
map $http_origin $cors_origin {
default "";
"~^https://(admin|mobile)\\.mycompany\\.com$" $http_origin;
"~^https://partner\\.example\\.org$" $http_origin;
}
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Credentials "true";
✅ 使用正则匹配 ~^,可灵活支持子域名,如 admin-dev.mycompany.com
❌ 错误做法 2:不处理 OPTIONS 请求
location /api/ {
proxy_pass http://localhost:8080;
# 没有处理 OPTIONS
}
❌ 前端发送 PUT 请求时,浏览器先发 OPTIONS,但 Nginx 未响应,后端返回 405,前端报错。
✅ 正确做法:显式处理 OPTIONS
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "…";
add_header Access-Control-Max-Age 86400;
return 204;
}
❌ 错误做法 3:暴露敏感响应头
add_header Access-Control-Expose-Headers "Set-Cookie, Authorization";
⚠️ 绝对不要暴露 Set-Cookie、Authorization、X-Admin-Token 等敏感头!
✅ 推荐暴露头(仅限前端需要的)
add_header Access-Control-Expose-Headers "X-Total-Count, X-Request-ID, X-RateLimit-Remaining";
📊 七、CORS 请求流程图(Mermaid)
下面是一个完整的 CORS 请求流程图,涵盖简单请求和预检请求两种路径:
#mermaid-svg-DQpURqBoAaO9zQQg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DQpURqBoAaO9zQQg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DQpURqBoAaO9zQQg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DQpURqBoAaO9zQQg .error-icon{fill:#552222;}#mermaid-svg-DQpURqBoAaO9zQQg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DQpURqBoAaO9zQQg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DQpURqBoAaO9zQQg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DQpURqBoAaO9zQQg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DQpURqBoAaO9zQQg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DQpURqBoAaO9zQQg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DQpURqBoAaO9zQQg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DQpURqBoAaO9zQQg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DQpURqBoAaO9zQQg .marker.cross{stroke:#333333;}#mermaid-svg-DQpURqBoAaO9zQQg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DQpURqBoAaO9zQQg p{margin:0;}#mermaid-svg-DQpURqBoAaO9zQQg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-DQpURqBoAaO9zQQg .cluster-label text{fill:#333;}#mermaid-svg-DQpURqBoAaO9zQQg .cluster-label span{color:#333;}#mermaid-svg-DQpURqBoAaO9zQQg .cluster-label span p{background-color:transparent;}#mermaid-svg-DQpURqBoAaO9zQQg .label text,#mermaid-svg-DQpURqBoAaO9zQQg span{fill:#333;color:#333;}#mermaid-svg-DQpURqBoAaO9zQQg .node rect,#mermaid-svg-DQpURqBoAaO9zQQg .node circle,#mermaid-svg-DQpURqBoAaO9zQQg .node ellipse,#mermaid-svg-DQpURqBoAaO9zQQg .node polygon,#mermaid-svg-DQpURqBoAaO9zQQg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DQpURqBoAaO9zQQg .rough-node .label text,#mermaid-svg-DQpURqBoAaO9zQQg .node .label text,#mermaid-svg-DQpURqBoAaO9zQQg .image-shape .label,#mermaid-svg-DQpURqBoAaO9zQQg .icon-shape .label{text-anchor:middle;}#mermaid-svg-DQpURqBoAaO9zQQg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DQpURqBoAaO9zQQg .rough-node .label,#mermaid-svg-DQpURqBoAaO9zQQg .node .label,#mermaid-svg-DQpURqBoAaO9zQQg .image-shape .label,#mermaid-svg-DQpURqBoAaO9zQQg .icon-shape .label{text-align:center;}#mermaid-svg-DQpURqBoAaO9zQQg .node.clickable{cursor:pointer;}#mermaid-svg-DQpURqBoAaO9zQQg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DQpURqBoAaO9zQQg .arrowheadPath{fill:#333333;}#mermaid-svg-DQpURqBoAaO9zQQg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DQpURqBoAaO9zQQg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DQpURqBoAaO9zQQg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DQpURqBoAaO9zQQg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DQpURqBoAaO9zQQg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DQpURqBoAaO9zQQg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DQpURqBoAaO9zQQg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DQpURqBoAaO9zQQg .cluster text{fill:#333;}#mermaid-svg-DQpURqBoAaO9zQQg .cluster span{color:#333;}#mermaid-svg-DQpURqBoAaO9zQQg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DQpURqBoAaO9zQQg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DQpURqBoAaO9zQQg rect.text{fill:none;stroke-width:0;}#mermaid-svg-DQpURqBoAaO9zQQg .icon-shape,#mermaid-svg-DQpURqBoAaO9zQQg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DQpURqBoAaO9zQQg .icon-shape p,#mermaid-svg-DQpURqBoAaO9zQQg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DQpURqBoAaO9zQQg .icon-shape .label rect,#mermaid-svg-DQpURqBoAaO9zQQg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DQpURqBoAaO9zQQg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DQpURqBoAaO9zQQg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DQpURqBoAaO9zQQg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
是
否
是
否
匹配
不匹配
前端发起跨域请求
是否为简单请求?
直接发送请求
浏览器发送 OPTIONS 预检请求
Nginx 接收 OPTIONS 请求
Origin 是否在白名单?
返回 204 + CORS 头
返回 403 或 200 但无 CORS 头
浏览器验证响应头
所有头匹配?
发送真实请求(GET/POST/PUT…)
阻止请求,控制台报错
Nginx 代理到后端
后端返回响应
Nginx 添加 CORS 头
浏览器检查 Access-Control-Allow-Origin
前端接收数据
阻止响应,控制台报错
📌 注意: 该流程图是浏览器行为,Nginx 只是响应者。Nginx 的任务是“正确地响应预检请求”和“正确地添加响应头”。
🧪 八、Java 后端配合:是否需要 CORS 配置?
很多开发者会在 Spring Boot 中使用 @CrossOrigin 注解:
@RestController
@CrossOrigin(origins = "https://frontend.example.com")
@RequestMapping("/api/users")
public class UserController { ... }
❗ 不推荐!
为什么?
| 🧩 重复配置 | 每个 Controller 都要加,维护成本高 |
| 🚫 无法统一管理 | 无法动态切换白名单 |
| 🧱 紧耦合 | 前端域名变更,需重新编译部署 Java 服务 |
| 🐞 容易遗漏 | 新增接口忘记加注解 |
| 🚫 不支持预检 | @CrossOrigin 无法处理 OPTIONS 请求的复杂逻辑 |
✅ 正确做法:只在 Nginx 层统一处理 CORS
Java 后端保持“干净”,只关注业务逻辑。CORS 由边缘网关(Nginx)统一管理,这才是微服务架构的正确姿势。
🌟 最佳实践:Java 后端禁用所有 CORS,全部交给 Nginx 处理。
🔄 九、Nginx + Java 实战:完整部署流程
Step 1:准备 Java 后端(Spring Boot)
@SpringBootApplication
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public List<User> list() {
return List.of(
new User(1L, "Alice", "alice@example.com"),
new User(2L, "Bob", "bob@example.com")
);
}
@PostMapping
public User create(@RequestBody User user) {
user.setId(System.currentTimeMillis());
return user;
}
static class User {
private Long id;
private String name;
private String email;
// 构造器、getter、setter 省略
}
}
application.properties
server.port=8080
logging.level.org.springframework=INFO
Step 2:编译并运行 Java 服务
mvn clean package
java -jar target/api-0.0.1-SNAPSHOT.jar
Step 3:配置 Nginx
编辑 /etc/nginx/sites-available/api-gateway:
server {
listen 443 ssl http2;
server_name api.mycompany.com;
ssl_certificate /etc/ssl/certs/api.mycompany.com.crt;
ssl_certificate_key /etc/ssl/private/api.mycompany.com.key;
# 静态资源(可选)
location /static/ {
alias /var/www/static;
}
# API 代理
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
# 白名单映射
map $http_origin $allowed_origin {
default "";
"https://admin.mycompany.com" "https://admin.mycompany.com";
"https://mobile.mycompany.com" "https://mobile.mycompany.com";
"https://partner.example.org" "https://partner.example.org";
}
# 响应头
add_header Access-Control-Allow-Origin $allowed_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
add_header Access-Control-Expose-Headers "X-Total-Count,X-Request-ID";
# 预检请求
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $allowed_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key";
add_header Access-Control-Max-Age 86400;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
# 错误页
error_page 404 /404.html;
location = /404.html {
internal;
root /var/www/html;
}
}
Step 4:启用站点并重启 Nginx
sudo ln -s /etc/nginx/sites-available/api-gateway /etc/nginx/sites-enabled/
sudo nginx -t # 检查语法
sudo systemctl reload nginx
Step 5:前端测试页面(部署在 admin.mycompany.com)
<!DOCTYPE html>
<html>
<head>
<title>CORS 测试</title>
</head>
<body>
<h2>测试跨域请求</h2>
<button onclick="testGet()">获取用户列表</button>
<button onclick="testPost()">创建用户</button>
<div id="output"></div>
<script>
const API = 'https://api.mycompany.com/api/users';
async function testGet() {
try {
const res = await fetch(API, {
method: 'GET',
credentials: 'include'
});
const data = await res.json();
document.getElementById('output').innerHTML =
`<pre>${JSON.stringify(data, null, 2)}</pre>`;
} catch (err) {
document.getElementById('output').innerHTML =
`<p style="color:red">❌ ${err.message}</p>`;
}
}
async function testPost() {
try {
const res = await fetch(API, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'David', email: 'david@example.com' })
});
const user = await res.json();
document.getElementById('output').innerHTML =
`<p>✅ 创建成功: ${user.name} (${user.email})</p>`;
} catch (err) {
document.getElementById('output').innerHTML =
`<p style="color:red">❌ ${err.message}</p>`;
}
}
</script>
</body>
</html>
✅ 打开 https://admin.mycompany.com/test.html,点击按钮,应能成功调用 API!
🔍 十、调试技巧:如何验证 CORS 是否生效?
方法 1:Chrome DevTools
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- Access-Control-Allow-Methods
🚫 如果看到 Access-Control-Allow-Origin: * 且 credentials: include → 浏览器会报错!
方法 2:curl 模拟 OPTIONS 预检
curl -v \\
-H "Origin: https://admin.mycompany.com" \\
-H "Access-Control-Request-Method: POST" \\
-H "Access-Control-Request-Headers: Authorization, Content-Type" \\
-X OPTIONS \\
https://api.mycompany.com/api/users
查看响应头是否包含:
Access-Control-Allow-Origin: https://admin.mycompany.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
方法 3:使用在线 CORS 测试工具
你可以使用以下工具在线测试你的 API 是否支持 CORS:
- https://www.test-cors.org(可测试任意 URL)
- https://httpbin.org/cors(官方示例)
💡 例如访问:https://www.test-cors.org/?url=https%3A%2F%2Fapi.mycompany.com%2Fapi%2Fusers
✅ 若提示 “CORS is working!”,说明配置成功!
🚀 十一、进阶:使用 Lua + OpenResty 实现动态 CORS(可选)
如果你的架构更复杂(如多租户、动态租户域名),可以使用 OpenResty(Nginx + Lua)实现动态白名单从数据库加载:
— nginx.conf
location /api/ {
access_by_lua_block {
local origin = ngx.var.http_origin
local allowed = require("cors").is_allowed(origin)
if allowed then
ngx.header["Access-Control-Allow-Origin"] = origin
ngx.header["Access-Control-Allow-Credentials"] = "true"
else
ngx.header["Access-Control-Allow-Origin"] = ""
end
}
proxy_pass http://backend;
…
}
— cors.lua
local M = {}
function M.is_allowed(origin)
local whitelist = {
"https://tenant1.example.com",
"https://tenant2.example.com"
}
for _, allowed in ipairs(whitelist) do
if origin == allowed then
return true
end
end
return false
end
return M
💡 这种方式适合大型平台,但对普通项目属于过度设计。Nginx map 已足够。
📈 十二、性能与缓存优化
1. Access-Control-Max-Age 设置建议
| 高频 API(如电商) | 86400(24小时) | 减少预检请求,提升体验 |
| 低频 API(如后台) | 3600(1小时) | 平衡安全与性能 |
| 开发环境 | 0 或 60 | 避免缓存导致调试困难 |
2. 避免重复响应头
不要在多个 location 中重复设置相同头,会导致响应头重复:
add_header Access-Control-Allow-Origin "a.com";
add_header Access-Control-Allow-Origin "b.com"; # ❌ 会被忽略,只保留最后一个
✅ 使用 map 变量是唯一安全方式。
3. 启用 Gzip 压缩(非 CORS,但重要)
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript;
🧭 十三、常见错误排查清单
| No 'Access-Control-Allow-Origin' header | Nginx 没有添加响应头 | 检查 add_header 是否在 location 中,且未被 proxy_hide_header 覆盖 |
| Credentials flag is 'true' but Access-Control-Allow-Origin is '*' | 前端用了 credentials: true,后端用了 * | 改为具体域名,或移除 credentials: true |
| OPTIONS 请求返回 405 | Nginx 未处理 OPTIONS | 添加 if ($request_method = 'OPTIONS') { return 204; } |
| 请求被拦截,但响应头有 CORS | 前端请求头不匹配 | 检查 Access-Control-Allow-Headers 是否包含实际发送的头(如 Authorization) |
| 静态资源跨域失败 | Nginx 静态 location 未配置 CORS | 为 /static/ 也添加相同的 CORS 配置 |
| 生产环境突然失效 | SSL 证书过期或域名变更 | 检查 Nginx 配置中 server_name 和 SSL 证书是否匹配 |
🌍 十四、真实案例:某电商平台的 CORS 架构
📌 案例背景:某电商公司拥有:
- 主站:https://shop.com
- 管理后台:https://admin.shop.com
- 移动 H5:https://m.shop.com
- 第三方物流系统:https://logistics.partner.com
- API 服务:https://api.shop.com(Spring Boot 集群)
✅ 架构设计:
- 所有前端通过 Nginx Ingress 统一接入
- Nginx 配置白名单:shop.com、partner.com 的所有子域
- Java 后端无任何 CORS 注解
- 使用 map + 正则匹配 ~^https://.*\\.shop\\.com$
- 预检缓存 24 小时,降低 90% OPTIONS 请求
- 所有响应头添加 X-Request-ID 便于追踪
📊 效果:跨域请求成功率从 87% → 99.98%,预检请求减少 92%,前端加载速度提升 300ms。
💡 十五、总结:Nginx CORS 配置黄金法则
| ✅ 1. 统一管理 | 所有 CORS 由 Nginx 统一处理,后端不配置 |
| ✅ 2. 白名单优先 | 使用 map 明确允许的源,拒绝 * + credentials |
| ✅ 3. 预检必处理 | 必须显式处理 OPTIONS 请求,返回 204 |
| ✅ 4. 响应头最小化 | 只暴露前端需要的头,避免敏感信息泄露 |
| ✅ 5. 缓存预检 | Access-Control-Max-Age: 86400 提升性能 |
| ✅ 6. 测试验证 | 用 curl + DevTools 验证,不要只靠前端报错 |
| ✅ 7. 安全第一 | 永远不要信任前端传来的 Origin,只信任配置白名单 |
📚 十六、扩展阅读:CORS 与安全
CORS 并非万能。它只是浏览器的访问控制机制,不是后端认证。你仍需:
- 使用 CSRF Token 保护表单提交
- 使用 JWT 或 OAuth2 做身份认证
- 使用 Content-Security-Policy 防止 XSS
- 使用 SameSite=Strict 保护 Cookie
🔗 推荐阅读:
- MDN CORS 文档
- OWASP CORS Cheat Sheet
- CORS 与 CSRF 的区别
✅ 最终建议:你的 Nginx CORS 配置模板(可直接复制)
# 在 http 或 server 块中定义白名单
map $http_origin $cors_origin {
default "";
"~^https://(admin|mobile|www)\\.mycompany\\.com$" $http_origin;
"~^https://partner\\.example\\.org$" $http_origin;
"~^https://dev\\.mycompany\\.com$" $http_origin; # 开发环境
}
server {
listen 443 ssl http2;
server_name api.mycompany.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 响应头
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key,X-Request-ID";
add_header Access-Control-Expose-Headers "X-Total-Count,X-Request-ID,X-RateLimit-Remaining";
# 预检请求
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-API-Key,X-Request-ID";
add_header Access-Control-Max-Age 86400;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
}
🎉 结语:跨域不是问题,是架构的试金石
跨域问题,本质上是架构设计是否清晰的体现。一个优秀的系统,应该:
- 前后端分离,职责分明
- API 网关统一管理安全策略
- 服务无状态,可水平扩展
- 配置可追踪、可审计、可回滚
Nginx 的 CORS 配置,正是这种“边缘网关”思想的完美体现。它不侵入业务,却守护安全;它不修改代码,却提升体验。
当你下次看到前端报错 CORS policy 时,不要急着在 Spring Boot 里加注解。 打开 Nginx 配置文件,添加一行 map,世界就清净了。 🌿
💬 “真正的工程之美,不在于代码有多炫,而在于边界有多清晰。”
📌 附录:常用 Origin 正则表达式参考
| 只允许 https://example.com | "https://example.com" |
| 允许所有子域名 | "~^https://.*\\.example\\.com$" |
| 允许多个域名 | `"~^https://(example1 |
| 允许 http 和 https | `"~^(https?😕/)(admin |
| 开发环境允许 localhost | "~^https?://localhost(:[0-9]+)?$" |
✅ 你已经掌握了:
- CORS 的原理与工作流程 🧠
- Nginx 的完整 CORS 配置方案 🛠️
- Java 后端无需修改的架构设计 🚫
- 安全白名单的最佳实践 🔐
- 预检请求的处理技巧 ⚡
- 调试与验证方法 🔍
- 高可用、高性能的生产部署建议 🏗️
现在,你已具备在企业级项目中独立解决跨域问题的能力。 下一个项目,你就是那个“让前端不再报错”的人。 💪
🌟 记住: 不要让跨域问题成为你团队的“技术债”。 用 Nginx,一劳永逸。
📬 如有疑问,欢迎在评论区留言交流。 感谢阅读,愿你的 API 永远畅通无阻 🚀
🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨




