本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。
一些比较重要的配置信息,比如密码之类的敏感配置,我们希望将配置加密存储,保证安全性。Apollo框架本身没有提供数据加密的功能,如果想要实现数据加密的功能有两种方式,第一种是改Apollo的源码,增加加解密的逻辑,第二种比较简单,基于第三方的框架来对数据进行解密。
jasypt-spring-boot是一个基于Spring Boot开发的框架,可以将properties中加密的内容自动解密,在Apollo中也可以借助于jasypt-spring-boot这个框架来实现数据的加解密操作。
jasypt-spring-boot GitHub地址:https://github.com/ulisesbocchio/jasypt-spring-boot
将我们需要加密的配置通过jasypt-spring-boot提供的方法进行加密,然后将加密的内容配置在Apollo中,当项目启动的时候,jasypt-spring-boot会将Apollo加密的配置进行解密,从而让使用者获取到解密之后的内容。
创建一个新的Maven项目,加入Apollo和jasypt的依赖:
com.ctrip.framework.apollo
apollo-client
1.1.0
com.github.ulisesbocchio
jasypt-spring-boot-starter
1.16
加入下面的依赖信息:
server
.
port
=
8081
app
.
id
=
SampleApp
apollo
.
meta
=
http
:
//localhost:8080
apollo
.
bootstrap
.
enabled
=
true
apollo
.
bootstrap
.
namespaces
=
application
jasypt
.
encryptor
.
password
=
yinjihaunkey
jasypt.encryptor.password:配置加密的Key
创建一个加密的工具类,用于加密配置:
public
class
EncryptUtil
{
/**
* 制表符、空格、换行符 PATTERN
*/
private
static
Pattern
BLANK_PATTERN
=
Pattern
.
compile
(
"\\s*|\t|\r|\n"
);
/**
* 加密Key
*/
private
static
String
PASSWORD
=
"yinjihaunkey"
;
/**
* 加密算法
*/
private
static
String
ALGORITHM
=
"PBEWithMD5AndDES"
;
public
static
Map
<
String
,
String
>
getEncryptedParams
(
String
input
)
{
//输出流
ByteArrayOutputStream
byteArrayOutputStream
=
new
ByteArrayOutputStream
(
1024
);
PrintStream
cacheStream
=
new
PrintStream
(
byteArrayOutputStream
);
//更换数据输出位置
System
.
setOut
(
cacheStream
);
//加密参数组装
String
[]
args
=
{
"input="
+
input
,
"password="
+
PASSWORD
,
"algorithm="
+
ALGORITHM
};
JasyptPBEStringEncryptionCLI
.
main
(
args
);
//执行加密后的输出
String
message
=
byteArrayOutputStream
.
toString
();
String
str
=
replaceBlank
(
message
);
int
index
=
str
.
lastIndexOf
(
"-"
);
//返回加密后的数据
Map
<
String
,
String
>
result
=
new
HashMap
<
String
,
String
>();
result
.
put
(
"input"
,
str
.
substring
(
index
+
1
));
result
.
put
(
"password"
,
PASSWORD
);
return
result
;
}
/**
* 替换制表符、空格、换行符
*
* @param str
* @return
*/
private
static
String
replaceBlank
(
String
str
)
{
String
dest
=
""
;
if
(!
StringUtils
.
isEmpty
(
str
))
{
Matcher
matcher
=
BLANK_PATTERN
.
matcher
(
str
);
dest
=
matcher
.
replaceAll
(
""
);
}
return
dest
;
}
public
static
void
main
(
String
[]
args
)
{
System
.
out
.
println
(
getEncryptedParams
(
"hello"
));
}
}
执行main方法,可以得到如下输出:
{
input
=
0JK4mrGjPUxkB4XuqEv2YQ
==,
password
=
yinjihaunkey
}
input就是hello加密之后的内容,将input的值复制存储到Apollo中,存储的格式需要按照一定的规则才行:
test
.
input
=
ENC
(
0JK4mrGjPUxkB4XuqEv2YQ
==)
需要将加密的内容用ENC包起来,这样jasypt才会去解密这个值。
使用的地方可以直接根据名称注入配置,比如:
@Value
(
"${test.input}"
)
private
String
input
;
input的值就是解密之后的值,使用者不需要关心解密逻辑,jasypt框架在内部处理好了。
jasypt整合Apollo也是有一些不足的地方,目前我只发现了下面几个问题:
在配置中心修改值后,项目中的值不会刷新
注入Config对象获取的值无法解密
@ApolloConfig
private
Config
config
;
@GetMapping
(
"/config/getUserName3"
)
public
String
getUserName3
()
{
return
config
.
getProperty
(
"test.input"
,
"yinjihuan"
);
}
上面列举的2个问题,跟jasypt的实现方式是有关系的,意味着这种加密的方式可能只适合数据库密码之类的,启动时是可以解密的,而且只是用一次,如果是某些比较核心的业务配置需要加密的话,jasypt是支持不了的,无法做到实时更新。下章节我会讲解如何修改Apollo的源码来解决这2个问题。
扩展Apollo支持存储加解密
前面章节中给大家介绍了如何使用jasypt为Apollo中的配置进行加解密操作,基本的需求是能够实现的,但还是有一些不足的地方。
jasypt只是在启动的时候将Spring中带有ENC(xx)这种格式的配置进行解密,当配置发生修改时无法更新。由于Apollo框架本身没有这种对配置加解密的功能,如果我们想实现加解密,并且能够动态的更新,就需要对Apollo的源码做一些修改来满足需求。
对源码修改还需要重新打包,笔者在这边介绍一个比较简单的实现方式,就是创建一个跟Apollo框架中一模一样的类名进行覆盖,这样也不用替换已经在使用的客户端。
如果配置中心存储的内容是加密的,意味着Apollo客户端从配置中心拉取下来的配置也是加密之后的,我们需要在配置拉取下来之后就对配置进行解密,然后再走后面的流程,比如绑定到Spring中。在这个业务点进行切入之后,配置中心加密的内容就可以自动变成解密后的明文,对使用者透明。
通过分析Apollo的源码,笔者找到了一个最合适的切入点来做这件事情,这个类就是com.ctrip.framework.apollo.internals.DefaultConfig,DefaultConfig是Coonfig接口的实现类,配置的初始化和获取都会经过DefaultConfig的处理。
在DefaultConfig内部有一个更新配置的方法updateConfig,可以在这个方法中对加密的数据进行解密处理:
private
void
updateConfig
(
Properties
newConfigProperties
,
ConfigSourceType
sourceType
)
{
Set
<
Object
>
keys
=
newConfigProperties
.
keySet
();
for
(
Object
k
:
keys
)
{
String
key
=
k
.
toString
();
String
value
=
newConfigProperties
.
getProperty
(
key
);
// 加密Value
if
(
value
.
startsWith
(
"ENC("
)
&&
value
.
endsWith
(
")"
))
{
logger
.
debug
(
"加密Value {}"
,
value
);
// 解密然后重新赋值
try
{
String
decryptValue
=
AesEncryptUtils
.
aesDecrypt
(
value
.
substring
(
3
,
value
.
length
()-
1
),
DECRYPT_KEY
);
newConfigProperties
.
setProperty
(
key
,
decryptValue
);
}
catch
(
Exception
e
)
{
logger
.
error
(
"加密配置解密失败"
,
e
);
}
}
}
m_configProperties
.
set
(
newConfigProperties
);
m_sourceType
=
sourceType
;
}
这边使用了AES来解密,也就是说配置中心的加密内容也需要用相同的加密算法进行加密,至于格式的话还是用的ENC(xx)这种格式来标识这就是一个加密的配置内容。解密之后将解密的明文内容重新赋值到Properties 中,其他的流程不变。
创建一个加密测试类,加密配置内容,复制存储到Apollo中
public
class
Test
{
public
static
void
main
(
String
[]
args
)
{
String
msg
=
"hello yinjihaun"
;
try
{
String
encryptMsg
=
AesEncryptUtils
.
aesEncrypt
(
msg
,
"1111222233334444"
);
System
.
out
.
println
(
encryptMsg
);
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
}
}
}
输出内容如下:
Ke4LIPGOp3jCwbIHtmhmBA==
存储到Apollo中需要用ENC将加密内容包起来,如下:
test.input = ENC(Ke4LIPGOp3jCwbIHtmhmBA==)
还是用之前的代码进行测试,Config获取和Spring注入的方式如可以成功的获取到解密的数据,并且在配置中心修改后也能实时推送到客户端成功解密。
本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。
尹吉欢
我不差钱啊
喜欢作者