问题背景:
接到个需求,客户有两个系统要互相访问文件,文件服务器是通过nginx搭建的,原来的访问地址如下:http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx
客户说这个不安全,任何人都能访问,一定要做权限校验
接到这个需求我觉得安全隐患不是很大,因为文件名是随机的,nginx也不支持在线预览目录,盲猜是很难猜出正确地址的,除非被抓包,这种概率很小
没办法,客户坚持说有问题,只能安排加上token验证了
这个还是多系统的互相访问,涉及到互相认证,有点复杂,经过一番研究解决了,特地记录下
解决方法:
首先贴出解决方法,nginx配置文件如下:
docker-compose
services: nginx-9002: image: nginx:latest restart: always hostname: nginx-9002 container_name: nginx-9002 #privileged: true environment: - "TZ=Asia/Shanghai" ports: - 9002:80 volumes: - ./conf/nginx.conf:/etc/nginx/nginx.conf - ./conf/conf.d:/etc/nginx/conf.d/ - ./logs/:/var/log/nginx/ - ./ssl_key/:/data/ssl_key/ - ./www/:/usr/share/nginx/html/
这个配置涉及http和server模块,所以把nginx.conf和conf.d分别映射出来了
nginx.conf
user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" "$host"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 65; gzip on; include /etc/nginx/conf.d/*.conf; client_max_body_size 300M; #安全加固 keepalive_timeout 55; client_body_timeout 10; client_header_timeout 10; send_timeout 10; limit_conn ops 20; limit_conn_zone $binary_remote_addr zone=ops:10m; autoindex off; dav_methods off; server_tokens off; client_body_buffer_size 1K; client_header_buffer_size 1k; large_client_header_buffers 2 1k; add_header Content-Security-Policy "default-src 'self' http://abc.cn/ http://def.cn/ 'unsafe-inline' 'unsafe-eval' blob: data:;"; #add_header Content-Security-Policy "default-src 'self' 'unsafe-inline'";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; add_header X-Permitted-Cross-Domain-Policies "master-only"; add_header 'Referrer-Policy' 'origin'; add_header X-Download-Options "noopen" always; add_header Clear-Site-Data "storage"; add_header Cross-Origin-Embedder-Policy require-corp; add_header Cross-Origin-Opener-Policy same-site; add_header Cross-Origin-Resource-Policy same-site; add_header Permissions-Policy "interest-cohort=()"; #防止XSS攻击 add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options "nosniff"; #判断文件访问链接参数,进行校验接口分发 map $request_uri $auth_url { #存在'&source=yg'参数的链接走对应系统的校验地址 ~*&source=yg http://def.cn/api/file/link/validate; #把自有系统接口地址设置为默认出参 default http://192.168.100.93:8085/file/link/validate; } }
default.conf
server { listen 80; listen [::]:80; server_name localhost; #access_log /var/log/nginx/host.access.log main; #安全加固 #防止盗链或者恶意域名解析 if ( $host !~* 'abc.cn' ) { return 403; } #限制请求类型 if ($request_method !~ ^(GET|OPTIONS|POST)$ ) { return 501; } #封杀各种user-agent if ($http_user_agent ~* "python|perl|ruby|curl|bash|echo|uname|base64|decode|md5sum|select|concat|httprequest|nmap|scan|nessus|wvs" ) { return 403; } #if ($http_user_agent ~* "" ) { # return 403; #} #封杀特定的文件扩展名比如.bak以及目录; location ~* \.(bak|swp|save|sh|sql|mdb|svn|git|old)$ { rewrite ^/(.*)$ $host permanent; } location /(admin|phpadmin|status) { deny all; } location / { proxy_buffer_size 64k; proxy_buffers 32 32k; proxy_busy_buffers_size 128k; root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } #正式环境api接口 location ^~/online-api/ { proxy_buffer_size 64k; proxy_buffers 32 32k; proxy_busy_buffers_size 128k; proxy_pass http://192.168.100.93:8085/; } #正式环境文件接口 location ^~/online-file-api/ { proxy_buffer_size 64k; proxy_buffers 32 32k; proxy_busy_buffers_size 128k; proxy_pass http://192.168.100.93:9000/; #优化安全策略,使自有页面可以加载图片 add_header X-Frame-Options "ALLOW-FROM http://abc.cn/"; #把链接参数赋值到nginx内部参数 set $token $arg_token; set $from $arg_from; #参数调试 #add_header token $token; #add_header auth_url $auth_url; #add_header from $from; #add_header source $arg_source; #proxy_set_header X-Original-URI $request_uri; #跳转token校验 auth_request /auth; #校验接口返回401直接跳转未认证链接处理 error_page 401 = /unauthorized; } #token校验接口 location = /auth {
#只能内部调用 internal; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; #设置token和from值到head传给校验接口 proxy_set_header token $token; proxy_set_header from $from; #指定nginx域名解析dns resolver 218.2.135.1 61.147.37.1; #把map的出参传入代理地址 proxy_pass $auth_url; } location = /unauthorized { #未认证用户直接返回403 return 403 'Access denied'; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } #素描 location /sketch { proxy_pass http://192.168.100.93:7777; } }
配置详解:
1.auth_request
由于我们项目已经开发完毕,这个只是安全改造,所以最佳的方式就是结合nginx来进行权限校验,找了一圈还真找到了
https://blog.csdn.net/u014374743/article/details/135937481
https://blog.csdn.net/nalanxiaoxiao2011/article/details/133769412
先来说思路,这个来自于nginx自带的ngx_http_auth_request_module模块,实现基于子请求的结果的客户端的授权。如果子请求返回2xx响应码,则允许访问。如果它返回401或403,则访问被拒绝并显示相应的错误代码。子请求返回的任何其他响应代码都被认为是错误的。即在原来的基础上加了一层后端鉴权服务。
auth_request 就是整个配置的核心,当有请求来匹配到静态资源,会优先通过auth_request跳转到校验接口进行token校验,当校验成功则接口返回200状态,校验失败则返回401状态,auth_request收到200会返回相应的资源,收到401会抛出401报错
同时还发现另一种思路:https://www.cnblogs.com/lowmanisbusy/p/11718345.html
就是通过internal; 参数让静态文件只能接收内部访问,然后通过接口做校验,校验通过走内部访问,校验失败拒绝访问,这种是从接口层面直接进行token校验
但是考虑到多系统互相访问,要进行多系统分流校验,这种方式并不合适
于是就确定了用auth_request的方案,本以为问题就此愉快解决的时候,没想到,坑才刚刚开始。。
2.auth_request 根据参数分别请求不同验证地址
本来想法很简单
请求地址变为:http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx?token=xxxx&from=web&source=abc
后端把文件地址自动拼上token和其他参数传给前端,前端页面来访问
from=web/app 来区分客户端
source=abc/def 来区分系统
nginx自带参数arg_source来获取链接里的source参数
再使用if语句判断,然后分别设置接口校验地址,最后把地址传给auth_request 不就行了
这样就能根据链接传参来调用相应系统的权限校验接口地址,从而实现多系统的权限校验,类似于这样
location / { root /usr/share/nginx/html; index index.html index.htm; # 从查询参数中获取 token,并赋值给token变量 set $token $arg_token; set $source $arg_source; # 自定义验证失败时的处理页面 error_page 401 = /auth-required; if ($source ~ "jt") { set $flag $source; set $auth_path autha; } if ($source ~ "yg") { set $flag $source; set $auth_path authb; } add_header token $token; add_header from $arg_from; add_header flag $flag; add_header auth_path $auth_path; auth_request /$auth_path; #auth_request /authFileValidA; }
本来想的很完美,但是实际做起来 auth_request 并不能识别传参,打开http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx 照样能访问,nginx日志报错, invalid URL prefix in "auth-required" auth_request 参数为空
把 auth_request 写成实际参数的时候,可以拒绝访问,试了很多次都是这样,无奈只好换种方式,于是我想到了给校验接口传参
类似于这样
location = /authFileValidA { internal; # 只允许内部访问 proxy_pass $auth_path; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; # 设置AccessToken 的值为token proxy_set_header token "$token"; proxy_set_header from "$arg_from"; }
想法没毛病对吧,但是实际测试访问http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx 报500报错,token正确也报500,查看nginx日志报错 invalid URL prefix in "" auth request unexpected status: 500,也是识别不到nginx
百度了一下,不得不说百度AI这次帮了不少忙,问了一下百度AI
proxy_pass 支持传参吗
proxy_pass本身并不直接支持传参。proxy_pass主要用于将请求转发到另一个服务器,但它本身并不处理或传递参数。在Nginx配置中,proxy_pass指令用于定义代理服务器的地址,它将客户端的请求转发到该地址。
如果你需要在代理过程中传递参数,你可能需要使用其他方法或工具来实现。一种常见的方法是在Nginx配置中使用变量来构建代理URL,并在该URL中包含所需的参数。这样,每次请求时,Nginx都会根据这些变量动态地生成代理URL,并将请求转发到相应的地址。
需要注意的是,具体的实现方式可能因你的具体需求和Nginx版本而有所不同。因此,在实际应用中,你可能需要查阅Nginx的官方文档或相关资源,以了解如何根据你的需求进行配置和传参。
另外,如果你正在使用某种特定的编程语言或框架来处理代理请求,那么可能会有其他方式来实现传参的功能。在这种情况下,你可以查阅该语言或框架的文档以获取更多信息。
好吧,只能另辟蹊径了,于是我又问AI:
auth_request 根据参数分别请求不同验证地址 在Nginx配置中,auth_request指令允许你根据请求的参数来决定向不同的验证地址发送请求。以下是一个配置示例,其中使用了$arg_param变量来决定请求的验证地址: http { map $arg_param $auth_url { default /auth; value1 /auth1; value2 /auth2; } server { location / { auth_request /auth; error_page 401 = /unauthorized; } location = /auth { internal; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_pass http://backend$auth_url; } location = /unauthorized { return 401 'Access denied'; } } } 在这个配置中: map块定义了一个映射,根据请求参数param的值决定使用哪个验证URL。 当请求到达服务器块的location时,会自动触发auth_request指令,并将请求转发到/auth内部location。 proxy_pass使用了变量$auth_url,这是基于请求参数param的值动态决定的。 如果认证失败,会返回401错误。
说的很清楚,有木有,这就是我想要的map!
本以为问题就此迎刃而解的时候,一个坑又接踵而至。。
3.map获取参数并判断
就在我按AI的方式全部配好时,问题来了
一开始访问直接报500错误,看了下日志,说参数为空,于是我把map设置了default值,访问系统A可以正常访问,访问系统B,报错401,查看接口日志,其实还是调用的系统A的接口,当然校验不通过
试了很多次,都是这样,于是我不禁怀疑,莫非map 不支持 arg_param 获取链接参数?看来只能想其他方法了
另外有一点要吐槽的是,nginx配置调试太痛苦了,看不到里面参数值到底是多少,像开盲盒,通过设置head的方式也只能在反回200的页面看到参数,报错的页面也看不到
于是我又开始研究起使用正则表达式来匹配的方式
由于太纠结于获取参数,一开始想法跑偏了。。
要匹配URL中的source参数值,你可以使用以下正则表达式:
(?<=&source=)[^&]*这个正则表达式的关键部分使用了正向后发断言(positive lookbehind assertion),它允许你匹配一个位于特定模式之后的字符串。在这个例子中,(?<=&source=)是一个正向后发断言,它确保匹配的字符串是在&source=之后出现的。
[^&]*这部分则匹配任何不是&字符的零个或多个字符,这通常是参数值的内容。
下面是一个Python示例,演示如何使用这个正则表达式来从URL中提取source参数的值:
pythonimport re# 示例URL
url = "http://example.com/path?param1=value1&source=value2¶m2=value3#fragment"# 正则表达式模式
pattern = r"(?<=&source=)[^&]*"# 使用re.search来查找匹配项
match = re.search(pattern, url)# 如果找到匹配项,则提取捕获组中的值
if match:
source_value = match.group(0)
print(f"提取到的source参数值: {source_value}")
else:
print("未找到source参数")在这个例子中,source_value将包含value2,这是source参数的值。如果URL中不存在source参数,re.search将返回None,因此我们在提取值之前检查了匹配项是否存在。
请注意,这个正则表达式假设URL参数是用&符号分隔的,并且source参数不包含&字符。如果source参数的值可能包含&字符,并且你想要匹配整个值(包括&字符),那么你可能需要使用稍微不同的正则表达式,例如:
regex&source=([^&#]*)这个正则表达式会匹配&source=后面直到遇到&或#字符(或字符串结束)的部分。
经过实测 (?<=&source=)[^&]*这个确实可以获取到参数,通过这个网站可以验证https://www.lzltool.com/regex
但是获取之后怎么弄呢,我傻眼了
直到看到这篇,对这就是我想要的
https://blog.csdn.net/zcy_wxy/article/details/123499842
map $request_uri $auth_url { #存在'&source=yg'参数的链接走对应系统的校验地址 ~*&source=yg http://1.1.1.1:9001/file/link/validate; #把自有系统接口地址设置为默认出参 default http://192.168.100.93:8085/file/link/validate; }
好了,开始配置,测试,又报错了。。
4.no resolver defined to resolve
百度了一圈也没人说明白,于是我又问了AI
nginx no resolver defined to resolve
报错解释:
这个错误表明Nginx试图解析一个域名,但是它没有在配置文件中指定解析器(resolver)指令。Nginx需要知道如何将域名解析为IP地址,这通常是通过在Nginx配置中设置resolver指令来完成的。
解决方法:
打开Nginx配置文件(通常位于/etc/nginx/nginx.conf或者/etc/nginx/conf.d/目录下的某个文件中)。
定位到需要修改的server块或location块。
添加resolver指令,并指定DNS服务器地址。例如:
resolver 8.8.8.8 8.8.4.4;
其中8.8.8.8和8.8.4.4是Google提供的公共DNS服务器地址。
重新加载Nginx配置以使更改生效:
sudo nginx -s reload
或者重启Nginx服务:
sudo systemctl restart nginx
或者
sudo service nginx restart
确保在进行这些更改时,你有足够的权限,并且在生产环境中操作时要小心,因为不当的配置可能会导致服务中断。
这个解释简单明了,就是nginx要指定dns服务器,来帮助他解析域名,要不然解析不了域名
我们有个系统权限校验接口用的是域名
于是我又加上配置,指定dns服务器
#指定nginx域名解析dns resolver 218.2.135.1 61.147.37.1;
至此问题解决,整个流程终于跑通了!
5.linux统计当前文件夹内所有文件后缀类型
要统计当前文件夹内所有文件的后缀类型,你可以使用ls
命令结合awk
和sort
等工具来实现。下面是一个简单的命令,它会列出当前目录下的所有文件,提取它们的后缀,并统计每个后缀的出现次数:
bashls -1 | awk -F. '{print $NF}' | sort | uniq -c
这里是命令的详细解释:
ls -1
: 列出当前目录下的所有文件和目录,每个文件或目录占一行。awk -F. '{print $NF}'
: 使用awk
命令处理ls
的输出。-F.
设置字段分隔符为点(.
),{print $NF}
打印每行的最后一个字段,即文件的后缀名。注意,这个命令也会包括没有后缀名的文件,并且如果文件名中有多个点,它只会打印最后一个点后面的部分。sort
: 对awk
的输出进行排序,这样相同的后缀会相邻。uniq -c
: 统计相邻的重复行的数量,也就是统计每个后缀出现的次数。
如果你只关心文件(不包括目录),你可以使用find
命令过滤出文件:
bashfind . -maxdepth 1 -type f -exec basename {} \; | awk -F. '{print $NF}' | sort | uniq -c
这里find
命令的作用是:
.
: 在当前目录下查找。-maxdepth 1
: 限制搜索深度为1,即只在当前目录中查找,不包括子目录。-type f
: 只查找文件。-exec basename {} \;
: 对找到的每个文件执行basename
命令,这会去掉路径信息,只保留文件名。
然后,和前面的命令一样,使用awk
、sort
和uniq
来提取和统计后缀类型。
实测确实很好用
6.安全遗留问题
其实,这种方式的改造还是有安全隐患,就是如果 http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx?token=xxxx&from=web&source=abc
这个地址被抓包,在一段时间内,任何人也是可以通过这地址获取到文件
最好的方式就是,后端单独生成一个 token给文件服务器用,然后配置只能使用一次,校验后即失效,这样是安全的
但是也会有问题,就是如果用户不刷新页面再点一下,就看不到文件了,得自己刷新下页面,重新获取token
7.后期bug
经过测试,发现有的文件访问地址变成了http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx?token=xxxx&from=web&source=abc?token=xxxx&from=web&source=abc
连续带了两个token,这样有时候会访问不了
经过排查,原因为修改页面,前端直接把http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx?token=xxxx&from=web地址传给后端入库导致的
也就是后端要做两个拦截器,一个处理出参加参数,一个用于过滤入参删除不必要的参数
app端预览文件地址变成了http://abc.cn/file/fa1a8d99a47b4c8c9d59152728af9930.docx?token=xxxx
经过排查为前端框架获取参数遇到&符号自动截断了,做下处理就行