Nginx文件下载预览加权限验证的思考和实现

做的项目中多个模块涉及到附件、图片、PDF/Excel等文件的处理,包括预览、导出和下载等功能。对于体积较小的文件,可以直接由后端以流形式传输给前端处理;而较大的文件则需要通过nginx进行转发。但是如果nginx中不设置鉴权服务,可能会造成数据泄露的风险。为保障数据安全,有必要在nginx中加上鉴权功能,对文件传输和访问进行控制。

先来说思路,为nginx增加ngx_http_auth_request_module模块,实现基于子请求的结果的客户端的授权。如果子请求返回2xx响应码,则允许访问。如果它返回401或403,则访问被拒绝并显示相应的错误代码。子请求返回的任何其他响应代码都被认为是错误的。即在原来的基础上加了一层后端鉴权服务。

1. 下载扩展鉴权模块ngx_http_auth_request_module
git clone https://github.com/PiotrSikora/ngx_http_auth_request_module.git
查看nginx编译安装时安装了哪些模块

Nginx文件下载预览加权限验证的思考和实现_第1张图片
通过在nginx目录下执行:./nginx -V,可以看出编译安装使用了--prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module

2. 安装新模块ngx_http_auth_request_module

进入nginx的源码包(一般编译安装完nginx后会删除,如果本地没有了的话就去官网下个源码包),加入需要安装的模块,重新编译,在编译参数最后添加 --add-module=/usr/local/ngx_http_auth_request_module

# ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --add-module=/usr/local/ngx_http_auth_request_module
# make    // 这里不要make install,不然就覆盖了原来的nginx!!!

注意:这里只需要make就行了,千万不要make install,不然就覆盖了原来的nginx!!!

3. 修改nginx配置
# Managed static resources
server {
    listen       15550;

    location /zcauth {
        internal;
		# 鉴权服务器的地址
        proxy_pass http://127.0.0.1:8081/auth/token;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
    }

    # ROOT for ALL Files(数据文件根路径)
    location / {
        auth_request /zcauth;
		auth_request_set $auth_status $upstream_status;

        root /home/Filedata/;

        #add_header Access-Control-Allow-Origin $http_origin;
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods *;
        add_header Access-Control-Allow-Headers *;

        #add_header Content-Type "application/octet-stream";
        autoindex on;
    }
}

在需要请求身份验证的位置,指定auth_request指令,在该指令中指定将授权子请求转发到的内部位置/zcauth,在这里,对于每个请求,都会向内部/zcauth位置发出一个子请求。在/zcauth内的proxy_pass指令,将把身份验证子请求代理到身份验证(鉴权)服务器或服务。由于身份验证子请求将丢弃请求体,因此需要将proxy-pass-request-body指令设置为off,并将Content-Length头设置为空字符串。使用带有proxy_set_header指令的参数传递完整的原始请求URI。(作为选择,可以使用auth_request_set指令根据子请求的结果设置变量值)。

4. 实现后端鉴权服务
package com.yorma.staticauth.controller;

import cn.hutool.core.date.DateUtil;
import com.yorma.util.MD5Util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;

import static cn.hutool.core.text.CharSequenceUtil.isBlank;
import static cn.hutool.core.text.CharSequenceUtil.isNotBlank;
import static cn.hutool.core.util.ObjectUtil.isEmpty;
import static com.yorma.staticauth.domain.Const.*;

/**
 * auth验证
 *
 * @author ZHANGCHAO
 * @version 1.0.0
 * @date 2023/7/17 11:34
 */
@Slf4j
@RestController
@RequestMapping("/auth")
public class NginxAuthRequestController {
    
 	public static final String HEADER_TOKEN = "X-Access-Token";
    public static final String USERNAME = "username";
    public static final String PREFIX_USER_TOKEN_INFO = "prefix_user_info_token_";
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/token")
    public void auth(HttpServletRequest request, HttpServletResponse response) {
        // 通过 HttpServletRequest 获取请求头中的 token
        String token = request.getHeader(HEADER_TOKEN);
        log.info("Token from HttpServletRequest: " + token);
        String uri = request.getHeader("X-Original-URI");
        log.info("URI from HttpServletRequest: " + uri);
        if (isBlank(token)) {
            if (isNotBlank(uri) && uri.contains("X-Access-Token=")) {
                String tokens = uri.split("X-Access-Token=")[1];
                String match = MD5Util.MD5Encode(DateUtil.formatDate(new Date()) + "F70C5833-7D02-47A6-B8D5-93B97CBAF87F", "utf-8");
                log.info("本地MD5后的值: " + match + " | 过来的值:" + tokens);
                if (match.equals(tokens)) {
                    response.setStatus(200);
                    return;
                }
            }
            response.setStatus(401);
            return;
        }
        String username = "";
        if (redisTemplate.hasKey(PREFIX_USER_TOKEN_INFO + token)) {
            HashMap<String, Object> claim = (HashMap<String, Object>) redisTemplate.opsForValue().get(PREFIX_USER_TOKEN_INFO + token);
            if (isEmpty(claim)) {
                response.setStatus(403);
                return;
            }
            username = String.valueOf(claim.get(USERNAME));
        }
        if (isBlank(username)) {
            response.setStatus(403);
            return;
        }
        response.setStatus(200);
    }
}

支持两种方式:1. token放到请求头Header里的正常token 2. 直接放请求URL中的按一定规则生成的32位MD5 token。先取Header里的,如果取不到则取URL中的。

5. 测试效果

先来个导出Excel功能,token在请求头中,功能正常。
Nginx文件下载预览加权限验证的思考和实现_第2张图片

表单图片,token在请求URL中,预览正常。
Nginx文件下载预览加权限验证的思考和实现_第3张图片

如果直接在浏览器请求,则失败:
Nginx文件下载预览加权限验证的思考和实现_第4张图片

总结

Nginx文件服务鉴权使用ngx_http_auth_request_module模块实现,功能简单实用。当然还有其他的技术方案可以来实现,如第三方的扩展模块x-sendfile等。可根据自己项目的实际情况灵活处理。

你可能感兴趣的:(nginx,运维)