Nginx 中的 X-Accel-Redirect 响应头
在 Nginx 的 ngx_http_proxy_module 的使用说明中,有对 X-Accel-Redirect
的描述:
“X-Accel-Redirect” performs an internal redirect to the specified URI;
本文就简单的描述一下在某种特殊场景下 X-Accel-Redirect
的使用。
需求
在实际业务中,需求是在一堆限制条件中,达到某个目的。我这里将复杂的业务特性都去掉,最简化的描述一下。
- 有个前端 OpenResty 服务,称为 A,一个后段 OpenResty 服务,称为 B。请求从 A 代理到 B。
- 一般情况下,请求在服务 A 中的访问日志,记录在正常日志文件
logs/access.log
里。 - 如果后端服务 B 响应为 403,且包含了一个值为 1 的特定的响应头
Invalid-Domain
,则该请求在服务 A 中的访问日志,记录在特定日志文件logs/invalid_domain.log
里。
这里面有一个需要注意的地方:
后端服务 B,其响应 403 的时候,有两种情况,一种是根据业务逻辑,包含值为 1 的特定的响应头 Invalid-Domain
,而其他情况的 403 响应中会添加一些其他的正常的响应头,在通过 A 访问时,这些正常的头部不能有缺失。
方案一
最简单的实现方式,是在服务 A 中使用 error_page,最简化的完整配置如下:
events {
}
http {
# 模拟 后端服务 Server B, 这里为了简化, 让其永远返回 403.
# 在请求带了参数 invalid_domain 为 1 时, 添加额外的响应头 Invalid-Domain.
# 其他情况下, 添加响应头 Valid-Domain.
server {
listen 8081;
location / {
access_log logs/server_b.log;
header_filter_by_lua_block {
if ngx.var.arg_invalid_domain == "1" then
ngx.header["Invalid-Domain"] = "1"
else
ngx.header["Valid-Domain"] = "1"
end
ngx.exit(403)
}
}
}
# 模拟前端服务 Server A.
server {
listen 8082;
access_log logs/access.log;
location / {
proxy_pass http://127.0.0.1:8081;
proxy_intercept_errors on;
error_page 403 = @check_invalid_domain;
}
location @check_invalid_domain {
internal;
if ($upstream_http_invalid_domain = "1") {
access_log logs/invalid_domain.log;
}
return 403;
}
}
}
分别使用参数 invalid_domain=1
和不带参数去访问服务 A,会发现访问日志的生效是满足需求的。但有一个容易忽略的地方:当不带参数访问时,后端服务 B 的响应头 Valid-Domain
由于 error_page 带来的内部跳转被去掉了。
这看起来不是什么大事,但我这份配置只是为了演示而极端简化过的,在实际业务中,Valid-Domain
头代表着复杂各异的定制化需求,被去掉是肯定不行的。
问题的焦点,在于我们要同时判断后端响应头和响应状态码,但 error_page 指令中无法对响应头进行组合逻辑判断,所以需要到 error_page 目标 location 中做进一步判断,于是就导致了我们上面说的正常 403 时响应头 Valid-Domain
头的丢失。
方案二
我们再来梳理一下逻辑,看看解决问题的思路。
首先,响应 403 且带有 Invalid-Domain=1
响应头的请求,因为要定制化 log 路径,是需要一个内部跳转到一个新的 location 中去的,即 location @check_invalid_domain
是必须要引入的。可能有人会说,直接用 if 条件组合来定制 access_log 是不是也可以。第一,我个人非常不喜欢 Nginx 的 if 指令,甚至整个 rewrite 模块我都不喜欢使用;第二,由于要判断后端响应码和响应头,这个时候 if 是不管用的,因为它的阶段太靠前。
引入 location @check_invalid_domain
后,所有的 403 响应都会跳转到该 location,而如前所述,业务中的 403 分为两类,有一类的后端响应头是不能丢失的。所以,如果一定要使用这种方案,我们要在 location @check_invalid_domain
中,将后端的响应头再添加进去,在实际业务中,这块会涉及到整个业务框架的部分,可能会很复杂。
那我们再换个思路,能不能在进入 location @check_invalid_domain
之前,就做好响应头的判断,在 location @check_invalid_domain
中,只配置访问日志。但同样如前所属,error_page 无法做组合逻辑,所以似乎路又堵死了。
这时,我们再来看一下 X-Accel-Redirect
头,就会惊喜的发现,它恰好是干这个的。本质上只有 Server B 才真正知道 403 的各种情况,那么只要在需要 Server A 修改访问日志时,加一个 X-Accel-Redirect
头来通知 Server A 做内部跳转就好了,起到一种精确制导的效果,而非将这些逻辑判断交给前端 Server A。
配置如下:
events {
}
http {
# 模拟 后端服务 Server B, 这里为了简化, 让其永远返回 403.
# 在请求带了参数 invalid_domain 为 1 时, 添加额外的响应头 Invalid-Domain.
# 其他情况下, 添加响应头 Valid-Domain.
server {
listen 8081;
location / {
access_log logs/server_b.log;
header_filter_by_lua_block {
if ngx.var.arg_invalid_domain == "1" then
ngx.header["X-Accel-Redirect"] = "@check_invalid_domain"
ngx.header["Invalid-Domain"] = "1"
else
ngx.header["Valid-Domain"] = "1"
end
ngx.exit(403)
}
}
}
# 模拟前端服务 Server A.
server {
listen 8082;
access_log logs/access.log;
location / {
proxy_pass http://127.0.0.1:8081;
}
location @check_invalid_domain {
internal;
access_log logs/invalid_domain.log;
return 403;
}
}
}
可以看到,与前一版的区别,只有 3 个地方:
- 后端 Server B 在适当的时候增加
X-Accel-Redirect
响应头。 - 前端 Server A 的配置,去掉 error_page,由响应头
X-Accel-Redirect
在 nginx 的内部处理逻辑来自行做内部跳转。 - 内部跳转的目标地址只需要修改访问日志路径。
方案三
真正最灵活的方案,是修改一下 Nginx 的代码,将 access_log 的配置由静态配置改成每请求动态配置,并提供 lua 接口来给业务层调用,这样压根就不需要做内部跳转,代码改起来也不复杂,但实际操作中,能用已有的功能较好的满足需求就已经足够了。