Nginx 跨域设置详解与实战 – wiki基地


Nginx 跨域设置详解与实战

1. 引言:跨域问题——现代 Web 开发的痛点

在现代 Web 应用架构中,前端和后端往往部署在不同的服务器、不同的端口,甚至不同的域名下。这种分离带来了灵活性、可扩展性和专业化分工,但也引入了一个核心安全限制:同源策略(Same-Origin Policy)。

同源策略是浏览器的一个重要安全机制,它限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这里的“源”(Origin)由协议(protocol)、域名(domain)和端口(port)三部分组成。只有当这三者完全一致时,才被认为是“同源”。

当一个 Web 页面尝试去请求另一个不同源的资源(例如通过 AJAX/Fetch API 请求后端 API),浏览器会阻止这个请求,这就是所谓的“跨域问题”。

虽然同源策略保障了用户安全(防止恶意网站读取其他网站的敏感数据),但它也给合法的跨源数据交互带来了障碍。为了解决这个问题,出现了一系列技术方案,其中最标准、最推荐的一种是 跨源资源共享(Cross-Origin Resource Sharing, CORS)

CORS 是一种基于 HTTP 头部的机制,它允许服务器声明哪些源被授权访问其资源。浏览器在检测到跨域请求时,会检查响应头中是否包含允许该请求源的 CORS 头部信息,如果允许,则继续处理响应;否则,拒绝响应。

在实际部署中,我们通常不会直接在应用服务(如 Node.js、Java Spring Boot、Python Django/Flask 等)中处理 CORS 逻辑。原因在于:
1. 逻辑重复: 每个服务都需要实现一套 CORS 处理逻辑。
2. 性能开销: 应用服务主要关注业务逻辑,处理 CORS 会分散其资源。
3. 管理复杂: 随着服务数量增加,统一管理 CORS 策略变得困难。

此时,作为高性能反向代理和静态文件服务器的 Nginx 成为了处理 CORS 的理想选择。Nginx 位于客户端和后端服务之间,可以集中、高效地为所有或特定的后端服务配置 CORS 策略,无需修改后端应用代码。

本文将详细介绍 CORS 的原理,并深入探讨如何在 Nginx 中进行 CORS 设置,包括基本配置、高级配置以及多种实战场景,帮助您彻底理解并解决跨域问题。

2. 理解 CORS 工作原理

在 Nginx 配置 CORS 之前,非常有必要深入理解 CORS 的工作原理,特别是“简单请求”和“预检请求”的概念。

2.1 简单请求 (Simple Requests)

某些类型的跨域请求被称为“简单请求”。它们不需要在发送实际请求之前进行预检。满足以下所有条件的请求是简单请求:

  1. 请求方法: 只能是 GET, HEAD, POST 之一。
  2. HTTP 头部: 除了 CORS 规范允许的少数几个头部(Accept, Accept-Language, Content-Language, Content-Type, Range),以及 Access-Control-Allow-Origin 头部,且 Content-Type 只能是 application/x-www-form-urlencoded, multipart/form-data, text/plain 三者之一。
  3. 无自定义头部。

对于简单请求,浏览器会直接发送实际的跨域请求,并在请求头中带上 Origin 头部,表明请求来源的源。

http
GET /resource HTTP/1.1
Host: server.com
Origin: http://client.com

服务器接收到请求后,如果允许该源访问资源,则在响应头中包含 Access-Control-Allow-Origin 头部,其值要么是请求的 Origin 值,要么是 * (表示允许所有源)。

http
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: http://client.com

如果响应中包含正确的 Access-Control-Allow-Origin 头部且值匹配,浏览器就会将响应数据暴露给前端 JavaScript 代码。否则,即使服务器返回了数据,浏览器也会拒绝访问并抛出跨域错误。

2.2 预检请求 (Preflight Requests)

