Nginx解決CORS跨域配置的指南
在現(xiàn)代 Web 開發(fā)中,前后端分離架構已成為主流。前端應用通常部署在獨立的域名或端口上(如 https://frontend.example.com),而后端 API 服務則運行在另一個域(如 https://api.example.com)。當瀏覽器加載前端頁面并嘗試通過 JavaScript 調用跨域 API 時,會觸發(fā) 同源策略(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 后端實戰(zhàn)案例,構建一個安全、靈活、可擴展的跨域解決方案。

一、CORS 是什么?為什么需要它?
1.1 同源策略的誕生
瀏覽器的同源策略是 Web 安全的基石之一。它規(guī)定:只有當協(xié)議、域名、端口三者完全相同時,頁面腳本才能訪問另一個頁面的資源。
例如:
| URL | 是否同源 | 原因 |
|---|---|---|
https://example.com | ? 同源 | — |
https://example.com/api | ? 同源 | 路徑不同不影響 |
http://example.com | ? 不同源 | 協(xié)議不同(http vs https) |
https://api.example.com | ? 不同源 | 子域名不同 |
https://example.com:8080 | ? 不同源 | 端口不同 |
這種限制雖然保護了用戶免受 XSS、CSRF 等攻擊,但也給現(xiàn)代應用開發(fā)帶來了挑戰(zhàn) —— 前后端分離架構下,前端與后端必然不在同一源。
1.2 CORS 的作用
CORS 是 W3C 制定的標準,允許服務器明確聲明哪些外部源可以訪問其資源。它通過在 HTTP 響應頭中添加特定字段,告知瀏覽器:“你可以信任這個來源,允許它發(fā)起跨域請求”。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)
不滿足上述條件的請求,瀏覽器會先發(fā)送一個 OPTIONS 預檢請求(Preflight Request),詢問服務器是否允許該跨域請求。只有預檢通過后,才會發(fā)送真實請求。
常見觸發(fā)場景:
- 使用
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
只有當這些頭信息匹配時,瀏覽器才允許后續(xù)的 PUT 請求。
二、Nginx 作為 CORS 網(wǎng)關:為什么選它?
在微服務架構中,前端通常通過 Nginx 作為統(tǒng)一入口,代理多個后端服務。Nginx 擁有以下優(yōu)勢:
| 優(yōu)勢 | 說明 |
|---|---|
| 性能極高 | 基于事件驅動,單進程處理高并發(fā),內存占用低 |
| 配置靈活 | 支持基于 location、if、map 等精細控制 |
| 反向代理 | 可統(tǒng)一代理多個后端服務,集中處理 CORS |
| 安全可控 | 可在入口層統(tǒng)一攔截非法請求,減少后端負擔 |
| 無依賴 | 不需修改后端代碼,實現(xiàn)零侵入式跨域管理 |
最佳實踐:將 CORS 配置放在 Nginx 層,而非后端(如 Spring Boot)。這樣可以避免每個服務重復配置,提升維護性。
三、Nginx CORS 配置核心指令詳解
Nginx 通過 add_header 指令設置響應頭,從而實現(xiàn) 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 請求也轉發(fā)給后端,而大多數(shù) Java 服務(如 Spring Boot)默認不處理 OPTIONS,導致返回 405 或 500,前端報錯。
四、實戰(zhàn)場景一:單域名前端 + 單后端服務
假設我們有:
- 前端:
https://app.mycompany.com(Nginx 靜態(tài)站點) - 后端:
http://localhost:8080(Java Spring Boot 應用) - Nginx 監(jiān)聽 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;
# 靜態(tài)資源
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()">創(chuàng)建用戶</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>? 創(chuàng)建成功: ${user.name} (${user.email})</p>`;
} catch (error) {
document.getElementById('result').innerHTML =
`<p style="color:red">? 創(chuàng)建失敗: ${error.message}</p>`;
}
}
</script>
</body>
</html>關鍵點:前端必須設置 credentials: 'include',否則瀏覽器不會發(fā)送 Cookie 或 Authorization 頭。
五、實戰(zhàn)場景二:多前端域名授權(白名單機制)
在企業(yè)級應用中,可能有多個前端項目(如管理后臺、移動端 H5、第三方合作伙伴)需要訪問同一個 API。
5.1 需求
- 允許
https://admin.mycompany.com - 允許
https://mobile.mycompany.com - 允許
https://partner.example.org(第三方合作伙伴) - 禁止所有其他來源
5.2 Nginx 實現(xiàn)方案:使用 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;
# 動態(tài)設置 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 不能為 *,必須是明確的源。
正確做法:動態(tài)匹配 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
}
前端發(fā)送 PUT 請求時,瀏覽器先發(fā) 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 請求流程圖,涵蓋簡單請求和預檢請求兩種路徑:

