Nginx 权限控制文件预览和下载

Nginx Jul 31, 2020 1

@author: momo
@date: 2020-07-31 06:00

基于 Nginx + Java(SpringBoot) 实现带权限验证的静态文件服务器,支持文件下载、PDF预览和图片预览

需要注意的是,无需权限判断的图片不建议使用此方法,大量的图片访问会增加后台服务器的处理压力。

一、实现原理

本质上是使用了X-Sendfile功能来实现,X-Sendfile 是一种将文件下载请求重定向到Web 服务器处理的机制,该Web服务器只需负责处理请求(例如权限验证),而无需执行读取文件并发送给用户的任务。

X-Sendfile可显著提高后台服务器的性能,消除了后端程序既要读文件又要处理发送的压力,尤其是处理大文件下载的情形下!

Nginx也具有此功能,但实现方式略有不同。在Nginx中,此功能称为X-Accel-Redirect

用户请求文件,权限控制时序图:

二、实现步骤

1. Nginx配置

1、静态文件通过file_server访问,会被设置为internal,即只能内部访问不允许外部直接访问。
2、所有静态资源请求均被重定向到Java后台,经过权限验证后才能访问。

# 文件下载服务
location ^~ /file_server {
    # 内部请求(即一次请求的Nginx内部请求),禁止外部访问,重要。
    internal;
    # 文件路径
    alias U:/file/private/;
    limit_rate 200k;
    # 浏览器访问返回200,然后转由后台处理
    error_page 404 =200 @backend;
}

# 文件下载鉴权
location @backend {
    # 去掉访问路径中的 /file_server/,然后定义新的请求地址。
    rewrite ^/file_server/(.*)$ /uecom/attach/$1 break;
    # 这里的url后面不可以再拼接地址
    proxy_pass http://192.168.12.68:9023;
    proxy_redirect   off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

2. Java SpringBoot 后台权限验证

用户请求只需要传递附件参数和身份token,不再需要传递服务器的真实路径。例如:

http://192.168.12.68:9022/file_server/preview_file?id=13e9d1887b46455a96842c503c2434cb&access_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ3ZWJfMSIsImV4cCI6MTU5NjE1NzkxOH0.eR4yYlDjIMXZmNNTq2gdghkYhw6I30NgZlvuPUmRoyk

2.1 权限校验文件下载

response.setHeader("X-Accel-Redirect", "/file_server" + attach.getAttachPath()); ,重点是X-Accel-Redirect配置返回服务器文件的真实路径,该路径返回后由Nginx内部请求处理,不会暴露给请求用户。

/**
 * @describe 使用token鉴权的文件下载
 * @author momo
 * @date 2020-7-30 13:44
 * @param id 文件唯一编码 uuid
 * @return void
 */
@GetMapping("/download_file")
public void downloadFile(@NotNull String id) throws IOException {
    HttpServletResponse response = super.getHttpServletResponse();
    // 通过唯一编码查询附件
    Attach attach = attachService.getOne(new LambdaQueryWrapper<Attach>().eq(Attach::getUuid, id));
    if (attach == null) {
        // 附件不存在,跳转404
        this.errorPage(404);
        return;
    }

    // 非公开,即需要权限访问的文件进行鉴权
    if (attach.getIfPublic().equals(Constant.FALSE)) {
        // 从访问token中获取用户id。 token也可以通过 参数 access_token 传递
        Integer userId = UserKit.getUserId();
        if (userId == null || ) {
           // 无权限访问,跳转403
            this.errorPage(403);
            return;
        }
    }

    // 公开 或 已被授权访问
    // 文件下载
    response.setHeader("Content-Disposition", "attachment; filename=\"" + new String(attach.getAttachName().getBytes("GBK"), "iso-8859-1") + "\"");
    // 文件以二进制流传输
    response.setHeader("Content-Type", "application/octet-stream;charset=utf-8");
    // 返回真实文件路径交由 Nginx 处理,保证前端无法看到真实的文件路径。
    // 这里的 "/file_server" 为 Nginx 中配置的下载服务名
    response.setHeader("X-Accel-Redirect", "/file_server" + attach.getAttachPath());
    // 限速,单位字节,默认不限
    // response.setHeader("X-Accel-Limit-Rate","1024");
    // 是否使用Nginx缓存,默认yes
    // response.setHeader("X-Accel-Buffering","yes");
	response.setHeader("X-Accel-Charset", "utf-8");
    
    // 禁止浏览器缓存
    response.setHeader("Pragma", "No-cache");
    response.setHeader("Cache-Control", "No-cache");
    response.setHeader("Expires", "0");
}

后台可配置属性:

Content-Type: 
Content-Disposition: : 
Accept-Ranges: 
Set-Cookie: 
Cache-Control: 
Expires: 

# 设置文件真实路径的URI,默认void
X-Accel-Redirect: void
# 限制下载速度,单位字节。默认不限速度off。
X-Accel-Limit-Rate: 1024|off
# 设置此连接的代理缓存,将此设置为no将允许适用于Comet和HTTP流式应用程序的无缓冲响应。将此设置为yes将允许响应被缓存。默认yes。
X-Accel-Buffering: yes|no
# 如果已传输过的文件被缓存下载,设置Nginx文件缓存过期时间,单位秒,默认不过期 off。
X-Accel-Expires: off|seconds
# 设置文件字符集,默认utf-8。
X-Accel-Charset: utf-8

2.2 权限校验文件预览

文件下载和文件预览本质上没有区别,只是response返回时告诉浏览器执行下载还是执行打开预览。即response.setHeader("Content-Disposition", "inline; filename="xxx.pdf");, 然后修改返回数据的格式Content-Type即可。

Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联inline)的形式(即网页或者页面的一部分),还是以附件attachment)的形式下载并保存到本地。

