一、环境
SpringBoot 2.2.5.RELEASE
fastdfs-client 1.27.2
二、业务要求
由于FastDFS分布式文件系统预览图片时需要暴露FastDFS所在服务器的IP、端口、文件路径,这样对于网络安全要求较高的环境,是不允许出现的。
例如以下:
http//:xxxx:10000/group1/M00/01/02/CgADEl3PgkSSCAGAcwwwy0BBAAAKiXEYGJQ070.png?fileName=logo.png
效果图:
三、方案一
对文件路径进行加密处理
/preview/img/56801203E60385071EDBCA415054B57CD4EB5A943DC74E0D2559C97DF4270809F4C8F851078CE58BE2F1BC86D2BB4F65E8CC698C1E13DAA4C175C426911CEDAF6170B7542D11FC18?fileName=7.png
1. 后端代码
1. 1 加密工具类
public class EncryptUtil {
public static final String DES = "DES";
/**
* 加密key,可以在配置文件中,进行定期更换
*/
public static final String DES_KEY = "xxxxx";
/**
* 编码格式;默认使用uft-8
*/
public String charset = "utf-8";
/**
* DES
*/
public int keysizeDES = 0;
public static EncryptUtil me;
private EncryptUtil() {
//单例
}
//双重锁
public static EncryptUtil getInstance() {
if (me == null) {
synchronized (EncryptUtil.class) {
if (me == null) {
me = new EncryptUtil();
}
}
}
return me;
}
/**
* 使用KeyGenerator双向加密,DES/AES,注意这里转化为字符串的时候是将2进制转为16进制格式的字符串,不是直接转,因为会出错
*
* @param res 加密的原文
* @param algorithm 加密使用的算法名称
* @param key 加密的秘钥
* @param keysize
* @param isEncode
* @return
*/
private String keyGeneratorES(String res, String algorithm, String key, int keysize, boolean isEncode) {
try {
KeyGenerator kg = KeyGenerator.getInstance(algorithm);
if (keysize == 0) {
byte[] keyBytes = charset == null ? key.getBytes() : key.getBytes(charset);
kg.init(getSecureRandom(keyBytes));
} else if (key == null) {
kg.init(keysize);
} else {
byte[] keyBytes = charset == null ? key.getBytes() : key.getBytes(charset);
kg.init(keysize, getSecureRandom(keyBytes));
}
SecretKey sk = kg.generateKey();
SecretKeySpec sks = new SecretKeySpec(sk.getEncoded(), algorithm);
Cipher cipher = Cipher.getInstance(algorithm);
if (isEncode) {
cipher.init(Cipher.ENCRYPT_MODE, sks);
byte[] resBytes = charset == null ? res.getBytes() : res.getBytes(charset);
return parseByte2HexStr(cipher.doFinal(resBytes));
} else {
cipher.init(Cipher.DECRYPT_MODE, sks);
return new String(cipher.doFinal(parseHexStr2Byte(res)));
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* (解决在linux中无法解密问题)
* @param keyBytes
* @return
* @throws NoSuchAlgorithmException
*/
private SecureRandom getSecureRandom(byte[] keyBytes) throws NoSuchAlgorithmException {
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG") ;
secureRandom.setSeed(keyBytes);
return secureRandom;
}
/**
* 将二进制转换成16进制
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 将16进制转换为二进制
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1) {
return null;
}
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**
* 使用DES加密算法进行加密(可逆)
*
* @param res 需要加密的原文
* @param key 秘钥
* @return
*/
public String DESencode(String res, String key) {
return keyGeneratorES(res, DES, key, keysizeDES, true);
}
/**
* 对使用DES加密算法的密文进行解密(可逆)
*
* @param res 需要解密的密文
* @param key 秘钥
* @return
*/
public String DESdecode(String res, String key) {
return keyGeneratorES(res, DES, key, keysizeDES, false);
}
}
1.2 文件路径处理工具类
public class FileUrlEncryptUtils {
/**
* 拼接文件路径
*
* @param path
* 接口地址
* @param val
* 文件路径
* @return
* 加密后的文件路径
*/
public static String concatUrl(String path,String val){
if(StringUtils.isBlank(val)){
return val;
}
String[] split = val.split("fileName=");
String fileName = StringUtils.EMPTY;
if(split.length>0){
fileName = split[split.length-1];
}
String encodeStr = EncryptUtil.getInstance().DESencode(val,EncryptUtil.DES_KEY);
return path+ encodeStr+ "?fileName="+ fileName;
}
}
1.3 配置文件application.yml配置接口地址
#图片预览地址
preview-img-path: /preview/img/
#图片下载地址
download-img-path: /download/img/
1.4 后端返回文件路径时进行加密
/**
* 预览图片的接口地址,在配置文件中配置
*/
@Value("${preview-img-path}")
private String previewImgPath;
/**
* 预览图片的接口地址,在配置文件中配置
*/
@Value("${download-img-path}")
private String downloadImgPath;
//使用文件的地方,调用方法进行加密
String previewImgPath = FileUrlEncryptUtils.concatUrl(previewImgPath,val)
String downloadImgPath= FileUrlEncryptUtils.concatUrl(downloadImgPath,val)
1.5 后端文件处理
/**
* 下载附件
*
* @param fileName
* 文件名称
* @param response
* 响应流
* @throws Exception
* 遇到任何异常时
*/
@GetMapping(value = "/download/img/{fileUrl}")
public void downloadFile(@PathVariable(value = "fileUrl") String fileUrl,@RequestParam("fileName") String fileName, HttpServletResponse response, HttpServletRequest request){
try {
String url = fileUrl;
url = EncryptUtil.getInstance().DESdecode(url, EncryptUtil.DES_KEY);
resourceService.download(url,fileName, response, request);
} catch (Exception e) {
e.printStackTrace();
log.error("------------------下载文件异常-----------------"+e.getMessage());
}
}
/**
* 预览图片
*
* @param fileName
* 文件名称
* @param response
* 响应流
* @throws Exception
* 遇到任何异常时
*/
@GetMapping(value = "/preview/img/{fileUrl}")
public void previewImg(@PathVariable(value = "fileUrl") String fileUrl,@RequestParam("fileName") String fileName, HttpServletResponse response, HttpServletRequest request){
try {
String url = fileUrl;
url = EncryptUtil.getInstance().DESdecode(url, EncryptUtil.DES_KEY);
resourceService.previewImg(url,fileName, response,request);
} catch (Exception e) {
e.printStackTrace();
log.error("------------------预览图片异常-----------------"+e.getMessage());
}
}
/**
* 支持上传的文件类型
*/
private static final List<String> PREVIEW_IMG = Arrays.asList("image/png","image/jpeg","image/jpg","image/gif");
@Override
public void download(String fileUrl,String fileName,HttpServletResponse response, HttpServletRequest request)
throws IOException {
if (StringUtils.isBlank(fileUrl)) {
throw new MyException("文件路径不能为空");
}
byte[] bytes = fastDfsUtil.downloadFile(fileUrl);
if (null == bytes && bytes.length < 1) {
throw new MyException("文件路径不能为空");
}
response.addHeader("Pragma", "No-cache");
response.addHeader("Cache-Control", "No-cache");
response.setCharacterEncoding("UTF-8");
String suffix = fileUrl.split("\\.")[1];
if (StringUtils.isBlank(fileName)) {
fileName = String.valueOf(System.currentTimeMillis()) + sp + suffix;
}
fileName = new String(fileName.getBytes("ISO8859-1"), "UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
String s = fileUrl.split("/")[fileUrl.split("/").length - 1];
log.info("下载文件-》文件类型{}", request.getServletContext().getMimeType(s));
response.setContentType("application/json;charset=utf-8");
OutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
try {
bos.write(bytes, 0, bytes.length);
bos.flush();
} catch (Exception e) {
e.printStackTrace();
throw new MyException("文件下载异常");
} finally {
if (bos != null) {
bos.close();
}
if (out != null) {
out.close();
}
}
}
@Override
public void previewImg(String fileUrl,String fileName,HttpServletRequest request,HttpServletResponse response) throws IOException{
if (StringUtils.isBlank(fileUrl)) {
log.error("图片路径不能为空");
throw new myException("图片路径不能为空");
}
byte[] bytes = fastDfsUtil.downloadFile(fileUrl);
if (null == bytes && bytes.length < 1) {
throw new myException("图片预览异常");
}
response.addHeader("Pragma", "No-cache");
response.addHeader("Cache-Control", "no-store,No-cache");
response.setCharacterEncoding("UTF-8");
log.info("------------------图片名称-----------------" + fileName);
String s = fileUrl.split("/")[fileUrl.split("/").length - 1];
String mimeType = request.getServletContext().getMimeType(s);
log.info("图片预览-》图片类型{}", mimeType);
if (!PREVIEW_IMG.contains(mimeType)) {
throw new myException("图片类型不匹配");
}
response.setContentType(request.getServletContext().getMimeType(s)+";charset=utf-8");
OutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
try {
bos.write(bytes, 0, bytes.length);
bos.flush();
} catch (Exception e) {
e.printStackTrace();
throw new myException("图片预览异常");
} finally {
if (bos != null) {
bos.close();
}
if (out != null) {
out.close();
}
}
}
2 前端代码
<!-- 图片预览 -->
<el-dialog
title="浏览图像"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="40%">
<el-image style="max-width:300px;max-height:300px;" :src="imgUrl"></el-image>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="downloadImg(beforeImgUrl)">下载</el-button>
<el-button @click="dialogVisible = false">关 闭</el-button>
</span>
</el-dialog>
2.1 main.js
#定义全局变量
Vue.prototype.$BASEAPI = process.env.BASE_API;
2.2 dev.js
BASE_API: '"http://xxxxxxx:8080"',
2.3 预览图片
// 预览图片
previewImg(fileUrl){
this.imgUrl = Vue.prototype.$BASEAPI + fileUrl;
this.dialogVisible = true;
}
2.4 下载图片
// 下载文件
downloadImg(fileUrl){
let filePath = fileUrl.split('preview/img');
filePath = Vue.prototype.$BASEAPI+'download/img'+filePath[1];
var a = document.createElement('a');
a.download = name || 'pic';
// 设置图片地址
a.href = filePath;
a.click();
},