注意: 該流程圖是瀏覽器行為,Nginx 只是響應者。Nginx 的任務是“正確地響應預檢請求”和“正確地添加響應頭”。
八、Java 后端配合:是否需要 CORS 配置?
很多開發(fā)者會在 Spring Boot 中使用 @CrossOrigin 注解:
@RestController
@CrossOrigin(origins = "https://frontend.example.com")
@RequestMapping("/api/users")
public class UserController { ... }不推薦!
為什么?
| 問題 | 說明 |
|---|---|
| 重復配置 | 每個 Controller 都要加,維護成本高 |
| 無法統(tǒng)一管理 | 無法動態(tài)切換白名單 |
| 緊耦合 | 前端域名變更,需重新編譯部署 Java 服務 |
| 容易遺漏 | 新增接口忘記加注解 |
| 不支持預檢 | @CrossOrigin 無法處理 OPTIONS 請求的復雜邏輯 |
正確做法:只在 Nginx 層統(tǒng)一處理 CORS
Java 后端保持“干凈”,只關注業(yè)務邏輯。CORS 由邊緣網(wǎng)關(Nginx)統(tǒng)一管理,這才是微服務架構的正確姿勢。
最佳實踐:Java 后端禁用所有 CORS,全部交給 Nginx 處理。
九、Nginx + Java 實戰(zhàn):完整部署流程
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;
# 靜態(tài)資源(可選)
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()">創(chuàng)建用戶</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>? 創(chuàng)建成功: ${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 面板
- 發(fā)起請求
- 查看響應頭是否包含:
Access-Control-Allow-OriginAccess-Control-Allow-CredentialsAccess-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
十一、進階:使用 Lua + OpenResty 實現(xiàn)動態(tài) CORS(可選)
如果你的架構更復雜(如多租戶、動態(tài)租戶域名),可以使用 OpenResty(Nginx + Lua)實現(xiàn)動態(tài)白名單從數(shù)據(jù)庫加載:
-- 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 已足夠。
十二、性能與緩存優(yōu)化
1.Access-Control-Max-Age設置建議
| 場景 | 建議值 | 說明 |
|---|---|---|
| 高頻 API(如電商) | 86400(24小時) | 減少預檢請求,提升體驗 |
| 低頻 API(如后臺) | 3600(1小時) | 平衡安全與性能 |
| 開發(fā)環(huán)境 | 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 是否包含實際發(fā)送的頭(如 Authorization) |
| 靜態(tài)資源跨域失敗 | Nginx 靜態(tài) location 未配置 CORS | 為 /static/ 也添加相同的 CORS 配置 |
| 生產環(huán)境突然失效 | SSL 證書過期或域名變更 | 檢查 Nginx 配置中 server_name 和 SSL 證書是否匹配 |
總結:Nginx CORS 配置黃金法則
| 法則 | 說明 |
|---|---|
| 1. 統(tǒng)一管理 | 所有 CORS 由 Nginx 統(tǒng)一處理,后端不配置 |
| 2. 白名單優(yōu)先 | 使用 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
最終建議:你的 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; # 開發(fā)環(huán)境
}
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;
}
}
}
結語:跨域不是問題,是架構的試金石
跨域問題,本質上是架構設計是否清晰的體現(xiàn)。一個優(yōu)秀的系統(tǒng),應該:
- 前后端分離,職責分明
- API 網(wǎng)關統(tǒng)一管理安全策略
- 服務無狀態(tài),可水平擴展
- 配置可追蹤、可審計、可回滾
Nginx 的 CORS 配置,正是這種“邊緣網(wǎng)關”思想的完美體現(xiàn)。它不侵入業(yè)務,卻守護安全;它不修改代碼,卻提升體驗。
當你下次看到前端報錯 CORS policy 時,不要急著在 Spring Boot 里加注解。
打開 Nginx 配置文件,添加一行 map,世界就清凈了。
“真正的工程之美,不在于代碼有多炫,而在于邊界有多清晰。”
附錄:常用 Origin 正則表達式參考
| 需求 | 正則表達式 |
|---|---|
只允許 https://example.com | "https://example.com" |
| 允許所有子域名 | "~^https://.*\.example\.com$" |
| 允許多個域名 | `"~^https://(example1 |
| 允許 http 和 https | `"~^(https???/)(admin |
| 開發(fā)環(huán)境允許 localhost | "~^https?://localhost(:[0-9]+)?$" |
你已經(jīng)掌握了:
- CORS 的原理與工作流程
- Nginx 的完整 CORS 配置方案
- Java 后端無需修改的架構設計
- 安全白名單的最佳實踐
- 預檢請求的處理技巧
- 調試與驗證方法
- 高可用、高性能的生產部署建議
現(xiàn)在,你已具備在企業(yè)級項目中獨立解決跨域問題的能力。下一個項目,你就是那個“讓前端不再報錯”的人。
以上就是Nginx解決CORS跨域配置的指南的詳細內容,更多關于Nginx解決CORS跨域的資料請關注腳本之家其它相關文章!
相關文章
使用Kubernetes部署Springboot或Nginx的詳細教程
這篇文章主要介紹了用Kubernetes部署Springboot或Nginx的詳細教程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
nginx使用replace-filter-nginx-module實現(xiàn)內容替換的示例
本篇文章主要介紹了nginx使用replace-filter-nginx-module實現(xiàn)內容替換的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03
nginx?攔截指定ip訪問指定url的實現(xiàn)示例
本文主要介紹了nginx?攔截指定ip訪問指定url的實現(xiàn)示例,使用$http_x_forwarded_for變量來獲取客戶端的真實IP地址,感興趣的可以了解一下2024-12-12
使用ngxtop實時監(jiān)控Nginx日志文件的示例代碼
在Nginx日志分析領域,ngxtop是一款強大的實時監(jiān)控工具,它能夠即時解析Nginx的訪問日志文件,提供直觀、可定制的實時統(tǒng)計信息,幫助管理員更好地了解服務器的運行狀況和Web流量,本文給大家介紹使用ngxtop實時監(jiān)控Nginx日志文件的示例代碼,需要的朋友可以參考下2024-01-01
nginx中使用nginx-http-concat模塊合并靜態(tài)資源文件
這篇文章主要介紹了nginx中使用nginx-http-concat模塊合并靜態(tài)資源文件,用以加速網(wǎng)站的CSS、JS等靜態(tài)資源載入速度,需要的朋友可以參考下2014-06-06