对于不符合“简单请求”条件的跨域请求(例如使用了 PUT, DELETE 方法,或发送了自定义头部,或 Content-Typeapplication/json 等),浏览器会在发送实际请求之前,自动发起一个 OPTIONS 请求到目标服务器,这个 OPTIONS 请求就是“预检请求”。

预检请求的目的是询问服务器,当前这个跨域请求是否安全且允许发送。预检请求会携带一些特殊的头部,告知服务器实际请求的方法、将要使用的头部等信息。

http
OPTIONS /resource HTTP/1.1
Host: server.com
Origin: http://client.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header, Content-Type

服务器收到 OPTIONS 请求后,需要根据这些信息判断是否允许后续的实际请求。如果允许,服务器会在 OPTIONS 请求的响应头中包含一系列 Access-Control-Allow-* 头部,表明允许哪些源、哪些方法、哪些头部等。

http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://client.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-Custom-Header, Content-Type
Access-Control-Max-Age: 86400 # 预检请求结果的缓存时间

如果预检请求成功(服务器返回了允许的头部),浏览器才会发送实际的请求。如果预检请求失败(例如服务器返回 403 错误,或者响应头中没有包含必需的 CORS 头部),浏览器会终止后续的实际请求并抛出跨域错误。

Access-Control-Max-Age 头部很重要,它指定了预检请求的结果可以被缓存多长时间(单位:秒)。在这段缓存时间内,如果浏览器再次发起相同的跨域请求,将不再发送 OPTIONS 预检请求,而是直接发送实际请求,从而提高了性能。

总结: Nginx 处理 CORS,主要是通过配置 add_header 指令,在响应中添加正确的 Access-Control-Allow-* 头部。对于预检请求 (OPTIONS),需要特别处理,确保它能正确响应 CORS 头部,并且通常会直接返回成功状态码 (如 204 No Content) 而不将请求传递给后端应用服务。

3. Nginx CORS 基本配置

在 Nginx 中配置 CORS,主要是在 http, server, location 等块中使用 add_header 指令来添加响应头部。最常见的位置是在需要启用 CORS 的 location 块中。

3.1 允许所有源 (*)

最简单(但不总是最安全)的配置是允许所有源访问资源。

“`nginx
server {
listen 80;
server_name api.example.com;

location /api/ {
    # 允许所有来源进行跨域访问
    add_header 'Access-Control-Allow-Origin' '*';

    # 允许的跨域请求方法
    # 注意:对于非简单请求,浏览器会先发送 OPTIONS 请求,所以通常需要包含 OPTIONS
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';

    # 允许的自定义请求头部
    # 根据你的接口实际情况添加,常用头部通常无需在此列出(如 Content-Type, Accept等)
    # 但如果你的前端使用了非标准头部,必须在此列出
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';

    # 可选:指定预检请求的缓存时间(单位:秒)。在此时间内,对于同一URL的同类型请求,浏览器将不再发送OPTIONS预检请求
    # add_header 'Access-Control-Max-Age' 1728000; # 20天

    # 处理 OPTIONS 预检请求
    # 如果请求方法是 OPTIONS,直接返回 204 No Content 状态码,无需将请求转发给后端
    # 务必确保 OPTIONS 请求也包含了上述 CORS 头部
    if ($request_method = 'OPTIONS') {
        # 再次添加 CORS 头部,确保 OPTIONS 请求响应也带上
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        # add_header 'Access-Control-Max-Age' 1728000;

        # 返回 204 No Content 表示成功,且响应体为空
        # 对于预检请求,浏览器只关心响应头部和状态码
        return 204;
    }

    # 其他后端代理配置,例如:
    # proxy_pass http://backend_service;
    # proxy_set_header Host $host;
    # proxy_set_header X-Real-IP $remote_addr;
    # ...
}

# ... 其他 location 或配置

}
“`

