在实际的业务场景,有时需要导出sql脚本文件,再导入执行sql脚本文件去同步数据,这里大多数数据量比较大,涉及的表和服务比较多,还可能不止一个数据库的场景。导入sql脚本文件直接执行而不去校验文件的完整性,会存在严重的安全隐患,必须要保证导入和导出的是同一个文件。
sql文件如何去做完整性校验呢?
导出sql文件时
- 将导出的数据写入到本地生成sql脚本文件中;
- 对生成sql脚本文件做MD5;
- 对得到的MD5值,使用AES算法加密;
- 使用AES加密后的值去重命名该sql文件;
- 导出该文件。
关键代码如下:
try {
String path = System.getProperty("user.dir") + filePath + "export" + ".sql";
export(sqlList, path);
String md5Value = FileEncryptUtil.getFileMd5(path);
log.info("md5Value: {}", md5Value);
String aesValue = FileEncryptUtil.aesEncrypt(md5Value);
String newPath = System.getProperty("user.dir") + filePath + aesValue + ".sql";
File oldFile = new File(path);
File newFile = new File(newPath);
oldFile.renameTo(newFile);
log.info("path: {}", newPath);
return newPath;
} catch (IOException e) {
log.error("导出sql文件异常,appId = {}", appId);
}
package com.angel.ocean.utils;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@Slf4j
public class FileEncryptUtil {
final static String key = "Y1yIwqT3bwEYoT1q";
final static String iv = "gmbgzchNN9YFQAc0";
/**
* 加密
*
* @param content 需要加密的内容
* @return
*/
public static String aesEncrypt(String content) {
AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, key.getBytes(), iv.getBytes());
return aes.encryptBase64(content);
}
/**解密
* @param content 待解密内容
* @return
*/
public static String aesDecrypt(String content) {
AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, key.getBytes(), iv.getBytes());
return aes.decryptStr(content);
}
/**
* 求文件的MD5值
* @param path
* @return
*/
public static String getFileMd5(String path) {
File file = new File(path);
try {
MessageDigest md5Digest = MessageDigest.getInstance("MD5");
return getFileChecksum(md5Digest, file);
} catch (NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException, {}", e.getMessage(), e);
} catch (IOException e) {
log.error("IOException, {}", e.getMessage(), e);
}
return null;
}
public static String getFileChecksum(MessageDigest digest, File file) throws IOException {
FileInputStream fis = new FileInputStream(file);
byte[] byteArray = new byte[1024];
int bytesCount;
while ((bytesCount = fis.read(byteArray)) != -1) {
digest.update(byteArray, 0, bytesCount);
};
fis.close();
byte[] bytes = digest.digest();
StringBuilder sb = new StringBuilder();
for(int i=0; i< bytes.length; i++) {
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
}
导入sql文件时
- 读取sql文件流,写入到本地;
- 获取文件名称,剔除.sql,就拿到AES加密后的值;
- 对该值进行AES解密,得到MD5值,假设为oldMd5;
- 对该文件进行MD5,即可得到该文件的MD5值,假设为newMd5;
- 校验oldMd5与newMD5的值是否相同,不一致则抛出文件完整性校验失败,导入sql失败;
- MD5值一致,则执行sql脚本文件。
关键代码如下:
@Override
public void importSqlFile(MultipartFile file) {
String fileName = file.getOriginalFilename();
if(!fileName.contains(".sql")) {
throw new BusinessException("sql文件导入完整性校验失败");
}
String newFilePath = "";
try {
byte[] bytes = file.getBytes();
newFilePath = System.getProperty("user.dir") + filePath + fileName;
log.info("filePath:{}, fileName:{}", newFilePath, fileName);
Path path = Paths.get(newFilePath);
Files.write(path, bytes);
String aesValue = fileName.replace(".sql", "");
String md5Value = FileEncryptUtil.aesDecrypt(aesValue);
log.info("md5Value:{}", md5Value);
String newMd5 = FileEncryptUtil.getFileMd5(newFilePath);
log.info("newMd5:{}", newMd5);
if(md5Value.equals(newMd5)) {
// 执行sql脚本
executeSqlScript(newFilePath);
log.info("sql文件导入成功");
} else {
log.info("sql文件导入完整性校验失败");
throw new BusinessException("sql文件导入完整性校验失败");
}
} catch (IOException e) {
log.error("文件上传失败, IOException:{}", e.getMessage());
} finally {
// deleteSqlFile(newFilePath);
}
}
/**
* 执行sql脚本
* @throws SQLException
*/
private void executeSqlScript(String path) throws IOException {
ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
// resourceDatabasePopulator.addScript(new ClassPathResource(path));
resourceDatabasePopulator.addScript(new FileSystemResource(path));
resourceDatabasePopulator.execute(dataSource);
}