做的项目中多个模块涉及到附件、图片、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目录下执行:./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在请求头中,功能正常。
表单图片,token在请求URL中,预览正常。
如果直接在浏览器请求,则失败:
总结
Nginx文件服务鉴权使用ngx_http_auth_request_module
模块实现,功能简单实用。当然还有其他的技术方案可以来实现,如第三方的扩展模块x-sendfile
等。可根据自己项目的实际情况灵活处理。