解释:

  • add_header 'Access-Control-Allow-Origin' '*': 这是最核心的头部,告诉浏览器允许任何源(*)访问。
  • add_header 'Access-Control-Allow-Methods' '...': 告诉浏览器允许哪些 HTTP 方法进行跨域访问。通常需要包含你的 API 支持的方法(如 GET, POST, PUT, DELETE)以及 OPTIONS (用于预检请求)。
  • add_header 'Access-Control-Allow-Headers' '...': 告诉浏览器允许哪些请求头部。列出你的前端代码在进行跨域请求时会发送的自定义头部。常见的如 Content-Type, X-Requested-With 等通常不需要列出(属于简单请求头部),但如果前端设置了 X-Custom-Header 等自定义头部,则必须在此列出。
  • add_header 'Access-Control-Max-Age' ...: 缓存预检请求结果,可选但推荐。
  • if ($request_method = 'OPTIONS') { ... return 204; }: 这是处理预检请求的关键。当接收到 OPTIONS 请求时,我们直接在这里返回成功(204 No Content)并附带 CORS 头部,不再将请求转发给后端。注意: 需要在 if 块内部也添加 CORS 头部,以确保 OPTIONS 请求的响应也包含这些头部。

注意: 使用 * 允许所有源是最宽松的设置,适用于公共 API 或开发/测试环境。但在生产环境中,如果你的 API 并非完全公开,强烈建议限制允许的源。

4. Nginx CORS 高级配置

在生产环境中,出于安全考虑,通常需要限制允许进行跨域访问的源。同时,处理带有凭证(Credentials,如 Cookies, HTTP 认证或客户端 SSL 证书)的跨域请求需要特别注意。

4.1 允许指定的源

当不允许所有源时,你需要将 Access-Control-Allow-Origin 的值设置为允许的特定源。

nginx
add_header 'Access-Control-Allow-Origin' 'http://allowed-domain.com';

但如果你需要允许多个特定的源呢?直接写多个 add_header 是无效的,Nginx 会以后面的覆盖前面的。CORS 规范也要求 Access-Control-Allow-Origin 在响应中只能出现一次,且值为单个源或 *(在不允许凭证时)。

为了解决允许多个特定源的问题,我们需要根据请求的 Origin 头部动态设置 Access-Control-Allow-Origin。Nginx 的 map 指令非常适合处理这种情况。

“`nginx

在 http 块中定义 map

map $http_origin 变量来根据请求的 Origin 头部动态设置一个变量

如果请求的 Origin 匹配列表中的某个模式,则将 $cors_origin 设置为 $http_origin 的值

如果不匹配,则设置为默认值 “” (空字符串)

map $http_origin $cors_origin {
default “”; # 默认不允许任何不匹配的源

# 使用正则表达式匹配允许的源
# ~* 表示不区分大小写的正则匹配
"~*^https?://(www\.)?domain1\.com$" "$http_origin";
"~*^https?://(www\.)?domain2\.org$" "$http_origin";
"~*^http://localhost(:[0-9]+)?$" "$http_origin"; # 方便本地开发调试

}

server {
listen 80;
server_name api.example.com;

location /api/ {
    # 检查 $cors_origin 变量是否非空
    # 如果非空,说明请求的 Origin 在允许列表中,才添加 CORS 头部
    # 这样,对于不允许的源,Nginx 不会添加 CORS 头部,浏览器会拒绝请求

    if ($request_method = 'OPTIONS') {
        # OPTIONS 预检请求的处理
        # 只有当 $cors_origin 非空时才返回 CORS 头部和 204
        if ($cors_origin != "") {
            add_header 'Access-Control-Allow-Origin' "$cors_origin";
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            add_header 'Access-Control-Max-Age' 1728000; # 缓存20天

            # 如果需要支持跨域携带 Cookie 等凭证,必须设置 Access-Control-Allow-Credentials 为 true
            # 并且此时 Access-Control-Allow-Origin 不能是 '*'
            add_header 'Access-Control-Allow-Credentials' 'true';

            return 204;
        }
        # 如果 $cors_origin 为空 (即不允许的源),则不做处理,让后续配置决定如何响应
        # 通常,如果到这里 $cors_origin 仍为空,并且没有其他配置处理OPTIONS,Nginx会返回405 Not Allowed或403 Forbidden,浏览器会报错。
        # 或者你可以在此处添加一个 return 403;
        # return 403; # 明确拒绝不允许的源的 OPTIONS 请求
    }

    # 实际请求的处理 (GET, POST, PUT, DELETE 等)
    # 只有当 $cors_origin 非空时才添加 CORS 头部
    if ($cors_origin != "") {
        add_header 'Access-Control-Allow-Origin' "$cors_origin";
        # Access-Control-Allow-Methods, Access-Control-Allow-Headers 在实际请求中不是必需的,
        # 因为它们主要用于预检请求。但为了保持一致性或兼容性,也可以添加。
        # add_header 'Access-Control-Methods' 'GET, POST, PUT, DELETE';
        # add_header 'Access-Control-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';

        # 如果需要支持跨域携带 Cookie 等凭证,必须设置 Access-Control-Allow-Credentials 为 true
        # 并且此时 Access-Control-Allow-Origin 不能是 '*'
        add_header 'Access-Control-Allow-Credentials' 'true';

        # 如果需要让前端JS能够访问后端响应中的自定义头部,在此处列出
        # add_header 'Access-Control-Expose-Headers' 'X-Backend-Header1, X-Backend-Header2';
    }


    # ... 其他后端代理配置
    # proxy_pass http://backend_service;
    # proxy_set_header Host $host;
    # proxy_set_header X-Real-IP $remote_addr;
    # ...
}

# ... 其他 location 或配置

}
“`

