一、背景
现在的敏感文件存在minio系统中,但该系统可能存在未知漏洞,导致文件并不安全。
本方案默认文件系统为公开访问的前提下,保证敏感文件不发生泄露。
最终目标
1. 直接访问minio链接不可读
https://file.sdtlbco.com/chinapost/OTHERS/172b5b91b8ea4a22be30c60d4a3e3390.png.encrypt
2. 前端访问系统接口
http://downloadEncrypt/token=xxx&url=https://file.sdtlbco.com/chinapost/OTHERS/xxxx.png.encrypt
转二进制流获取图片
3. 区分敏感文件和非敏感文件
对于非敏感文件不受影响,无须额外操作。
二、方案详情
1. 存量数据加密更新
- 从数据库中,导出敏感文件url列表
- 批量下载至本机
- 本地将文件采取AES加密
- 上传至minio,并添加.encrypt后缀名,区分敏感与非敏感文件
- 更新数据库url
- 删除原有文件
2. 新增数据上传
- 对于敏感文件,采用/upload_encrypt接口上传
- 服务端接收后,对文件进行AES加密,再传至minio
3. 下载
- 前端区分文件名是否包含.encrypt,若包含,则通过敏感文件下载接口下载
- 敏感文件下载接口:业务系统鉴权后,从minio下载至服务端,解密后,再传至前端
三、实施
存量数据迁移
1. 通过mysql导出文件url列表
SELECT ITEM_VALUE FROM `tbl_promotion_apply_item` where TAG_TYPE = "upload" and item_value like "https://file.sdtlbco.com%"
2. 切割item_value与多线程下载
由于item_value可能由多个url组成,如url1,url2,url3,所以使用python进行切割并下载至本机
import urllib.request
import threadpool
import os
from alive_progress import alive_bar
rawFolder = "./raw/"
ThreadNum = 16
def download(url):
filename = url.split("/")[-1]
# 判断是否已经下载过
if(os.path.exists(rawFolder+filename)):
return
urllib.request.urlretrieve(url, rawFolder+filename)
def callBack(r,m):
print(r)
bar()
if __name__ == '__main__':
threadPool = threadpool.ThreadPool(ThreadNum)
downloadList = []
with open("raw_url_list.txt") as raw_url_list:
for urlStr in raw_url_list:
urlStr = urlStr.strip()
if urlStr == "":
continue
urlList = urlStr.split(",")
for url in urlList:
downloadList.append(url)
with alive_bar(len(downloadList), force_tty=True) as bar:
rs = threadpool.makeRequests(download, downloadList,callback=callBack)
for req in rs:
threadPool.putRequest(req)
threadPool.wait()
3. 对本地文件进行加密,重命名
秘钥存配置文件,配置文件加密参考这篇文章
https://blog.uuorb.com/archives/yamlencry
public class SecurityUtil {
// 固定密钥,在生产中通过配置文件获取
private static final String BASE64_SECRET = "";
/**
* 加密解密的byte[]
*/
private final static byte[] SECRET_BYTES = Base64.decode(BASE64_SECRET);
/**
* 根据秘钥得到aes对象
*/
private final static AES aes = SecureUtil.aes(SECRET_BYTES);
public static byte[] encrypt2Byte(InputStream inputStream) {
byte[] out = aes.encrypt(inputStream);
return out;
}
}
4. 上传minio
5. 修改数据库链接
6. 验证
2. 业务系统更新
前端
- 对于所有需要预览图片的前端组件,根据后缀名进行判断,若为加密链接,则通过解密接口转码预览。
服务端
- 上传接口
@ApiOperation("加密上传单个文件")
@PostMapping("uploadEncrypt")
public AjaxResult uploadFileEncrypt(MultipartFile file) {
try {
// 对文件进行加密
MinioFile minioFile = minIoUtil.uploadEncrypt(file, FileType.OTHERS);
AjaxResult ajax = AjaxResult.success();
ajax.put("url", minioFile.getPreviewUrl());
ajax.put("fileName", minioFile.getFileName());
ajax.put("newFileName", minioFile.getFileName());
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
- 加密并上传
public MinioFile uploadEncrypt(MultipartFile file, FileType fileType) {
String fileName = file.getOriginalFilename();
String suffix = FileNameUtil.extName(fileName);
String newFileName = IdUtil.fastSimpleUUID();
String objectName = fileType.name() + "/" + newFileName + "." + suffix + ".encrypt";
String url = endPoint + "/" + bucketName + "/" + objectName;
InputStream inputStream = file.getInputStream();
byte[] encryptData = SecurityUtil.encrypt2Byte(inputStream);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encryptData);
// 对文件进行加密
PutObjectArgs objectArgs = PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(byteArrayInputStream, encryptData.length, -1)
.contentType("application/octet-stream")
.build();
minioClient.putObject(objectArgs);
return MinioFile.builder()
.previewUrl(url)
.fileUrl(url)
.fileName(newFileName)
.build();
}
- 根据url,解密下载
@ApiOperation("访问加密的文件")
@GetMapping("downloadEncrypt")
@Anonymous
public void downloadEncrypt(@RequestParam("fileUrl") String fileUrl, @RequestParam("token") String token, HttpServletResponse response) {
boolean b = tokenService.verifyToken(token);
if (!b) {
return;
}
try {
byte[] bytes = HttpUtil.downloadBytes(fileUrl);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
byte[] decryptedBytes = SecurityUtil.decrypt(byteArrayInputStream);
response.setContentType(MediaType.IMAGE_PNG_VALUE);
// 将解密后的字节数组写入响应输出流
OutputStream outputStream = response.getOutputStream();
outputStream.write(decryptedBytes);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("下载文件失败", e);
}
}
- 不使用token过滤器,自定义判断token参数
public boolean verifyToken(String token) {
// 获取请求携带的令牌
if (StringUtil.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
return claims != null;
} catch (Exception ignored) {
return false;
}
}
return false;
}
五、影响与风险
1. 影响
- 大文件的加解密性能问题
- 文件经过两次下载的宽带损耗:minio到服务端,服务端到前端
2. 风险
存量文件的误删,漏处理问题