jasypt-spring-boot-starter实现加解密和数据返显
一、青铜:jasypt-spring-boot-starter在springboot中的加解密(默认加密法)
1、导包
<!--实现自动 加密解密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
注意这里引入的版本号,2.x和3.x会有差别,后面会讲
2、配置yml
2.1、关于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
2.2、关于加密部分的配置:
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.3、重点:(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.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
2、写配置类(重点)
springboot中的条件注解汇总
注解 | 作用 |
@Conditional | 判断是否满足当前指定条件 |
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean才执行 |
@ConditionalOnMissingBean | 容器中不存在指定Bean才执行 |
@ConditionalOnExpression | 满足SpEL表达式指定 |
@ConditionalOnClass | 系统中有指定的类 |
@ConditionalOnMissingClass | 系统中没有指定的类 |
@ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean |
@ConditionalOnProperty | 系统中指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径下是否存在指定资源文件 |
@ConditionalOnWebApplication | 当前是web环境 |
@ConditionalOnNotWebApplication | 当前不是web环境 |
@ConditionalOnJndi | JNDI存在指定项 |
2.1、启动加载的配置类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();
}
}
2.2、监听类,它会找到配置文件中包含指定前后缀的数据,如这里指定的 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;
}
}
2.3、实现凯撒解密并把数据返显回配置文件
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;
}
}
2.4、凯撒加解密的公共方法
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、写配置文件
3.1、配置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
可以不要,因为我们重写了解密方法,不用它本身的加解密方法。写上也不会报错,用不上。我留下它们主要是想说明版本差异造成的报错及解决办法
3.2、数据库的连接配置
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
,因为加载的是我们自定义加解密方法
三、王者:使用jasypt做一个starter
最近遇到一个业务需求,要把数据库、redis、es等等的密码都从kms的密码保管箱中获取,场景是项目启动就获取并加载到配置文件中,java代码只需通过@Value去取,实现方法是自定义一个starter,并使用jasypt,不需要它的加解密功能,只要返显数据到配置文件即可。
我的文件目录结构是:
1、定义starter的启动加载文件spring.factories
下载resource/META-INF下面:(它是starter的入口)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pingan.idaas.datadecryptbootautoconfigure.jasypt.JasyptConfiguration
2、写jasypt的文件
2.1、配置类,spring.factories加载的就是这个类
@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);
}
}
2.2、获取配置文件参数并判断出指定前后缀的是哪些
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;
}
}
2.3、这里本来是加解密的类,但是我们根据业务需求,这里不做加解密,做业务逻辑,并返显数据到配置文件参数中
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;
}
}
3、写属性类和业务类
3.1、属性类HelloProperties即是获取调用本starter的yml(或properties)中的指定前缀的数据内容
这里指定要获取前缀为
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;
}
}
3.2、获取业务类
此处仅仅是摆设未实际用到
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;
}
}
3.3 工具类
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的写法。
4、调用方的配置(调用方即谁引用我的这个starter)
4.1、引入starter
<dependency>
<groupId>com.pingan.idaas</groupId>
<artifactId>data-decrypt-boot-starter</artifactId>
</dependency>
注意:这里的要返显的字段不要写在要传入的字段中,即这里的message1……7不要写在 atguigu.hello下面.
4.2、配置yml
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的自定义获取配置文件传参、执行业务方法、获取返回数据就写完了。优点是对代码无侵入,可插拔,可自定义前后缀加解密方法,留下了远程获取的口子,缺点是只有启动时加载一次,不能动态感知变化。
四、jasypt原理分析
①META-INF/spring.factories 指定的入口类JasyptSpringCloudBootstrapConfiguration
②在这个类中通过@Import引入 EnableEncryptablePropertiesConfiguration
③在EnableEncryptablePropertiesConfiguration中加载了一个叫EnableEncryptablePropertiesBeanFactoryPostProcessor
④EnableEncryptablePropertiesBeanFactoryPostProcessor实现了一个接口BeanFactoryPostProcessor,Ordered重写了一个方法 叫postProcessBeanFactory
BeanFactoryPostProcessor:
Bean工厂后处理器,在BeanDefinitionMap填充完毕,Bean实例化之前执行BeanPostProcessor:
Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行。初始化前后分别执行postProcessBeforeInitialization
和postProcessAfterInitialization
该方法中的主要逻辑就是,获取所有的propertySources。
获取所有配置文件:MutablePropertySources propSources = environment.getPropertySources();
⑤判断配置文件中的属性是否是加密过(是否有指定前缀后缀)
⑥解密
五、springboot加载外部starter的原理(自动配置原理)
springboot加载外部starter的原理:
①springboot的启动类有一个注解叫
@SpringBootApplication
②该注解是个复合注解,其中有一个
@EnableAutoConfiguration
③@EnableAutoConfiguration 也是复合注解 其中就有@Import(AutoConfigurationImportSelector.class)
④AutoConfigurationImportSelector类中有
getCandidateConfigurations
方法⑤getCandidateConfigurations 方法的loadSpringFactories会去读配置文件
META-INF/spring.factories
文件⑥该文件的
org.springframework.boot.autoconfigure.EnableAutoConfiguration
指定的类会被加载到spring容器,这就完成了springboot加载外部bean或者第三方jar中的bean