解释:

  • map $http_origin $cors_origin { ... }: 在 http 块中(server 块外部)定义一个映射。$http_origin 是 Nginx 内置变量,获取请求的 Origin 头部的值。$cors_origin 是我们自定义的变量,用于存储允许的源。
  • default "";: 如果 Origin 头部不匹配任何已定义的规则,$cors_origin 的值将被设置为 ""
  • "~*^https?://..." "$http_origin";: 使用正则表达式匹配允许的源。如果匹配,将 $cors_origin 设置为当前的 $http_origin 值。这样做的原因是,如果允许携带凭证 (Access-Control-Allow-Credentials: true),Access-Control-Allow-Origin 必须设置为请求的实际源,而不是 *
  • location 块内,通过 if ($cors_origin != "") { ... } 条件判断,只对允许的源添加 CORS 头部。
  • 重要: OPTIONS 请求和实际请求需要分别处理。在 OPTIONS 块内,确保返回所有必要的 Access-Control-Allow-* 头部,并使用 return 204; 终止请求处理链。对于实际请求,只需添加 Access-Control-Allow-Origin 和可选的 Access-Control-Allow-CredentialsAccess-Control-Expose-Headers

注意: 正则表达式的编写需要小心,确保它们准确匹配你允许的源。例如,^https?://(www\.)?domain1\.com$ 匹配 http://domain1.comhttps://domain1.com 以及它们的 www 子域。

4.2 处理凭证 (Credentials)

当跨域请求需要携带 Cookie、HTTP 认证信息或客户端 SSL 证书时,前端需要在请求中设置 withCredentials = true

例如使用 fetch:

javascript
fetch('http://api.example.com/resource', {
credentials: 'include' // 或 'same-origin', 'omit'
// ... other options
})

或者使用 XMLHttpRequest:

javascript
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'http://api.example.com/resource', true);
// ... send request

为了使带有 withCredentials = true 的跨域请求成功,服务器响应必须同时满足以下两个条件:

  1. 响应头包含 Access-Control-Allow-Credentials: true
  2. 响应头中的 Access-Control-Allow-Origin 不能*,必须是请求的实际源。

这正是上面使用 map 动态设置 Access-Control-Allow-Origin 的一个重要原因。

在 Nginx 配置中,只需在需要支持凭证的 location 块中(包括 OPTIONS 处理块和实际请求处理块)添加:

nginx
add_header 'Access-Control-Allow-Credentials' 'true';

