我的需求是,后台生成了合同文件,用户需要进行预览,如果采用流的实现方式的话,会涉及到输入流、输出流,性能开销较大,所以采用的是直接访问文件,这里就涉及到一个问题,就是
- 需要设置权限访问
- 不能把文件地址暴漏在浏览器
基于上述两个要求,我想到了forward转发,在权限校验通过之后,转发到文件服务地址。起初我的代码是这样的:
//前端代码
window.open("/house/contract/infoHouseCntr?cntrId=" + obj.data.idStr, "_blank");
@RequestMapping(value = "/infoHouseCntr", method = RequestMethod.GET)
public void infoHouseCntr(Long cntrId, HttpServletRequest request, HttpServletResponse response) throws Exception {
String path = iHouseCntrServiceImpl.infoHouseCntr(cntrId);
request.getRequestDispatcher("/cntr-doc/"+path).forward(request,response);
// response.sendRedirect("/cntr-doc/"+path); //有效
}
// nginx配置
location ^~ /cntr-doc/ {
root D:/**/**/templates/cntr-doc/;
}
在转发的时候,访问nginx,返回真正的文件路径到浏览器。
但是在写完之后,一直404,查看nginx的日志,错误日志、成功日志,都没有打印日志,所以,怀疑是转发后并没有访问nginx,也就不会被浏览。为了验证想法,就换成了重定向,因为重定向时,浏览器会重新发送请求,得到的结果是重定向可以访问到文件,不过地址也被暴漏到了浏览器。所以,通过这个,也得到了重定向与转发一个潜在的区别,但是在用到时,有可能就是致命的:转发不会经过nginx,而重定向会再次经过nginx。
接下来,开始实现正确的实现方式
参考博客:https://bbs.huaweicloud.com/blogs/360823
本质上是使用了X-Sendfile功能来实现,X-Sendfile 是一种将文件下载请求重定向到Web 服务器处理的机制,该Web服务器只需负责处理请求(例如权限验证),而无需执行读取文件并发送给用户的任务。
X-Sendfile可显著提高后台服务器的性能,消除了后端程序既要读文件又要处理发送的压力,尤其是处理大文件下载的情形下!
Nginx也具有此功能,但实现方式略有不同。在Nginx中,此功能称为X-Accel-Redirect。重点是X-Accel-Redirect配置返回服务器文件的真实路径,该路径返回后由Nginx内部请求处理,不会暴露给请求用户。
nginx配置
- 静态文件通过file_server访问,会被设置为internal,即只能内部访问不允许外部直接访问。
- 所有静态资源请求均被重定向到Java后台,经过权限验证后才能访问。
# 文件服务
location ^~ /file {
# 内部请求(即一次请求的Nginx内部请求),禁止外部访问,重要。
internal;
# 文件路径
alias D:/data/ideaworkspace/apm/OnlineHouseAchieve/src/main/resources/templates/cntr-doc/;
limit_rate 200k;
# 浏览器访问返回200,然后转由后台处理
error_page 404 =200 @backend;
}
# 文件下载鉴权
location @backend {
# 去掉访问路径中的 /file/,然后定义新的请求地址。/uecom/attach/$1 被替换的路径,$1表示参数不变
rewrite ^/file/(.*)$ /aa/bb/$1 break; # 这里注意一下替换的路径
# 这里的url后面不可以再拼接地址
proxy_pass http://127.0.0.1:8080;
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;
}
前端访问
window.open("/file/preview_file?cntrId=" + obj.data.idStr, "_blank");
后台代码
@GetMapping("/aa/bb/preview_file")
public void previewFile(Long cntrId,HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = iHouseCntrServiceImpl.infoHouseCntr(cntrId);
// 已被授权访问
// 文件直接显示
response.setHeader("Content-Disposition", "inline; filename=\"" + new String(path.getBytes("GBK"), "iso-8859-1") + "\"");
if (path.endsWith("pdf")) {
// PDF
response.setHeader("Content-Type", "application/pdf;charset=utf-8");
} else {
// 图片
response.setHeader("Content-Type", "image/*;charset=utf-8");
}
// 返回真实文件路径交由 Nginx 处理,保证前端无法看到真实的文件路径。
// 这里的 "/file" 为 Nginx 中配置的下载服务名
response.setHeader("X-Accel-Redirect", "/file/" + path);
// 浏览器缓存 1 小时
response.setDateHeader("Expires", System.currentTimeMillis() + 1000 * 60 * 60);
}
同样的,下载也是这个意思,只不过需要在response返回时告诉浏览器执行下载还是执行打开预览。即response.setHeader(“Content-Disposition”, "inline; filename=“xxx.pdf”);, 然后修改返回数据的格式Content-Type即可。告诉浏览器,你是想预览还是下载。
/**
* @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.getById(id);
if (attach == null) {
// 附件不存在,跳转404
this.errorPage(404);
return;
}
// 从访问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
比如:
// 限速,单位字节,默认不限
response.setHeader(“X-Accel-Limit-Rate”,“1024”);
防盗链
//判断 Referer
String referer = request.getHeader("Referer");
if (referer == null || !referer.startsWith("https://www.itmm.wang")) {
// 无权限访问,跳转403
this.errorPage(403);
return;
}
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 是没法做到的,因为请求交给了后台,你并不知道下载是否成功。