欢迎光临
我们一直在努力

Nginx- 解决跨域问题:Nginx 的 CORS 跨域配置实战

在这里插入图片描述

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕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 安全的基石之一。它规定:只有当协议、域名、端口三者完全相同时,页面脚本才能访问另一个页面的资源。

例如:

URL是否同源原因
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

  • 打开 Network 面板
  • 发起请求
  • 查看响应头是否包含:
    • 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 永远畅通无阻 🚀


    🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨

    赞(0)
    未经允许不得转载:171主机测评 » Nginx- 解决跨域问题:Nginx 的 CORS 跨域配置实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址