并确保 Access-Control-Allow-Origin 不是 *,而是通过 $cors_origin 变量动态设置的请求源。

4.3 暴露自定义响应头部

默认情况下,前端 JavaScript 只能访问响应的少数几个头部(如 Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma)。如果你的后端响应包含自定义的头部(例如分页信息的 X-Pagination-Total-Count),并且希望前端能够读取它们,你需要在 CORS 响应头中通过 Access-Control-Expose-Headers 明确列出这些头部。

nginx
add_header 'Access-Control-Expose-Headers' 'X-Pagination-Total-Count, X-Custom-Response-Header';

这个头部只需要在实际请求的响应中添加,不需要在 OPTIONS 请求的响应中添加。

5. Nginx CORS 实战示例

下面结合实际场景给出几个完整的 Nginx 配置示例。

5.1 示例 1:简单 API 服务器(允许所有源,无凭证)

适用于公共 API 或内部系统中不需要区分来源且不依赖 Cookie/Session 的情况。

“`nginx
server {
listen 80;
server_name api.public.com;

location /api/v1/ {
    # 允许所有源
    add_header 'Access-Control-Allow-Origin' '*';

    # 允许常用方法,包含 OPTIONS
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';

    # 允许常用头部及一些自定义头部
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';

    # 可选:缓存预检请求结果
    add_header 'Access-Control-Max-Age' 3600; # 缓存1小时

    # 处理 OPTIONS 预检请求
    if ($request_method = 'OPTIONS') {
        # 再次添加 CORS 头部
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
        add_header 'Access-Control-Max-Age' 3600;
        # add_header 'Access-Control-Allow-Credentials' 'false'; # 不允许凭证时可以明确设置

        # 返回 204 No Content 状态码
        # add_header 'Content-Type' 'text/plain charset=UTF-8'; # 可选,确保返回Content-Type
        # add_header 'Content-Length' 0; # 可选,确保响应体为空
        return 204;
    }

    # 实际业务请求转发到后端服务
    proxy_pass http://backend_api_service;
    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;
}

}
“`

5.2 示例 2:API 服务器(允许特定两个源,支持凭证)

适用于前后端分离部署,前端部署在特定域名下,后端API需要支持用户登录状态(依赖 Cookie/Session)。

“`nginx

在 http 块中定义 map,位于 server 块外部

http {
# … 其他 http 配置

map $http_origin $cors_origin {
    default ""; # 默认不允许

    # 允许的前端源,注意协议和端口
    "~*^https?://(www\.)?frontend1\.com$" "$http_origin";
    "~*^https?://app\.frontend2\.org(:[0-9]+)?$" "$http_origin"; # 允许特定端口
    "~*^http://localhost(:[0-9]+)?$" "$http_origin"; # 本地开发允许
}

server {
    listen 80;
    server_name api.example.com;

    location /api/v2/ {
        # 处理 OPTIONS 预检请求
        if ($request_method = 'OPTIONS') {
            # 只有当请求源被允许时才添加 CORS 头部并返回 204
            if ($cors_origin != "") {
                add_header 'Access-Control-Allow-Origin' "$cors_origin";
                add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
                add_header 'Access-Control-Max-Age' 1728000; # 缓存预检请求20天
                add_header 'Access-Control-Allow-Credentials' 'true'; # 允许携带凭证

                return 204;
            }
             # 如果请求源不在允许列表中,可以返回 403 Forbidden
             return 403;
        }

        # 实际业务请求处理
        # 只有当请求源被允许时才添加 CORS 头部
        if ($cors_origin != "") {
            add_header 'Access-Control-Allow-Origin' "$cors_origin";
            add_header 'Access-Control-Allow-Credentials' 'true'; # 允许携带凭证

            # 可选:如果后端响应有自定义头部需要前端访问
            # add_header 'Access-Control-Expose-Headers' 'X-Custom-Header, X-Another-Header';
        } else {
            # 对于不允许的源的实际请求,也返回 403
            return 403;
        }

        # 转发到后端服务
        proxy_pass http://backend_api_service;
        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;
    }
}

}
“`

