Nginx 配置指令的执行顺序(三)同一阶段的执行顺序 & access 阶段 (转载)

本文详细解析了Nginx配置中rewrite阶段与其他第三方模块交互的特性,包括请求头操作指令(如more_set_input_headers)和rewrite阶段末尾执行的指令(如rewrite_by_lua)的执行顺序及作用,以及access阶段如何实现访问控制。

转载自 http://blog.sina.com.cn/openresty

 

如前文所述,除非像 ngx_set_misc 模块那样使用特殊技术,其他模块的配置指令即使是在 rewrite 阶段运行,也不能和 ngx_rewrite 模块的指令混合使用。不妨来看几个这样的例子。

 

    第三方模块 ngx_headers_more 提供了一系列配置指令,用于操纵当前请求的请求头和响应头。其中有一条名叫 more_set_input_headers 的指令可以在 rewrite 阶段改写指定的请求头(或者在请求头不存在时自动创建)。这条指令总是运行在 rewrite 阶段的末尾,该指令的文档中有这么一行标记:

    phase: rewrite tail

其中的 rewrite tail 的意思就是 rewrite 阶段的末尾。

 

    既然运行在 rewrite 阶段的末尾,那么也就总是会运行在 ngx_rewrite 模块的指令之后,即使我们在配置文件中把它写在前面,例如:

    ? location /test {

    ?     set $value dog;

    ?     more_set_input_headers "X-Species: $value";

    ?     set $value cat;

    ?

    ?     echo "X-Species: $http_x_species";

    ? }

这个例子用到的 $http_XXX 内建变量在读取时会返回当前请求中名为 XXX 的请求头,我们在 Nginx 变量漫谈(二) 中曾经简单提过它。需要注意的是,$http_XXX 变量在匹配请求头时会自动对请求头的名字进行归一化,即将名字的大写字母转换为小写字母,同时把间隔符(-)替换为下划线(_),所以变量名 $http_x_species 才得以成功匹配 more_set_input_headers 语句中设置的请求头 X-Species.

 

    此例书写的指令顺序会误导我们认为 /test 接口输出的 X-Species 头的值是 dog,然而实际的结果却并非如此:

    $ curl 'http://localhost:8080/test'

    X-Species: cat

显然,写在 more_set_input_headers 指令之后的 set $value cat 语句却先执行了。

 

    上面这个例子证明了即使运行在同一个请求处理阶段,分属不同模块的配置指令也可能会分开独立运行(除非像 ngx_set_misc 等模块那样针对 ngx_rewrite 模块提供特殊支持)。换句话说,在单个请求处理阶段内部,一般也会以 Nginx 模块为单位进一步地划分出内部子阶段。

 

    第三方模块 ngx_lua 提供的 rewrite_by_lua 配置指令也和 more_set_input_headers 一样运行在 rewrite 阶段的末尾。我们来验证一下:

    ? location /test {

    ?     set $a 1;

    ?     rewrite_by_lua "ngx.var.a = ngx.var.a + 1";

    ?     set $a 56;

    ?

    ?     echo $a;

    ? }

这里我们在 rewrite_by_lua 语句内联的 Lua 代码中对 Nginx 变量 $a 进行了自增计算。从该例的指令书写顺序上看,我们或许会期望输出是 56,可是因为 rewrite_by_lua 会在所有的 set 语句之后执行,所以结果是 57:

    $ curl 'http://localhost:8080/test'

    57

