springboot集成jasypt-spring-boot-starter对yml加密

发布于:2025-06-20 ⋅ 阅读:(21) ⋅ 点赞:(0)

传统使用

添加依赖

<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中。


网站公告

今日签到

点亮在社区的每一天
去签到