5.3 示例 3:静态文件服务器(允许特定源访问字体、图片等)

有时前端需要加载跨域的字体文件(如 WOFF, WOFF2)或图片、JSON 等资源。特别是字体文件,如果不配置 CORS,浏览器可能会阻止跨域加载。

“`nginx

在 http 块中定义 map,如果需要允许特定源访问静态资源

可以复用上面的 map $http_origin $cors_origin {…}

server {
listen 80;
server_name static.example.com;
root /var/www/static; # 静态文件根目录

# 允许所有源访问静态资源 (如果需要完全公开)
# location / {
#     add_header 'Access-Control-Allow-Origin' '*';
#     # 对于静态资源,通常只需要 GET/HEAD 方法,并且不需要处理 OPTIONS
#     # 因为静态资源的 GET/HEAD 请求通常是简单请求
#     # 但如果通过 fetch 等方法加上自定义头部,则可能变为预检请求,此时需要考虑OPTIONS
#     # 简单起见,如果允许所有源,可以不特殊处理 OPTIONS
#     try_files $uri $uri/ =404;
# }

# 更安全的做法:只允许特定源访问静态资源 (需要map定义 $cors_origin)
location / {
    # 如果使用了 map $http_origin $cors_origin
    if ($cors_origin != "") {
         add_header 'Access-Control-Allow-Origin' "$cors_origin";
         # 对于静态文件,不需要凭证,Allow-Credentials 设置为 false 或不设置
         # add_header 'Access-Control-Allow-Credentials' 'false';
    }
    # 如果请求方法是 OPTIONS,并且源被允许,返回 204
    if ($request_method = 'OPTIONS') {
         if ($cors_origin != "") {
            add_header 'Access-Control-Allow-Origin' "$cors_origin";
            add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS'; # 静态文件常用方法
            add_header 'Access-Control-Allow-Headers' 'Content-Type'; # 静态文件常用头部
            add_header 'Access-Control-Max-Age' 86400; # 缓存1天
            return 204;
         }
         return 403; # 拒绝不允许的源的 OPTIONS
    }

    try_files $uri $uri/ =404;
}

# 如果只需要对特定类型的静态文件启用 CORS,可以更精细控制
# 例如,只对字体文件和某些数据文件启用
location ~* \.(eot|ttf|woff|woff2|otf|json)$ {
    # 同样使用 map $http_origin $cors_origin
    if ($cors_origin != "") {
         add_header 'Access-Control-Allow-Origin' "$cors_origin";
    }
     if ($request_method = 'OPTIONS') {
         if ($cors_origin != "") {
            add_header 'Access-Control-Allow-Origin' "$cors_origin";
            add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type';
            add_header 'Access-Control-Max-Age' 86400;
            return 204;
         }
         return 403;
    }
    # 其他处理静态文件的指令,如 expires 等
    expires 30d;
    log_not_found off;
}

}
“`

5.4 示例 4:在一个 Server 块内处理多个 Location 的 CORS

同一个 server 块内可能代理多个不同的后端服务路径,它们的 CORS 策略可能不同。

