问题背景:

接到个需求,客户有两个系统要互相访问文件,文件服务器是通过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&param2=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统计当前文件夹内所有文件后缀类型

 由于后端要做拦截器,所以需要统计下静态文件所有后缀类型,这样拦截的会比较准
这里还是请教百度AI
 

要统计当前文件夹内所有文件的后缀类型,你可以使用ls命令结合awksort等工具来实现。下面是一个简单的命令,它会列出当前目录下的所有文件,提取它们的后缀,并统计每个后缀的出现次数:

bashls -1 | awk -F. '{print $NF}' | sort | uniq -c

这里是命令的详细解释:

  1. ls -1: 列出当前目录下的所有文件和目录,每个文件或目录占一行。
  2. awk -F. '{print $NF}': 使用awk命令处理ls的输出。-F.设置字段分隔符为点(.),{print $NF}打印每行的最后一个字段,即文件的后缀名。注意,这个命令也会包括没有后缀名的文件,并且如果文件名中有多个点,它只会打印最后一个点后面的部分。
  3. sort: 对awk的输出进行排序,这样相同的后缀会相邻。
  4. 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命令,这会去掉路径信息,只保留文件名。

然后,和前面的命令一样,使用awksortuniq来提取和统计后缀类型。

实测确实很好用

 

 

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

经过排查为前端框架获取参数遇到&符号自动截断了,做下处理就行

 

posted on 2024-05-10 19:10  06  阅读(515)  评论(0编辑  收藏  举报