Nginx通过auth_request结合Springboot实现静态文件下载鉴权

1,190 阅读3分钟

前言:需求是判断用户是否购买了资源文件,如果没有购买则不允许下载

最传统的解决方案是通过SpringBoot后端直接鉴权并传输文件到浏览器,但是Tomcat对于大文件大量静态资源请求的处理效率确实堪忧。

我的想法是通过Nginx传输静态文件,但在nginx接受到请求时先将参数转发到后端接口,由后端接口判断权限,通过之后nginx再传输文件给浏览器

实现目标:

  1. 浏览器访问http://192.168.78.133/file/abcd.7z?token=tokenvalue
  2. Nginx截取文件名 abcd.7z和token值tokenvalue转发到后端接口http://192.168.78.133:8080/auth/file?file=abcd.7z&token=tokenvalue
  3. 后端判断权限和文件,没有权限返回到浏览器401,没有文件返回到浏览器404
sequenceDiagram
浏览器->>Nginx: http://192.168.78.133/file/abcd.7z?token=tokenvalue
Nginx--)后端接口: http://192.168.78.133:8080/auth/file?file=abcd.7z&token=tokenvalue
后端接口--)Nginx: 响应状态码 200 401 404
Nginx-) 浏览器: 200时正常传输文件,其它状态码则直接转给浏览器

准备工作:Nginx(版本高于1.5.4),这里我使用的是宝塔一键环境配置Nginx和auth_request模块

Nginx配置

server
{
    listen 80;
    server_name 192.168.78.133;
    index index.php index.html index.htm default.php default.htm default.html;
    root /www/wwwroot/ftp;

    #SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    #error_page 404/404.html;
    #SSL-END

    #ERROR-PAGE-START  错误页配置,可以注释、删除或修改
    #error_page 404 /404.html;
    #error_page 502 /502.html;
    #ERROR-PAGE-END

    #PHP-INFO-START  PHP引用配置,可以注释或修改
    include enable-php-00.conf;
    #PHP-INFO-END

    #REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
    include /www/server/panel/vhost/rewrite/192.168.78.133.conf;
    #REWRITE-END

    #禁止访问的文件或目录
    location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md)
    {
        return 404;
    }

    #一键申请SSL证书验证目录相关设置
    location ~ \.well-known{
        allow all;
    }

    #禁止在证书验证目录放入敏感文件
    if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
        return 403;
    }

    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
    {
        expires      30d;
        error_log /dev/null;
        access_log /dev/null;
    }

    location ~ .*\.(js|css)?$
    {
        expires      12h;
        error_log /dev/null;
        access_log /dev/null;
    }
    
    #对指定路径下的访问进行鉴权配置
    location /file 
    {
        set $token $arg_token; #获取url中param参数的token赋值给变量
        #没有token时返回401,不必要再请求后端接口
        if ($token = "")
        {
          return 401;
        }
        if ($request_uri ~* ^/(.*)/([^/]+)$) {  
          set $file $2;  
        }
        #没有获取到文件名时直接返回404
        if ($file = "") {  
          return 404;  
        }
        #将请求转发给鉴权接口
        auth_request /auth;
    }

    # 鉴权接口路径代理
    location /auth 
    {
        #仅允许nginx内部访问,外部访问404
        internal;
        #这个配置是不需要转发请求体相关的内容,参考的其它博主的配置
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        #比较常规的反向代理配置,如果有其它需求可以自己再添加
        proxy_set_header host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        #重点是这里,将token和file变量的内容转发给后端接口
        proxy_pass "http://192.168.78.1:8081/user/resource/auth?token=$token&file=$file";
    }
    
    access_log  /www/wwwlogs/192.168.78.133.log;
    error_log  /www/wwwlogs/192.168.78.133.error.log;
}

SpringBoot接口

@Operation(summary = "资源文件鉴权",description = "资源文件鉴权,用于nginx鉴权认证")
@GetMapping("/resource/auth")
public void checkResourceAuth(@RequestParam(required = false) String token,@RequestParam(required = false) String file,HttpServletResponse response){
    //判断token是否存在
    if(!StringUtils.hasText(token)){
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return;
    }
    //判断token是否有效,并根据token获取用户主键
    /**
    * 我使用的是Sa-token框架,没用SpringSecurity的原因是SpringSecurity太重,项目体量小用不上,还有一个原因是SpringSecurity没学透彻,大佬们有什么比较好的学习资料请分享一下,谢谢
    */
    Object loginId = StpUtil.getStpLogic().getLoginIdByToken(token);
    if(loginId==null){
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return;
    }
    //校验文件名是否未空
    if(!StringUtils.hasText(file)){
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        return;
    }
    
    //TODO 校验文件是否存在

    //校验用户是否拥有该文件
    if(checkUserHaveFile(Long.valueOf(loginId.toString()),file)){
        response.setStatus(HttpServletResponse.SC_OK);
    }else {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    }
}