显然,rewrite_by_lua 指令的行为不同于我们前面在 (二) 中介绍过的 set_by_lua 指令。

 

    有的读者可能要问,既然 more_set_input_headers 和 rewrite_by_lua 指令都运行在 rewrite 阶段的末尾,那么它们之间的先后顺序又是怎样的呢?答案是:不一定。我们应当避免写出依赖它们二者间顺序的配置。

 

    Nginx 的 rewrite 阶段是一个比较早的请求处理阶段,这个阶段的配置指令一般用来对当前请求进行各种修改(比如对 URI 和 URL 参数进行改写),或者创建并初始化一系列后续处理阶段可能需要的 Nginx 变量。当然,也不能阻止一些用户在 rewrite 阶段做一系列更复杂的事情,比如读取请求体,或者访问数据库等远方服务,毕竟有 rewrite_by_lua 这样的指令可以嵌入任意复杂的 Lua 代码。

 

    在 rewrite 阶段之后,有一个名叫 access 的请求处理阶段。Nginx 变量漫谈(五) 中介绍过的第三方模块 ngx_auth_request 的指令就运行在 access 阶段。在 access 阶段运行的配置指令多是执行访问控制性质的任务,比如检查用户的访问权限,检查用户的来源 IP 地址是否合法,诸如此类。

 

    例如,标准模块 ngx_access 提供的 allow 和 deny 配置指令可用于控制哪些 IP 地址可以访问,哪些不可以:

    location /hello {

        allow 127.0.0.1;

        deny all;

 

        echo "hello world";

    }

这个 /test 接口被配置为只允许从本机(IP 地址为保留的 127.0.0.1)访问,而从其他 IP 地址访问都会被拒(返回 403 错误页)。ngx_access 模块自己的多条配置指令之间是按顺序执行的,直到遇到第一条满足条件的指令就不再执行后续的 allow 和 deny 指令。如果首先匹配的指令是 allow,则会继续执行后续其他模块的指令或者跳到后续的处理阶段;而如果首先满足的是 deny 则会立即中止当前整个请求的处理,并立即返回 403 错误页。所以看上面这个例子,如果是从本地访问的,则首先匹配 allow 127.0.0.1 这一条语句,于是 Nginx 就继续往下执行其他模块的指令以及后续的处理阶段;而如果是从其他机器访问,则首先匹配的则是 deny all 这一条语句,即拒绝所有地址,它会导致 403 错误页立即返回给客户端。

 

    我们来实测一下。从本机访问这个接口可以得到

    $ curl 'http://localhost:8080/hello'

    hello world

而从另一台机器访问这台机器(假设运行 Nginx 的机器地址是 192.168.1.101)提供的接口时则得到

    $ curl 'http://192.168.1.101:8080/hello'

    <html>

    <head><title>403 Forbidden</title></head>

    <body bgcolor="white">

    <center><h1>403 Forbidden</h1></center>

    <hr><center>nginx</center>

    </body>

    </html>

值得一提的是,ngx_access 模块还支持所谓的“CIDR 记法”来表示一个网段,例如 169.200.179.4/24 则表示路由前缀是 169.200.179.0(或者说子网掩码是 255.255.255.0)的网段。

 

    因为 ngx_access 模块的指令运行在 access 阶段,而 access 阶段又处于 rewrite 阶段之后,所以前面我们见到的所有那些在 rewrite 阶段运行的配置指令,都总是在 allow 和 deny 之前执行,而无论它们在配置文件中的书写顺序是怎样的。所以,为了避免阅读配置时的混乱,我们应该总是让指令的书写顺序和它们的实际执行顺序保持一致

