用 nginx 对文件服务器鉴权
2023 11 21 12:35 PM 240次查看
用户在使用 seafile 上传文件,希望将其分享出去。
seafile 自己生成的分享链接类似于
https://seafile.xxx.com/f/23cc6812408e44f8b63e/
,若未过期且密码正确时会将其重定向到类似 http://seafile.yizhisec.com/seafhttp/files/e1687864-8a12-46db-9708-b4240df346be/1.txt
的地址。产品希望能对这个分享链接进行鉴权,并且最好不暴露实际的文件地址和避免发起重定向的请求。
因为对 seafile 进行二次开发比较麻烦,最简单的对其鉴权的方式是使用 nginx 的 ngx_http_auth_request_module:
server {
# 省略无关配置
location = /auth {
internal;
proxy_pass http://api-server/api/auth;
proxy_http_version 1.1;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
location /f/ {
auth_request /auth;
proxy_pass http://seafile;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这段配置的作用是在访问 /f/
前缀的链接时,nginx 会去调用 http://api-server/api/auth
进行鉴权。这个鉴权请求会被去掉 body,header 的 X-Original-URI
字段会被设置为原请求的 URI 的 host 之后的部分(即 path?query),其他 header 字段会被设为和原请求相同。我们定制的鉴权代码可以检查 header 部分,并决定是否允许访问:如果返回 2xx 状态码,则 nginx 会继续请求并返回 seafile 服务;如果返回 401、403 和 500 状态码,则禁止访问并返回对应状态码;其他状态码会导致 nginx 报错,禁止访问并返回 500 状态码。当禁止访问时,不会输出鉴权响应的 body。
那么,如果客户端需要区分不同情况下的禁止访问需要如何解决呢?
我想当然地尝试用
if
来判断:server {
location /f/ {
auth_request /auth;
auth_request_set $code $upstream_http_x_code;
proxy_pass http://seafile;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
if ($code) {
return 403 '{"code": $code}';
}
if ($code = "") {
proxy_pass http://api;
}
}
}
按我的想法,只要我在鉴权接口返回 403 时,同时设置一个 x-code
的 header,nginx 就会在 body 中返回这个 code。然而这却是不可行的,因为 if
是 ngx_http_rewrite_module 模块的指令,它的执行流程早于 auth_request
和 auth_request_set
,因此进行判断时,$code
永远是空字符串。而要在请求之后再判断的话,就需要用到 error_page 指令了:
server {
location /f/ {
auth_request /auth;
auth_request_set $code $upstream_http_x_code;
proxy_pass http://seafile;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
error_page 403 = @handle_denied;
}
location @handle_denied {
return 403 '{"code": $code}';
}
}
接下来解决重定向的问题。
正常情况下,重定向后的地址仍然需要鉴权才能保证安全性。而重定向很可能是客户端所用的 HTTP 库自动处理的,添加 header 可能导致客户端改动较大。再加上重定向后的地址暴露了文件路径,所以最好还是后端来解决。
其实只要检查到重定向之后,让 nginx 再发起一次
proxy_pass
就可以了。但是默认情况下,nginx 会直接将 3xx 的状态码返回给客户端,只有当 proxy_intercept_errors 指令开启后时,才会执行 error_page
指令。server {
location /f/ {
auth_request /auth;
proxy_pass http://seafile;
proxy_http_version 1.1;
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_intercept_errors on;
error_page 301 302 307 = @handle_redirects;
}
location @handle_redirects {
set $redirect_to '$upstream_http_location'; # 必须再存个临时变量,否则下面用不了
proxy_pass $redirect_to;
}
}
考虑到 nginx 本来也需要对 seafile 文件的下载进行反向代理,因此直接在重定向时进行反向代理也并不会增加多少开销。但是这样的配置对 seafile 返回的
Location
字段是有要求的:它的 host 部分需要能被 nginx 解析。正常情况下配置一个 dns 就能解决,而不巧的是我们的 nginx 是只能访问容器的内网环境的,而 seafile 返回的是一个外网地址。无奈之下只好自行解析
Location
字段了:map $upstream_http_location $location {
~^https?://[^/]+(.*)$ $1; # 以 http 开头的截取 path 部分
default $upstream_http_location; # 不以 http 开头的则获取全部
}
server {
location @handle_redirects {
set $redirect_to '$location';
proxy_pass http://seafile$redirect_to;
}
}
这里的 map
块是和 server
块同级的,需要在 http
块内。它的作用是解析 $upstream_http_location
变量,当它符合 ~^https?://[^/]+(.*)$
这个正则表达式时,将主机之后的部分截取为 $location
变量;当它只包含 path 和 query 时,直接全部保存为 $location
变量。之后在
proxy_pass
指令中补上内网的主机地址,就可以正常访问了。2023 年 11 月 28 日更新:
seafile 在文件分享后又被删除的情况下,会返回 200 状态码,并输出一个文件不存在的页面。我发现它的源码中并没有正确地设置状态码,于是提了个 pull request。
不过等它合并不知道要多久,于是先自行解决吧。比较了一下和正常的文件下载不同的地方,发现正常下载会输出
Content-Disposition
头字段,用以描述文件名。因此只要发现状态码是 200,但是没有输出 Content-Disposition
时,把状态码改成 404 或 410 即可。但是 nginx 并没有提供根据头字段修改状态码的指令,不得已只好用 OpenResty 来实现了,好在基本上是兼容的:
server {
location /f/ {
auth_request /auth;
proxy_pass http://seafile;
proxy_http_version 1.1;
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_intercept_errors on;
error_page 301 302 307 = @handle_redirects;
header_filter_by_lua_block {
if ngx.status == 200 and ngx.header["Content-Disposition"] == nil then
ngx.exit(404)
end
}
}
}
0条评论 你不来一发么↓