“`nginx

在 http 块中定义 map

http {
map $http_origin $api1_cors_origin {
default “”;
“~*^https?://frontend1.com$” “$http_origin”;
}

map $http_origin $api2_cors_origin {
    default "";
    "~*^https?://(www\.)?frontend2\.org$" "$http_origin";
    "~*^https?://frontend3\.net$" "$http_origin";
}

server {
    listen 80;
    server_name api.example.com;

    # /api/v1/ 路径使用 api1_cors_origin 策略
    location /api/v1/ {
        if ($request_method = 'OPTIONS') {
            if ($api1_cors_origin != "") {
                add_header 'Access-Control-Allow-Origin' "$api1_cors_origin";
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'Content-Type';
                add_header 'Access-Control-Max-Age' 86400;
                add_header 'Access-Control-Allow-Credentials' 'true';
                return 204;
            }
            return 403;
        }

        if ($api1_cors_origin != "") {
            add_header 'Access-Control-Allow-Origin' "$api1_cors_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
        } else {
            return 403;
        }

        proxy_pass http://backend_api1;
        # ... 其他代理配置
    }

    # /api/v2/ 路径使用 api2_cors_origin 策略
    location /api/v2/ {
        if ($request_method = 'OPTIONS') {
            if ($api2_cors_origin != "") {
                add_header 'Access-Control-Allow-Origin' "$api2_cors_origin";
                add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
                add_header 'Access-Control-Max-Age' 1728000;
                 # api2可能不需要凭证
                # add_header 'Access-Control-Allow-Credentials' 'false'; # 可以明确设置
                return 204;
            }
            return 403;
        }

        if ($api2_cors_origin != "") {
            add_header 'Access-Control-Allow-Origin' "$api2_cors_origin";
             # add_header 'Access-Control-Allow-Credentials' 'false';
        } else {
             return 403;
        }

        proxy_pass http://backend_api2;
        # ... 其他代理配置
    }

    # ... 其他 location
}

}
“`

这个示例展示了如何通过定义不同的 map 变量,并在不同的 location 块中引用它们,从而为不同的路径设置不同的 CORS 策略。

6. 常见问题与排查

  • 浏览器报错 “No ‘Access-Control-Allow-Origin’ header is present…”: 这是最典型的跨域错误。原因通常是 Nginx 没有正确添加 Access-Control-Allow-Origin 头部。
    • 检查你的 Nginx 配置,确保 add_header 'Access-Control-Allow-Origin' ...; 指令位于正确的 location 块内。
    • 如果你使用了 map,检查 map 的定义是否正确,以及 if ($cors_origin != "") { ... } 条件是否生效。
    • 确认请求的 Origin 头部是否被允许。
  • 浏览器报错 “Method is not allowed by Access-Control-Allow-Methods in preflight response.”: OPTIONS 预检请求的响应中的 Access-Control-Allow-Methods 没有包含实际请求将使用的方法。
    • 确保你的 OPTIONS 处理块中 add_header 'Access-Control-Allow-Methods' ...; 包含了所有需要的方法(GET, POST, PUT, DELETE, 以及 OPTIONS 本身)。
  • 浏览器报错 “Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers in preflight response.”: OPTIONS 预检请求的响应中的 Access-Control-Allow-Headers 没有包含前端实际发送的自定义头部。
    • 确保你的 OPTIONS 处理块中 add_header 'Access-Control-Allow-Headers' ...; 列出了前端使用的所有自定义头部。
  • 浏览器报错 “The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’.”: 前端设置了 withCredentials = true,但服务器响应的 Access-Control-Allow-Origin*
    • 如果你需要支持凭证,必须使用 map 根据请求的 Origin 动态设置 Access-Control-Allow-Origin 的值为实际请求源,并确保响应中包含 Access-Control-Allow-Credentials: true
  • 浏览器缓存了旧的 CORS 策略: 如果你修改了 CORS 配置,但浏览器仍然使用旧的策略,可能是因为 Access-Control-Max-Age 设置了较长的缓存时间。
    • 清空浏览器缓存,或者在开发/测试阶段将 Access-Control-Max-Age 设置为较小的值甚至不设置。
  • OPTIONS 请求没有正确处理: 浏览器发送了 OPTIONS 请求,但 Nginx 没有返回 204,或者返回了其他状态码/响应体。
    • 检查 if ($request_method = 'OPTIONS') { ... return 204; } 块是否正确配置,并且其中包含了所有必需的 add_header 指令。确保 return 204; 在正确的位置终止了处理。
  • 如何查看请求/响应头部?
    • 浏览器开发者工具: 打开浏览器的开发者工具(通常按 F12),切换到“网络” (Network) 选项卡。刷新页面或触发跨域请求,找到对应的请求,点击查看其“请求头” (Request Headers) 和“响应头” (Response Headers)。特别是对于预检请求,会看到一个 METHOD 为 OPTIONS 的请求。
    • curl 命令: 使用 curl -I <URL> 命令可以只获取指定 URL 的响应头部信息,这对于检查 CORS 头部非常有用,特别是对于 OPTIONS 请求:curl -I -X OPTIONS -H "Origin: http://client.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type,X-Custom-Header" <目标URL>

