问题
背景介绍
前后端分离框架中,前端在发起Ajax请求时,在Header携带Token值,后端获取该Token进行权限校验。
问题描述
在下载文件的场景,大部分浏览器是不支持Ajax请求文件下载,所以通常做法是直接请求下载地址,这时候是无法通过代码设置Header的参数,也就是无法知道当前用户信息,如何进行权限控制?
解决方案
该问题的痛点是数据权限问题。
初步思路
- 在用户要下载的时候,先进行权限校验
- 权限不足时,进行信息提示
- 权限足够时,则为用户生成一次性的下载链接(下载一次后失效,保证数据安全)
设计思路
- 流程设计
- 抽象功能
- 具体实现
流程设计
业务模块前端:业务模块的前端代码,例如用户信息下载的前端代码。
业务服务后端:业务模块的后端代码(非介绍重点,所以不做细节区分)。
文件服务类:文件服务的业务类,主要是处理业务逻辑,例如:生成下载地址的唯一编号,以及唯一编号映射的参数信息,文件生成。
文件控制类:提供统一的下载地址格式,放开权限。

代码设计
基于框架考虑,以下功能抽象出来:
- 持久化数据功能抽象(不同项目的具体情况不同)
- 生成文件功能抽象(因为不同业务生成文件的步骤是不一样的)
代码依赖关系

核心代码
DownloadController - 文件下载控制器,不做权限控制
package com.accloud.core.file.download.controller;
import com.accloud.core.file.download.FileResult;
import com.accloud.core.file.download.service.DownloadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
/**
* 文件下载控制器
*
* @author guoyu.huang
* @since 2020-10-16
*/
@Slf4j
@Controller
@RequestMapping("/download")
public class DownloadController {
@Autowired
private DownloadService downloadService;
@GetMapping("/{code}")
public void download(@PathVariable(value = "code") String code, HttpServletResponse response) throws IOException {
try {
FileResult fileResult = downloadService.getFile(code);
String filename = fileResult.getDownloadFilename();
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
response.setCharacterEncoding("UTF-8");
FileInputStream fis = new FileInputStream(fileResult.getFile());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
bos.close();
byte[] fileByte = bos.toByteArray();
OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
toClient.write(fileByte);
toClient.flush();
toClient.close();
if (fileResult.isRemoveFileAfterDownload()) {
fileResult.getFile().delete();
}
} catch (Exception e) {
log.error(e.getMessage());
response.reset();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(e.getMessage());
}
}
}
DownloadService - 文件服务类
package com.accloud.core.file.download.service;
import cn.hutool.core.util.StrUtil;
import com.accloud.core.exception.BusinessException;
import com.accloud.core.file.download.FileResult;
import com.accloud.core.file.download.code.DownloadCodeContent;
import com.accloud.core.file.download.code.IDownloadCodeService;
import com.accloud.core.file.handler.IFileHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* 下载服务类
*
* @author guoyu.huang
* @since 2020-10-16
*/
@Component
public class DownloadService {
@Autowired
private IDownloadCodeService downloadCodeService;
@Autowired(required = false)
private List<IFileHandler> fileHandlerList;
@Autowired
private Environment environment;
private String downloadServerPath;
@PostConstruct
public void init() {
downloadServerPath = environment.getProperty("business.downloadServer");
if (StrUtil.isBlank(downloadServerPath)) {
String defaultDownloadServerPath = "http://127.0.0.1:8808";
downloadServerPath = defaultDownloadServerPath;
}
}
/**
* 获取下载地址
*
* @param type
* @param parameter
* @return
*/
public String getDownloadUrl(String type, String parameter) {
String code = downloadCodeService.buildCode(type, parameter);
return downloadServerPath + "download/" + code;
}
/**
* 获取文件
*
* @param code
* @return
*/
public FileResult getFile(String code) {
DownloadCodeContent content = downloadCodeService.getContent(code);
if (content == null) {
throw new BusinessException("无此文件");
} else {
downloadCodeService.delete(code);
if (fileHandlerList == null) {
throw new BusinessException("功能异常,目前文件处理器列表为空");
} else {
for (IFileHandler fileHandler : fileHandlerList) {
if (fileHandler.handle(content.getType())) {
return fileHandler.getFileResult(content.getParameter());
}
}
throw new BusinessException("功能异常,无法处理类型为:%s 的文件", content.getType());
}
}
}
}
FileResult - 文件结果
/**
* 文件结果
*
* @author guoyu.huang
* @since 2020-10-16
*/
@NoArgsConstructor
@RequiredArgsConstructor
@Data
public class FileResult {
/**
* 下载的文件名
*/
@NonNull
private String downloadFilename;
/**
* 文件
*/
@NonNull
private File file;
/**
* 下载后是否删除文件
*/
private boolean removeFileAfterDownload;
}
IFileHandler - 文件生成处理器接口
/**
* 文件生成处理器接口
*
* @author guoyu.huang
* @since 2020-10-16
*/
public interface IFileHandler {
/**
* 判断能否处理
*
* @param type 传入类型,用于判断是否能够处理
* @return
*/
boolean handle(String type);
/**
* 获取文件结果
*
* @param parameter
* @return
*/
FileResult getFileResult(String parameter);
}
IDownloadCodeService - 下载编号接口
/**
* 下载编号接口
*
* @author guoyu.huang
* @since 2020-10-26
*/
public interface IDownloadCodeService {
/**
* 生成编号
*
* @param type
* @param parameter
* @return
*/
String buildCode(String type, String parameter);
/**
* 根据编号获取内容
*
* @param code
* @return
*/
DownloadCodeContent getContent(String code);
/**
* 删除编号
*
* @param code
* @return
*/
void delete(String code);
}
在前后端分离的框架中,由于Ajax请求文件下载的限制,无法通过Header传递用户信息进行权限校验。本文提出了一种解决方案:用户在下载前先进行权限检查,若权限足够则生成一次性下载链接,确保数据安全性。流程包括业务模块前端验证权限、生成唯一下载链接及文件服务处理。核心代码包括下载控制器、文件服务类和接口。

2809

被折叠的 条评论
为什么被折叠?