/**
 * @describe 使用token鉴权的文件预览(支持pdf和图片)
 * @author momo
 * @date 2020-7-30 13:44
 * @param id 文件唯一编码 uuid
 * @return void
 */
@GetMapping("/preview_file")
public void previewFile(@NotNull String id) throws IOException {
    HttpServletResponse response = super.getHttpServletResponse();
    // 通过唯一编码查询附件
    Attach attach = attachService.getOne(new LambdaQueryWrapper<Attach>().eq(Attach::getUuid, id));
    if (attach == null) {
        // 附件不存在,跳转404
        this.errorPage(404);
        return;
    }

    // 非公开,即需要权限访问的文件进行鉴权
    if (attach.getIfPublic().equals(Constant.FALSE)) {
        // 从访问token中获取用户id。 token也可以通过 参数 access_token 传递
        Integer userId = UserKit.getUserId();
        if (userId == null || ) {
           // 无权限访问,跳转403
            this.errorPage(403);
            return;
        }
    }
    
    // 公开 或 已被授权访问
    // 文件直接显示
    response.setHeader("Content-Disposition", "inline; filename=\"" + new String(attach.getAttachName().getBytes("GBK"), "iso-8859-1") + "\"");
    if ("pdf".equals(attach.getAttachType().toLowerCase())) {
        // PDF
        response.setHeader("Content-Type", "application/pdf;charset=utf-8");
    } else {
        // 图片
        response.setHeader("Content-Type", "image/*;charset=utf-8");
    }
    // 返回真实文件路径交由 Nginx 处理,保证前端无法看到真实的文件路径。
    // 这里的 "/file_server" 为 Nginx 中配置的下载服务名
    response.setHeader("X-Accel-Redirect", "/file_server" + attach.getAttachPath());
    // 浏览器缓存 1 小时
    response.setDateHeader("Expires", System.currentTimeMillis() + 1000 * 60 * 60);
}

三、扩展功能

1. 下载统计、访问日志

// 自由发挥

2. 下载限速

// 限速,单位字节,默认不限
response.setHeader("X-Accel-Limit-Rate","1024");

3. 防盗链

 //判断 Referer
String referer = request.getHeader("Referer");
if (referer == null || !referer.startsWith("https://www.itmm.wang")) {
    // 无权限访问,跳转403
    this.errorPage(403);
    return;
}

4. X-Sendfile

X-Sendfile是一项功能,每个代理服务器都有自己不同的实现。

Web Server Header
Nginx X-Accel-Redirect
Apache X-Sendfile
Lighttpd X-LIGHTTPD-send-file
Squid X-Accelerator-Vary

使用 X-SendFile 的缺点是你失去了对文件传输机制的控制。例如如果你希望在完成文件下载后执行某些操作,比如只允许用户下载文件一次,这个 X-Sendfile 是没法做到的,因为请求交给了后台,你并不知道下载是否成功。

参考资料

  1. X-Sendfile-从Web应用程序有效地提供大型静态文件
  2. Nginx-X-Accel-Redirect
  3. Nginx -XSendfile
  4. Content-Disposition
  5. Http Content-Type

目录

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.