7. 最佳实践与注意事项

  • 最小权限原则: 除非你的 API 确实是公共的,否则不要使用 Access-Control-Allow-Origin: *。始终尽量限制允许的源。
  • 使用 map 处理多源和凭证: 当需要允许多个特定源或支持跨域凭证时,map 指令是比 if 指令更推荐和更灵活的方式来动态设置 Access-Control-Allow-Origin
  • 正确处理 OPTIONS 请求: 预检请求是 CORS 的核心部分。确保你的配置能够识别 OPTIONS 请求,并返回正确的 Access-Control-Allow-* 头部和 204 状态码,且不将请求转发给后端应用。
  • 合理设置 Access-Control-Max-Age: 适当增加预检请求的缓存时间(例如几小时到几天)可以减少 OPTIONS 请求的数量,提高性能,但如果频繁修改 CORS 策略,开发阶段应设置较短时间或不设置。
  • 仅添加必需的头部: Access-Control-Allow-MethodsAccess-Control-Allow-Headers 只需列出前端实际可能使用的方法和非简单请求头部。
  • 区分 OPTIONS 和实际请求的头部: 有些头部(如 Access-Control-Max-Age)只在 OPTIONS 响应中需要。Access-Control-Expose-Headers 只在实际请求响应中需要。Access-Control-Allow-OriginAccess-Control-Allow-Credentials 在两者中都需要(如果实际请求需要的话)。
  • 配置的层级: add_header 指令可以在 http, server, location, if 块中使用。通常在 location 块中配置是最常见的,可以精确控制哪些路径启用 CORS。如果在 server 块配置,会作用于该服务器下的所有 location,除非子 location 覆盖。在 http 块配置会作用于所有服务器,非常不灵活,不推荐。
  • if 指令的限制: Nginx 的 if 指令在某些上下文中使用可能存在陷阱,特别是在与 rewrite 或其他模块指令混用时。但对于简单的 add_headerreturn 操作,结合 $request_method$map_variable 使用通常是安全的。不过,对于复杂的逻辑,可以考虑使用 Lua 模块或其他 Nginx 扩展来处理 CORS。但对于大多数场景,基于 mapif 的方案已经足够强大和清晰。
  • 测试: 修改配置后,务必使用 nginx -t 检查配置文件的语法错误,并重新加载配置 nginx -s reload。然后在浏览器和使用 curl 命令进行测试,确保 CORS 头部正确返回,且符合预期。

8. 总结

跨域问题是 Web 开发中绕不开的挑战,而 CORS 是解决这一问题的标准和推荐方案。通过将 CORS 配置集中在 Nginx 这个强大的反向代理层,我们可以有效地管理和控制不同源对后端资源的访问,无需修改应用服务代码,提高了系统的性能、安全性和可维护性。

本文详细介绍了 CORS 的基本原理(简单请求、预检请求)、Nginx 中配置 CORS 的核心指令 add_header、如何处理允许所有源、允许特定源(使用 map)、处理凭证(Access-Control-Allow-Credentials)、暴露自定义头部(Access-Control-Expose-Headers)以及处理预检请求(OPTIONS + return 204 + Access-Control-Max-Age)。通过多个实战示例,展示了如何将这些配置应用于不同的场景。

掌握 Nginx 中的 CORS 配置,是构建健壮、安全、高效的现代 Web 应用不可或缺的技能。希望本文能够帮助您深入理解并成功解决 Nginx 环境下的跨域问题。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部