&lt;think&gt;我们正在解决Axios无法获取响应头中特定字段(如Content-Disposition)的问题,核心原因是CORS限制。浏览器默认只暴露简单响应头,其他自定义头需要服务器通过`Access-Control-Expose-Headers`响应头显式暴露。 在Nginx配置中,我们需要在对应的location块中添加`add_header Access-Control-Expose-Headers`指令,后面跟上需要暴露的头部字段,多个字段用逗号分隔。 根据用户提供的引用,我们可以参考引用[1]和引用[2]中关于跨域配置的方式,在location块中添加`add_header`指令。 示例:假设我们想要暴露`Content-Disposition`和`Custom-Header`这两个响应头,配置如下: ```nginx server { listen 80; server_name example.com; location /api/ { # 处理跨域请求 if ($request_method = &#39;OPTIONS&#39;) { add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39;; add_header &#39;Access-Control-Allow-Methods&#39; &#39;GET, POST, OPTIONS&#39;; add_header &#39;Access-Control-Allow-Headers&#39; &#39;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&#39;; add_header &#39;Access-Control-Max-Age&#39; 1728000; add_header &#39;Content-Type&#39; &#39;text/plain; charset=utf-8&#39;; add_header &#39;Content-Length&#39; 0; return 204; } # 正常请求的处理 proxy_pass http://backend_server; # 设置允许的源,可以是具体域名或*(但注意如果带凭证请求则不能用*) add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39; always; # 暴露特定的响应头 add_header &#39;Access-Control-Expose-Headers&#39; &#39;Content-Disposition, Custom-Header&#39; always; # 其他安全头部,参考引用[3] add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header X-Xss-Protection &quot;1; mode=block&quot; always; } } ``` 注意: 1. 使用`always`参数确保在非200响应时也添加头(如4xx、5xx等)。 2. 如果前端需要携带凭证(如cookies),则`Access-Control-Allow-Origin`不能为`*`,必须指定具体的域名,并且需要设置`Access-Control-Allow-Credentials: true`(参考引用[4])。 3. 在同一个location块中,如果同时配置了OPTIONS请求的处理和普通请求的处理,注意普通请求的add_header指令要放在proxy_pass之后(因为Nginx配置顺序执行的,但add_header指令同一阶段会合并)。 如果只是简单的配置,也可以这样: ```nginx location /api/ { # ... 其他配置(如proxy_pass等) # 跨域相关配置 add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39; always; add_header &#39;Access-Control-Allow-Methods&#39; &#39;GET, POST, OPTIONS&#39; always; add_header &#39;Access-Control-Allow-Headers&#39; &#39;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&#39; always; add_header &#39;Access-Control-Expose-Headers&#39; &#39;Content-Disposition, Custom-Header&#39; always; add_header &#39;Access-Control-Allow-Credentials&#39; &#39;true&#39; always; # 如果需要凭证,注意这里和Allow-Origin不能为* } ``` 但是注意,如果配置了`Access-Control-Allow-Credentials: true`,那么`Access-Control-Allow-Origin`就不能使用`*`,必须指定具体的域名,例如: ```nginx add_header &#39;Access-Control-Allow-Origin&#39; &#39;http://your-frontend-domain.com&#39; always; ``` 配置完成后,使用`nginx -t`测试配置是否正确,然后使用`nginx -s reload`重新加载配置(参考引用[3])。 这样配置后,响应头中就会包含`Access-Control-Expose-Headers: Content-Disposition, Custom-Header`,浏览器就会允许前端JavaScript代码访问这些响应头字段。 相关问题: 1. Nginx中如何配置多个跨域头? 2. 如何解决跨域请求携带Cookie的问题? 3. Nginx配置CORS后为什么某些浏览器还是报跨域错误? 4. 如何为特定的文件类型(如字体文件)配置跨域头?&lt;/think&gt;### 在 Nginx配置 Access-Control-Expose-Headers 解决 Axios 获取响应头问题 #### 完整配置示例(解决 `Content-Disposition` 等自定义头不可见问题) ```nginx server { listen 80; server_name your-domain.com; location /api/ { # 核心配置:暴露自定义响应头字段 add_header Access-Control-Expose-Headers &quot;Content-Disposition, X-Custom-Token, File-Name&quot;; # 基础跨域配置 add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods &#39;GET, POST, OPTIONS&#39;; add_header Access-Control-Allow-Headers &#39;DNT,User-Agent,X-Requested-With,Content-Type,Authorization&#39;; # 安全增强配置(可选) add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; # 后端代理配置 proxy_pass http://backend-server; proxy_set_header Host $host; } # 针对字体文件的特殊配置(参考引用[2]) location ~* \.(eot|ttf|woff|svg|otf)$ { add_header Access-Control-Allow-Origin *; add_header Access-Control-Expose-Headers &quot;Content-Disposition&quot;; } } ``` #### 关键配置说明 1. **核心指令** ```nginx add_header Access-Control-Expose-Headers &quot;Content-Disposition, X-Custom-Token, File-Name&quot;; ``` - 将需要前端访问的响应头字段用逗号分隔 - 常见需暴露字段:`Content-Disposition`(文件下载)、`Authorization`(令牌)、`X-Pagination`(分页信息) 2. **跨域基础配置** ```nginx add_header Access-Control-Allow-Origin *; # 生产环境建议替换为具体域名 add_header Access-Control-Allow-Methods &#39;GET, POST, OPTIONS&#39;; add_header Access-Control-Allow-Headers &#39;Authorization, Content-Type&#39;; ``` 3. **配置生效范围** - 全局生效:放在 `server { }` 块内 - 局部生效:放在特定 `location { }` 块内(推荐) 4. **安全增强建议**(参考引用[3]) ```nginx add_header X-Content-Type-Options nosniff; # 禁止 MIME 类型嗅探 add_header X-Frame-Options DENY; # 防点击劫持 ``` #### 验证步骤 1. 保存配置执行: ```bash nginx -t -q # 验证配置语法(参考引用[3]) nginx -s reload # 热重载配置 ``` 2. 在浏览器开发者工具检查响应头: ```http HTTP/1.1 200 OK Access-Control-Expose-Headers: Content-Disposition, X-Custom-Token Access-Control-Allow-Origin: * Content-Disposition: attachment; filename=&quot;report.pdf&quot; ``` 3. 前端 Axios 获取: ```javascript axios.get(&#39;/api/file&#39;).then(res =&gt; { // 注意:必须使用小写字段名! const fileName = res.headers[&#39;content-disposition&#39;]; }) ``` #### 常见问题排查 1. **配置未生效** - 检查 `location` 路径是否匹配实际 API 路由 - 确认配置块内没有重复的 `add_header` 指令覆盖 2. **浏览器仍无法获取** - 确保响应头字段名拼写完全一致(包括大小写) - 检查是否存在多层代理(如 CDN)覆盖了响应头 3. **携带 Cookie 的场景**(参考引用[4]) ```nginx add_header Access-Control-Allow-Credentials &#39;true&#39;; add_header Access-Control-Allow-Origin &#39;https://your-frontend.com&#39;; # 不能使用 * ``` &gt; **关键原理**:浏览器 CORS 安全策略默认仅暴露[简单响应头](https://developer.mozilla.org/en-US/docs/Glossary/Simple_response_header),自定义头必须通过 `Access-Control-Expose-Headers` 显式声明暴露权限[^1][^2]。 --- ### 相关问题 1. **Nginx 配置 CORS 后为什么 OPTIONS 请求返回 404?** &gt; 需单独处理预检请求: &gt; ```nginx &gt; location / { &gt; if ($request_method = &#39;OPTIONS&#39;) { &gt; add_header Access-Control-Allow-Origin *; &gt; add_header Access-Control-Allow-Methods &#39;GET, POST, DELETE&#39;; &gt; add_header Access-Control-Allow-Headers &#39;Content-Type, Authorization&#39;; &gt; return 204; &gt; } &gt; } &gt; ``` 2. **如何为不同文件类型配置不同的跨域规则?** &gt; 使用正则匹配 location 块(参考引用[2]): &gt; ```nginx &gt; location ~* \.(pdf|docx)$ { &gt; add_header Access-Control-Expose-Headers &quot;Content-Disposition&quot;; &gt; } &gt; ``` 3. **生产环境如何安全配置 Access-Control-Allow-Origin?** &gt; 动态匹配请求来源: &gt; ```nginx &gt; map $http_origin $cors_header { &gt; default &quot;&quot;; &gt; &quot;~https?://(sub1\.|sub2\.)?example\.com&quot; $http_origin; &gt; } &gt; server { &gt; add_header Access-Control-Allow-Origin $cors_header; &gt; } &gt; ``` 4. **为什么 Chrome 能看到响应头但 Axios 获取不到?** &gt; 浏览器开发者工具显示的是原始响应头,但 JavaScript 访问受 CORS 策略限制,必须通过 `Access-Control-Expose-Headers` 暴露后才可访问[^1]。 [^1]: CORS 安全策略限制前端访问非暴露响应头字段 [^2]: Nginx 中需通过 `add_header` 显式声明暴露权限 [^3]: 配置修改后需执行 `nginx -t -q` 验证语法并重载服务 [^4]: 带凭证请求需配置 `Access-Control-Allow-Credentials` 和具体域名
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值