1、导包
<dependency>
<groupId>com.github.ulisesbocchiogroupId>
<artifactId>jasypt-spring-boot-starterartifactId>
<version>3.0.3version>
dependency>
注意这里引入的版本号,2.x和3.x会有差别,后面会讲
2、配置yml
关于jasypt部分的配置:
jasypt:
encryptor:
#加解密的密码
password: atpingan
#jasypt默认更改了算法,如果不指定那么会报错:failed to bind properties under 'spring.datasource.druid.password' to java.lang.String
#解决办法:①把版本降到2.x②指定加密方法,如下
algorithm: PBEWithMD5AndDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator
关于加密部分的配置:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.200.141:3306/mysql?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
#加密密码atpingan,ENC() 是默认加解密的标识
username: ENC(kud5ZnaMJYve284geT0ITw==)
password: ENC(0CwfH4246HP22Rv74d/ZPw==)
其中ENC()是默认加密法的固定配置,后面会讲自定义加密法
重点:(2.x和3.x的区别)
jasypt默认更改了算法,如果不指定那么会报错:
failed to bind properties under spring.datasource.druid.password' to java.lang.String
#解决办法:
①把版本降到2.x
②指定加密方法,如下
jasypt:
encryptor:
algorithm: PBEWithMD5AndDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator
password: atpingan
3、计算ENC()中的内容
上面你是否疑惑ENC(kud5ZnaMJYve284geT0ITw==)中的内容是怎么来的?
他是根据:
jasypt:
encryptor:
#加解密的密码
password: atpingan
的密码来计算出来的。自己建一个main方法类,计算出来即可,
计算完这个类就没用了,可以删除。
public static void main(String[] args) {
BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
textEncryptor.setPassword("atpingan");
String userName = textEncryptor.encrypt("root");
String passWord = textEncryptor.encrypt("123456");
System.out.println("userName==="+userName);
System.out.println("passWord==="+passWord);
}
4、添加注解
在启动类上添加注解 @EnableEncryptableProperties
启动项目,发现正常,那就是对数据库的账号密码实现了加密
1、导包(只列出最主要的包,其他相关的包不一一列举)
<dependency>
<groupId>com.github.ulisesbocchiogroupId>
<artifactId>jasypt-spring-boot-starterartifactId>
<version>3.0.3version>
dependency>
2、写配置类(重点)
启动加载的配置类JasyptConfiguration ,即入口。
@Configuration
public class JasyptConfiguration {
//这里的名字必须是jasyptStringEncryptor,不能改动
@Bean(name = "jasyptStringEncryptor")
@ConditionalOnMissingBean
public StringEncryptor stringEncryptor(MyEncryptablePropertyDetector propertyDetector){
return new DefaultEncryptor(propertyDetector);
}
//这里的名字必须是encryptablePropertyDetector,不能改动
@Bean(name = "encryptablePropertyDetector")
@ConditionalOnMissingBean
public MyEncryptablePropertyDetector encryptablePropertyDetector() {
return new MyEncryptablePropertyDetector();
}
}
监听类,它会找到配置文件中包含指定前后缀的数据,如这里指定的 ikms( 和 )
public class MyEncryptablePropertyDetector implements EncryptablePropertyDetector {
private String prefix = "ikms(";
private String suffix = ")";
public MyEncryptablePropertyDetector() {
}
public MyEncryptablePropertyDetector(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;
}
/**
*判断配置文件中的数据是否是按这里指定前后缀组装的
**/
@Override
public boolean isEncrypted(String message) {
if (StringUtils.isBlank(message)) {
return false;
} else {
String trimmedValue = message.trim();
return trimmedValue.startsWith(this.prefix) && trimmedValue.endsWith(this.suffix);
}
}
@Override
public String unwrapEncryptedValue(String message) {
/**
*获取到 上面方法返回true的数据
* 此处原数据返回,不作处理,统一在DefaultEncryptor处理
*/
return message;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
实现凯撒解密并把数据返显回配置文件
public class DefaultEncryptor implements StringEncryptor {
/**
*获取写在配置文件中的参数,这里是解密的密码
**/
@Value("${jasypt.encryptor.password}")
private int decryptPassword;
private MyEncryptablePropertyDetector propertyDetector;
public DefaultEncryptor() {
}
public DefaultEncryptor( MyEncryptablePropertyDetector propertyDetector) {
this.propertyDetector = propertyDetector;
}
/**
*这里是加密方法,我们不在这里加密,原参数返回
**/
@Override
public String encrypt(String encryptMessage) {
return encryptMessage ;
}
/**
*凯撒解密
**/
@Override
public String decrypt(String decryptMessage) {
/**
* 从MyEncryptablePropertyDetector的 unwrapEncryptedValue方法返回的数据在这里处理
*/
String prefix = propertyDetector.getPrefix();
String suffix = propertyDetector.getSuffix();
int prefixIndex = decryptMessage.indexOf(prefix);
int suffixIndex = decryptMessage.indexOf(suffix);
/**
* 截取括号中间部分,例如:ikms(邻居小玲) 里面的:邻居小玲
*/
decryptMessage = decryptMessage.substring(prefixIndex+prefix.length(),suffixIndex);
/**
* 做凯撒解密:加解密公共方法,请往后看
*/
String result = KaiserUtil.decryptKaiser(decryptMessage,decryptPassword);
return result;
}
}
凯撒加解密的公共方法
public class KaiserUtil {
public static void main(String[] args) {
String encryptKaiser = encryptKaiser("root",123456789);
String decryptKaiser = decryptKaiser(encryptKaiser, 123456789);
System.out.println("encryptKaiser==="+encryptKaiser);
System.out.println("decryptKaiser==="+decryptKaiser);
}
/**
* 使用凯撒加密方式加密数据
* @param orignal :原文
* @param key :密钥
* @return :加密后的数据
*/
public static String encryptKaiser(String orignal, int key) {
// 将字符串转为字符数组
char[] chars = orignal.toCharArray();
StringBuilder sb = new StringBuilder();
// 遍历数组
for (char aChar : chars) {
// 获取字符的ASCII编码
int asciiCode = aChar;
// 偏移数据
asciiCode += key;
// 将偏移后的数据转为字符
char result = (char) asciiCode;
// 拼接数据
sb.append(result);
}
return sb.toString();
}
/**
* 使用凯撒加密方式解密数据
* @param encryptedData :密文
* @param key :密钥
* @return : 源数据
*/
public static String decryptKaiser(String encryptedData, int key) {
// 将字符串转为字符数组
char[] chars = encryptedData.toCharArray();
StringBuilder sb = new StringBuilder();
// 遍历数组
for (char aChar : chars) {
// 获取字符的ASCII编码
int asciiCode = aChar;
// 偏移数据
asciiCode -= key;
// 将偏移后的数据转为字符
char result = (char) asciiCode;
// 拼接数据
sb.append(result);
}
return sb.toString();
}
}
3、写配置文件
配置jasypt密钥及指定加解密方法
jasypt:
encryptor:
#解密的密钥
password: 123456789
#jasypt 3.x版本默认更改了算法,如果不指定那么会报错:failed to bind properties under 'spring.datasource.druid.password' to java.lang.String
#解决办法:①把版本降到2.x ②指定加密方法,如下
algorithm: PBEWithMD5AndDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator
注意:这里的
algorithm
和iv-generator-classname
可以不要,因为我们重写了解密方法,不用它本身的加解密方法。写上也不会报错,用不上。我留下它们主要是想说明版本差异造成的报错及解决办法
数据库的连接配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.200.141:3306/mysql?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
#加密密码atpingan
#username: root
#password: 420188
username: ikms(춇춄춄춉)
password: ikms(쵉쵇쵅쵆쵍쵍)
至此就写完了自定义jasypt的加解密,这种方法也
不需要加@EnableEncryptableProperties
,因为加载的是我们自定义加解密方法
最近遇到一个业务需求,要把数据库、redis、es等等的密码都从kms的密码保管箱中获取,场景是项目启动就获取并加载到配置文件中,java代码只需通过@Value去取,实现方法是自定义一个starter,并使用jasypt,不需要它的加解密功能,只要返显数据到配置文件即可。
spring.factories
下载resource/META-INF下面:(它是starter的入口)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pingan.idaas.datadecryptbootautoconfigure.jasypt.JasyptConfiguration
@Configuration
@EnableConfigurationProperties(DataDeCryptProperties.class)
public class JasyptConfiguration {
@Bean
@ConditionalOnMissingBean(DataDecryptService.class)
public DataDecryptService dataDecryptService(DataDeCryptProperties dataDeCryptProperties){
return new DataDecryptService(dataDeCryptProperties);
}
@Bean(name="encryptablePropertyDetector")
@ConditionalOnMissingBean
public MyEncryptablePropertyDetector encryptablePropertyDetector(){
return new MyEncryptablePropertyDetector();
}
// 注意这里,因为它有多个实现类,其他实现类实例化后有可能导致,此处不实例化,所以这里要用@Primary标注
@Bean(name = "jasyptStringEncryptor")
@Primary
public StringEncryptor stringEncryptor(DataDecryptService dataDecryptService,MyEncryptablePropertyDetector myEncryptablePropertyDetector){
return new DefaultEncryptor(dataDecryptService,myEncryptablePropertyDetector);
}
}
public class MyEncryptablePropertyDetector implements EncryptablePropertyDetector {
private String prefix = "AIDSREN(";
private String suffix = ")";
public MyEncryptablePropertyDetector() {
}
public MyEncryptablePropertyDetector(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
@Override
public boolean isEncrypted(String message) {
if(StringUtils.isBlank(message)){
return false;
}
String trimValue = message.trim();
return trimValue.startsWith(this.prefix) && trimValue.endsWith(this.suffix);
}
@Override
public String unwrapEncryptedValue(String message) {
return message;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
public class DefaultEncryptor implements StringEncryptor {
private static final Log log = LogFactory.getLog(DefaultEncryptor.class);
private DataDecryptService dataDecryptService;
private MyEncryptablePropertyDetector propertyDetector;
@Autowired
private DataDeCryptProperties dataDeCryptProperties;
private String innerPrefix = "AEN(";
private String innerSuffix = ")";
public static final String COLON = "::";
public DefaultEncryptor() {
}
public DefaultEncryptor(DataDecryptService dataDecryptService, MyEncryptablePropertyDetector propertyDetector) {
this.dataDecryptService = dataDecryptService;
this.propertyDetector = propertyDetector;
}
public DefaultEncryptor(MyEncryptablePropertyDetector propertyDetector) {
this.propertyDetector = propertyDetector;
}
@Override
public String encrypt(String message) {
return message;
}
/**
* 解密
* @param message
* @return
*/
@Override
public String decrypt(String message) {
String prefix = propertyDetector.getPrefix();
String suffix = propertyDetector.getSuffix();
message = CommonTool.getMessage(message, prefix, suffix);
String result = "";
if (StringUtils.isNotBlank(message)) {
result = getDecryptOrRemoteKey(message);
}
return result;
}
/**
* 获取含有 :: 的个数并分支
* @param message
* @return
*/
private String getDecryptOrRemoteKey(String message) {
int countOfColon = countColon(message);
if (countOfColon <= 0) {
return getResultWithNoColon(message);
}
if (countOfColon == 1) {
return getResultWithColon(message);
}
return "";
}
/**
* 未含有 :: 的处理逻辑
* @param message
* @return
*/
private String getResultWithNoColon(String message) {
String trimValue = message.trim();
boolean flag = trimValue.startsWith(innerPrefix) && trimValue.endsWith(innerSuffix);
if(flag){
int decryptPassword = dataDeCryptProperties.getDecryptPassword();
message = CommonTool.getMessage(message, innerPrefix, innerSuffix);
return IdaasKaiserUtil.decryptKaiser(message,decryptPassword);
}
return message;
}
/**
* 含有一个 :: 的处理逻辑
* @param message
* @return
*/
private String getResultWithColon(String message) {
String result = null;
String[] messageArray = message.split(COLON);
String key = messageArray[0];
String value = messageArray[1];
String url = dataDeCryptProperties.getUrl();
result = getByRemoteOrKms(key, url);
if (StringUtils.isBlank(result)) {
result = getResultWithNoColon(value);
}
return result;
}
/**
* 远程获取密钥
* @param key
* @param url
* @return
*/
private String getByRemoteOrKms(String key, String url) {
CustomGetCryptorService cryptorService = getCryptorInstance();
try {
return cryptorService.getRemoteCryptor(url, key);
} catch (Exception e) {
log.error("获取远程密钥失败", e);
}
return null;
}
/**
* 获取对象(starter本身密钥获取对象 还是 客户端自定义密钥获取对象)
* @return
*/
private CustomGetCryptorService getCryptorInstance() {
String customGetCryptorClass = dataDeCryptProperties.getCustomGetCryptorClass();
CustomGetCryptorService customGetCryptorService = null;
try {
if (StringUtils.isNotBlank(customGetCryptorClass)) {
Class<?> clazz = Class.forName(customGetCryptorClass);
customGetCryptorService = (CustomGetCryptorService) clazz.newInstance();
} else {
customGetCryptorService = new CustomGetCryptorServiceImpl();
}
} catch (Exception e) {
log.error("获取客户端对象失败!");
}
return customGetCryptorService;
}
/**
* 计算::的个数
* @param message
* @return
*/
private int countColon(String message) {
String trimValue = message.trim();
int count = 0;
int length = message.length();
while (trimValue.indexOf(COLON) != -1) {
trimValue = trimValue.substring(trimValue.indexOf(COLON) + 1, length);
count++;
}
return count;
}
}
这里指定要获取前缀为
atguigu.hello
开头的参数
@ConfigurationProperties(prefix = "idaas.decerypt")
public class DataDeCryptProperties {
private String url;
private int decryptPassword;
private String customGetCryptorClass;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getDecryptPassword() {
return decryptPassword;
}
public void setDecryptPassword(int decryptPassword) {
this.decryptPassword = decryptPassword;
}
public String getCustomGetCryptorClass() {
return customGetCryptorClass;
}
public void setCustomGetCryptorClass(String customGetCryptorClass) {
this.customGetCryptorClass = customGetCryptorClass;
}
}
此处仅仅是摆设未实际用到
public class DataDecryptService {
private DataDeCryptProperties dataDeCryptProperties;
public DataDecryptService() {
}
public DataDecryptService(DataDeCryptProperties dataDeCryptProperties) {
this.dataDeCryptProperties = dataDeCryptProperties;
}
}
远程获取密钥口子:
接口:
public interface CustomGetCryptorService {
String getRemoteCryptor(String url, String key);
}
实现类:
public class CustomGetCryptorServiceImpl implements CustomGetCryptorService {
private static final Log log = LogFactory.getLog(CustomGetCryptorServiceImpl.class);
@Override
public String getRemoteCryptor(String url, String key) {
// todo 自定义远程获取密钥
log.info("starter获取远程密钥失败");
return null;
}
}
public class CommonTool {
public static String getMessage(String message,String prefix,String suffix){
int prefixIndex = message.indexOf(prefix);
int suffixIndex = message.lastIndexOf(suffix);
return message.substring(prefixIndex+prefix.length(),suffixIndex);
}
}
public class IdaasKaiserUtil {
/**
* 使用凯撒加密方式加密数据
* @param orignal :原文
* @param key :密钥
* @return :加密后的数据
*/
public static String encryptKaiser(String orignal, int key) {
// 将字符串转为字符数组
char[] chars = orignal.toCharArray();
StringBuilder sb = new StringBuilder();
// 遍历数组
for (char aChar : chars) {
// 获取字符的ASCII编码
int asciiCode = aChar;
// 偏移数据
asciiCode += key;
// 将偏移后的数据转为字符
char result = (char) asciiCode;
// 拼接数据
sb.append(result);
}
return sb.toString();
}
/**
* 使用凯撒加密方式解密数据
* @param encryptedData :密文
* @param key :密钥
* @return : 源数据
*/
public static String decryptKaiser(String encryptedData, int key) {
// 将字符串转为字符数组
char[] chars = encryptedData.toCharArray();
StringBuilder sb = new StringBuilder();
// 遍历数组
for (char aChar : chars) {
// 获取字符的ASCII编码
int asciiCode = aChar;
// 偏移数据
asciiCode -= key;
// 将偏移后的数据转为字符
char result = (char) asciiCode;
// 拼接数据
sb.append(result);
}
return sb.toString();
}
}
以上是自定义starter的自动配置类的写法,这里列出了主要内容,忽略了引包,和场景启动器的写法,如需细化请参看前一博客关于自定义starter的写法。
>
>com.pingan.idaas >
>data-decrypt-boot-starter >
>
注意:这里的要返显的字段不要写在要传入的字段中,即这里的message1……7不要写在 atguigu.hello下面.
idaas:
decerypt:
decryptPassword: 12345678 # 最低配置,必须
url: wwww.baidu.com #starter去远程获取密钥的地址(根据实际配置,若没有去远程获取可不配),非必须
customGetCryptorClass: com.athaite.idaas.CustomGetCryptor #客户端自己代码去远程获取密钥的类(此类要实现我们starter的接口CustomGetCryptorService),非必须
剩下的就是自己加密的部分了:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: IDSREN(IEN(憸憲憰憱憈憻懇懁憿憺憈慽慽慿憇憀慼慿憄憆慼慿憆憁慼慿憀憇憈憁憁慾憄慽憵懃憺憷憻憯憺憺憭憯憲憻憷憼憍懃懁憳憡憡憚憋憴憯憺懁憳慴憯憺憺憽懅憛懃憺懂憷憟懃憳懀憷憳懁憋懂懀懃憳慴懃懁憳憣憼憷憱憽憲憳憋懂懀懃憳慴憱憶憯懀憯憱懂憳懀憓憼憱憽憲憷憼憵憋憣憢憔慻憆慴憯懃懂憽憠憳憱憽憼憼憳憱懂憋懂懀懃憳))
#url: jdbc:mysql://192.168.183.129:3306/gulimall_admin?useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
#username: root
username: AIDSREN(AEN(懀憽憽懂))
#password: 420188
password: AIDSREN(AEN(憂憀慾慿憆憆))
特别提醒:
本starter兼容本地解密,starter远程解密,客户端自己远程解密
示例(4种类型):
本地解密 :AIDSREN(AEN(懀憽憽懂))
本地不解密 :AIDSREN(root)
远程解密(双冒号后是远程获取失败采用本地密文密钥):AIDSREN(name::AEN(懀憽憽懂))
远程解密(双冒号后是远程获取失败采用本地明文密钥):AIDSREN(name::root)
其他配置项与双冒号(::)错开,避免误判自此,starter的自定义获取配置文件传参、执行业务方法、获取返回数据就写完了。优点是对代码无侵入,可插拔,可自定义前后缀加解密方法,留下了远程获取的口子,缺点是只有启动时加载一次,不能动态感知变化。