传统使用
添加依赖
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
配置配置文件
yml可以添加加密相关信息:
jasypt:
encryptor:
algorithm: PBEWithMD5AndDES
# 这个是密码
password: mima
然后加密(比如数据库):
spring:
datasource:
druid:
master:
password: ENC(sfjhdskjfhkshdkjs)
加密工具类:
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.EnvironmentPBEConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
public class JasyptTest {
@Test
public void testPwdEncrypt() {
// 实例化加密器
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
// 配置加密算法和密钥
EnvironmentPBEConfig config = new EnvironmentPBEConfig();
config.setAlgorithm("PBEWithMD5AndDES");
config.setPassword("mima");
encryptor.setConfig(config);
// 加密字符串
String myPwd = "mymima";
String encryptedPwd = encryptor.encrypt(myPwd);
System.out.println("+++++++++++++++++++++++");
System.out.println("明文密码:" + myPwd);
System.out.println("加密后的密码:" + encryptedPwd);
System.out.println("+++++++++++++++++++++++");
}
@Test
public void testPwdDecrypt() {
// 实例化加密器
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
// 配置加密算法和密钥
EnvironmentPBEConfig config = new EnvironmentPBEConfig();
config.setAlgorithm("PBEWithMD5AndDES");
config.setPassword("mima");
encryptor.setConfig(config);
// 解密字符串
String encryptedPwd = "";
String decryptedPwd = encryptor.decrypt(encryptedPwd);
System.out.println("+++++++++++++++++++++++");
System.out.println("解密后的密码:" + decryptedPwd);
System.out.println("+++++++++++++++++++++++");
}
}
自定义形式
传统方式简单,但是对于 war
包支持不太好。
加密工具
AesEncryptUtils.class
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
public class AesEncryptUtils {
private static final String ENCODING = "utf-8";
/**
* 加密操作-JASYPT
*
* @param content
* @return
* @throws Exception
*/
public static String encryptByJasypt(String content) throws Exception {
byte[] encrypted = aesEncrypt(content.getBytes(EncryptConstant.DEFAULT_ENCODING),
EncryptConstant.JASYPT_KEY.getBytes(), EncryptConstant.AES_IV_JASYPT.getBytes());
return EncryptConstant.JASYPT_HEADER + encryptBASE64(encrypted) + EncryptConstant.JASYPT_END;
}
/**
* 解密操作-JASYPT
*
* @param content
* @return
* @throws Exception
*/
public static String decryptByJasypt(String content) throws Exception {
if (content.startsWith(EncryptConstant.JASYPT_HEADER) && content.endsWith(EncryptConstant.JASYPT_END)) {
content = content.substring(EncryptConstant.JASYPT_HEADER.length(), (content.length() - EncryptConstant.JASYPT_END.length()));
}
return decryptByJasyptBean(content);
}
/**
* 解密操作-JASYPT-Bean
*
* @param content
* @return
* @throws Exception
*/
public static String decryptByJasyptBean(String content) throws Exception {
byte[] bytes = aesDecrypt(decryptBASE64(content),
EncryptConstant.JASYPT_KEY.getBytes(), EncryptConstant.AES_IV_JASYPT.getBytes());
return byteToString(bytes);
}
/**
* 加密操作
*/
private static byte[] aesEncrypt(byte[] content, byte[] keyBytes, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(content);
return result;
} catch (Exception e) {
System.out.println("exception:" + e.toString());
}
return null;
}
private static byte[] aesDecrypt(byte[] content, byte[] keyBytes, byte[] iv) {
try {
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, key, new IvParameterSpec(iv));
byte[] result = cipher.doFinal(content);
return result;
} catch (Exception var6) {
System.out.println("exception:" + var6.toString());
return null;
}
}
public static String byteToString(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
public static byte[] decryptBASE64(String key) throws Exception {
return Base64.decodeBase64(key.getBytes());
}
/**
* BASE64加密 待验证
*/
public static String encryptBASE64(byte[] bytes) throws UnsupportedEncodingException {
return new String(Base64.encodeBase64(bytes), "UTF-8");
}
}
CustomEncryptableProperty.class
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyDetector;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* 自定义属性探测器和密码解析器注入
*/
@Component
public class CustomEncryptableProperty {
@Bean(name = "encryptablePropertyDetector")
public EncryptablePropertyDetector encryptablePropertyDetector() {
return new CustomEncryptablePropertyDetector();
}
@Bean("encryptablePropertyResolver")
public EncryptablePropertyResolver encryptablePropertyResolver(EncryptablePropertyDetector encryptablePropertyDetector) {
return new CustomEncryptablePropertyResolver(encryptablePropertyDetector);
}
}
CustomEncryptablePropertyDetector.class
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyDetector;
import org.springframework.util.Assert;
/**
* 自定义属性探测器
*/
public class CustomEncryptablePropertyDetector implements EncryptablePropertyDetector {
/**
* 探测字符串-前缀和后缀
*/
private String prefix = EncryptConstant.JASYPT_HEADER;
private String suffix = EncryptConstant.JASYPT_END;
public CustomEncryptablePropertyDetector() {
}
public CustomEncryptablePropertyDetector(String prefix, String suffix) {
Assert.notNull(prefix, "Prefix can't be null");
Assert.notNull(suffix, "Suffix can't be null");
this.prefix = prefix;
this.suffix = suffix;
}
/**
* 是否为可以解密的字符串【自定义规则为 prefix 开头、suffix 结尾】
*
* @param value 全部的字符串
* @return 是否是解密的字符串,true,是,false,否
*/
@Override
public boolean isEncrypted(String value) {
if (value == null) {
return false;
}
final String trimmedValue = value.trim();
return (trimmedValue.startsWith(prefix) &&
trimmedValue.endsWith(suffix));
}
/**
* 截取到除了标识之后的值【截取 prefix、suffix 之间的字符串】
*
* @param value 全部字符串
* @return string 去掉标识符的字符串
*/
@Override
public String unwrapEncryptedValue(String value) {
return value.substring(
prefix.length(),
(value.length() - suffix.length()));
}
}
CustomEncryptablePropertyResolver.class
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyDetector;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.exception.DecryptionException;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import java.util.Optional;
/**
* 自定义密码解析器
*/
public class CustomEncryptablePropertyResolver implements EncryptablePropertyResolver {
/**
* 属性探测器
*/
private final EncryptablePropertyDetector detector;
public CustomEncryptablePropertyResolver(EncryptablePropertyDetector detector) {
this.detector = detector;
}
/**
* 处理真正的解密逻辑
*
* @param value 原始值
* @return 如果值未加密,返回原值,如果加密,返回解密之后的值
*/
@Override
public String resolvePropertyValue(String value) {
return Optional.ofNullable(value)
.filter(detector::isEncrypted)
.map(resolvedValue -> {
try {
// 1.过滤加密规则后的字符串
String unwrappedProperty = detector.unwrapEncryptedValue(resolvedValue.trim());
// 2.解密
return AesEncryptUtils.decryptByJasyptBean(unwrappedProperty);
} catch (EncryptionOperationNotPossibleException e) {
throw new DecryptionException("Unable to decrypt: " + value + ". Decryption of Properties failed, make sure encryption/decryption " +
"passwords match", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.orElse(value);
}
}
EncryptConstant.class
public interface EncryptConstant {
String DEFAULT_ENCODING = "UTF-8";
/**
* jasypt-key
*/
String JASYPT_KEY = "2024";
/**
* 偏移,jasypt加密钥匙
*/
String AES_IV_JASYPT = "2024i";
/**
* jasypt-头
*/
String JASYPT_HEADER = "ENC(";
/**
* jasypt-尾
*/
String JASYPT_END = ")";
}
JasyptTest.class
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.util.StrUtil;
import com.test.jasypt.AesEncryptUtils;
import com.test.jasypt.EncryptConstant;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Function;
/**
* Jasypt加密yml
*/
public class JasyptTest {
/**
* 自定义yml路径,没有的话自动获取,支持根目录相对路径
*/
private static final String customYmlPath = "";
/**
* 是否解密操作
*/
private static final boolean isDecrypt = false;
/**
* 要加密的路径
*/
private static final String[] propertyPaths = new String[]{
"spring.datasource.druid.master.username",
"spring.datasource.druid.master.password"
};
/**
* 加密后的文件路径,为空时为原文件
*/
private static final String encryptToPath = "";
/**
* 解密后的文件路径,为空时为原文件
*/
private static final String decryptToPath = "";
public static void main(String[] args) {
System.out.println("========= " + (isDecrypt ? "解密操作" : "加密操作") + " =========");
String ymlPath = getYmlPath();
System.out.println("yml文件路径: " + ymlPath);
// 目标yml路径
String targetYmlPath = isDecrypt ? StrUtil.blankToDefault(checkAbsolute(decryptToPath), ymlPath) : StrUtil.blankToDefault(checkAbsolute(encryptToPath), ymlPath);
System.out.println("目标yml路径: " + targetYmlPath);
// 备份
// backupYmlFile(ymlPath);
// 加密
updateYaml(ymlPath, targetYmlPath, propertyPaths, propertyInfo -> {
// 内容
String propValue = propertyInfo.value;
// 是否已加密
boolean isEncryptValue = propValue.startsWith(EncryptConstant.JASYPT_HEADER)
&& propValue.endsWith(EncryptConstant.JASYPT_END);
if (StrUtil.isBlank(propValue)) {
System.out.println(propertyInfo.key + "内容为空,跳过");
return null;
}
String val = null;
if (isDecrypt) {
if (!isEncryptValue) {
System.out.println(propertyInfo.key + "未加密,跳过");
return propValue;
}
// 解密
val = decrypt(propValue);
} else {
if (isEncryptValue) {
System.out.println(propertyInfo.key + "已加密,跳过");
return propValue;
}
// 加密
val = encrypt(propValue);
}
System.out.println(propertyInfo.key + ":" + propValue + " ==> " + val);
return val;
});
}
/**
* 加密
*
* @param pwd 要加密的内容
* @return 加密后的内容
*/
public static String encrypt(String pwd) {
String encryptedPwd = null;
try {
encryptedPwd = AesEncryptUtils.encryptByJasypt(pwd);
} catch (Exception e) {
throw new RuntimeException(e);
}
return encryptedPwd;
}
/**
* 解密
*
* @param encryptedPwd 加密的内容
* @return 解密后的内容
*/
public static String decrypt(String encryptedPwd) {
String decryptedPwd = null;
try {
decryptedPwd = AesEncryptUtils.decryptByJasypt(encryptedPwd);
} catch (Exception e) {
throw new RuntimeException(e);
}
return decryptedPwd;
}
/**
* 备份yml文件
*/
public static void backupYmlFile(String ymlPath) {
File ymlFile = new File(ymlPath);
if (!ymlFile.exists() || !ymlFile.isFile()) {
System.out.println("文件不存在:" + ymlPath);
return;
}
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
// 修改备份路径为当前类所在路径
String classPath = JasyptTest.class.getProtectionDomain().getCodeSource().getLocation().getPath();
File classDir = new File(classPath).getParentFile();
String backupDir = String.join(File.separator, classDir.getPath(), "src", "test", "java", "backup");
System.out.println(backupDir);
backupDir = backupDir.replace("target\\", "");
// 确保备份目录存在
File backupDirFile = new File(backupDir);
if (!backupDirFile.exists()) {
backupDirFile.mkdirs();
}
String fileName = new File(ymlPath).getName();
String backupPath = backupDir + File.separator + fileName + "." + timeStamp + ".backup";
FileUtil.copy(ymlFile, new File(backupPath), true);
System.out.println("已备份文件:" + backupPath);
}
/**
* 检测是否相对路径转为绝对路径
*
* @param path 路径
* @return 绝对路径
*/
private static String checkAbsolute(String path) {
if (StrUtil.isBlank(path)) return path;
// 如果是相对路径,则基于项目根目录解析
if (!new File(path).isAbsolute()) {
// 获取项目根目录
String projectRoot = getProjectRootPath();
return projectRoot + File.separator + path.replace("/", File.separator);
}
return path;
}
/**
* 获取yml配置路径
*
* @return yml路径
*/
private static String getYmlPath() {
if (StrUtil.isNotBlank(customYmlPath)) {
return checkAbsolute(customYmlPath);
}
// 获取resource下的application.yml文件路径
ClassLoader classLoader = JasyptTest.class.getClassLoader();
String compiledPath = Objects.requireNonNull(classLoader.getResource("application.yml")).getPath();
// 将编译后的路径转换为源代码路径
// 一般从 target/classes 改为 src/main/resources
String ymlPath = compiledPath.replace("target/classes", "src/main/resources");
// 处理Windows系统下的路径格式
if (ymlPath.startsWith("/")) {
ymlPath = ymlPath.substring(1);
}
ymlPath = ymlPath.replace("%20", " ");
return ymlPath;
}
/**
* 获取项目根目录路径
*
* @return 项目根目录路径
*/
private static String getProjectRootPath() {
// 如果没找到,返回当前工作目录
return System.getProperty("user.dir");
}
/**
* 更新yml指定属性
*
* @param filePath yml路径
* @param targetFilePath 目标yml路径
* @param propertyPaths 属性路径数组
* @param propertyInfoFunction 属性值处理
*/
public static void updateYaml(String filePath, String targetFilePath, String[] propertyPaths, Function<PropertyInfo, String> propertyInfoFunction) {
List<String> lines = FileReader.create(new File(filePath)).readLines();
Deque<String> currentPath = new ArrayDeque<>();
int currentIndent = 0;
boolean isModified = false;
Map<String, String> updates = new HashMap<>();
for (String propertyPath : propertyPaths) {
updates.put(propertyPath, "1");
}
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
LineInfo info = parseLine(line);
if (info.isKeyLine) {
adjustCurrentPath(currentPath, currentIndent, info.indent, info.key);
currentIndent = info.indent;
// 将当前路径转换为点分字符串
String currentKeyPath = String.join(".", currentPath);
if (updates.containsKey(currentKeyPath)) {
// 替换值并记录修改
int colonIndex = line.indexOf(':');
if (colonIndex != -1) {
String[] split = line.split(":", 2);
PropertyInfo propertyInfo = new PropertyInfo();
propertyInfo.key = currentKeyPath;
// 处理value后有注释的情况
String valueWithComment = split[1].trim();
String value = valueWithComment;
String comment = "";
// 检查是否有注释(非字符串内的#)
int commentIndex = -1;
boolean inQuotes = false;
char[] chars = valueWithComment.toCharArray();
for (int j = 0; j < chars.length; j++) {
if (chars[j] == '"' || chars[j] == '\'') {
inQuotes = !inQuotes;
} else if (chars[j] == '#' && !inQuotes) {
commentIndex = j;
break;
}
}
// 分离值和注释
if (commentIndex != -1) {
value = valueWithComment.substring(0, commentIndex).trim();
comment = valueWithComment.substring(commentIndex);
}
propertyInfo.comment = comment;
propertyInfo.value = value;
String newValue = propertyInfoFunction.apply(propertyInfo);
if (Objects.isNull(newValue)) {
newValue = "";
} else {
newValue = " " + newValue;
}
// 保留注释
if (commentIndex != -1) {
lines.set(i, split[0] + ":" + newValue + " " + comment);
} else {
lines.set(i, split[0] + ":" + newValue);
}
}
updates.remove(currentKeyPath);
isModified = true;
// 所有需要更新的属性都已处理完成
if (updates.isEmpty()) break;
}
}
}
if (!updates.isEmpty()) {
System.out.println("以下路径不存在:" + CollUtil.join(updates.keySet(), ", "));
}
if (isModified) {
FileWriter.create(new File(targetFilePath)).writeLines(lines);
}
}
/**
* 处理行内容
*
* @param line 行内容
* @return 行信息
*/
private static LineInfo parseLine(String line) {
LineInfo info = new LineInfo();
String trimmed = line.trim();
// 跳过注释和空行
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
return info;
}
// 计算缩进
info.indent = 0;
while (info.indent < line.length() && line.charAt(info.indent) == ' ') {
info.indent++;
}
// 提取键名
if (trimmed.contains(":")) {
int colonIndex = trimmed.indexOf(':');
info.key = trimmed.substring(0, colonIndex).trim();
info.isKeyLine = true;
}
return info;
}
/**
* 计算路径变化
*
* @param currentPath 当前路径
* @param currentIndent 当前缩进
* @param newIndent 新缩进
* @param key key
*/
private static void adjustCurrentPath(Deque<String> currentPath,
int currentIndent,
int newIndent,
String key) {
if (newIndent == 0) {
currentPath.clear();
}
if (newIndent < currentIndent) {
int levelDiff = (currentIndent - newIndent) / 2 + 1;
for (int i = 0; i < levelDiff && !currentPath.isEmpty(); i++) {
currentPath.pollLast();
}
} else if (newIndent == currentIndent && !currentPath.isEmpty()) {
currentPath.pollLast();
}
if (key != null) {
currentPath.addLast(key);
}
}
private static class LineInfo {
int indent;
String key;
boolean isKeyLine;
}
private static class PropertyInfo {
// 键
private String key;
// 值
private String value;
// 注释
private String comment;
}
}
直接执行JasyptTest即可,可自定义yml位置,优点是能够一键加解密,注意:尽量不要讲JasyptTest.class一块打包,建议放到src/test/java中。