前期内容导读:
- 开源加解密RSA/AES/SHA1/PGP/SM2/SM3/SM4介绍
- 开源AES/SM4/3DES对称加密算法介绍及其实现
- 开源AES/SM4/3DES对称加密算法的验证实现
- 开源非对称加密算法RSA/SM2实现及其应用
- 非对称加密算法RSA实现
- 开源非对称加密算法SM2实现
- Java开源接口微服务代码框架
- Json在开源SpringBoot/SpringCloud微服务中的应用
+------------+
| bq-log |
| |
+------------+
Based on SpringBoot
|
|
v
+------------+ +------------+ +------------+ +-------------------+
|bq-encryptor| +-----> | bq-base | +-----> |bq-boot-root| +-----> | bq-service-gateway|
| | | | | | | |
+------------+ +------------+ +------------+ +-------------------+
Based on BouncyCastle Based on Spring Based on SpringBoot Based on SpringBoot-WebFlux
+
|
v
+------------+ +-------------------+
|bq-boot-base| +-----> | bq-service-auth |
| | | | |
+------------+ | +-------------------+
ased on SpringBoot-Web | Based on SpringSecurity-Authorization-Server
|
|
|
| +-------------------+
+-> | bq-service-biz |
| |
+-------------------+
说明:
bq-encryptor
:基于BouncyCastle
安全框架,已开源 ,加解密介绍
,支持RSA
/AES
/PGP
/SM2
/SM3
/SM4
/SHA-1
/HMAC-SHA256
/SHA-256
/SHA-512
/MD5
等常用加解密算法,并封装好了多种使用场景、做好了为SpringBoot所用的准备;bq-base
:基于Spring框架的基础代码框架,已开源 ,支持json
/redis
/DataSource
/guava
/http
/tcp
/thread
/jasypt
等常用工具API;bq-log
:基于SpringBoot框架的基础日志代码,已开源 ,支持接口Access日志、调用日志、业务操作日志等日志文件持久化,可根据实际情况扩展;bq-boot-root
:基于SpringBoot,已开源 ,但是不包含spring-boot-starter-web
,也不包含spring-boot-starter-webflux
,可通用于servlet
和netty
web容器场景,封装了redis
/http
/定时器
/加密机
/安全管理器
等的自动注入;bq-boot-base
:基于spring-boot-starter-web
(servlet,BIO),已开源 ,提供常规的业务服务基础能力,支持PostgreSQL
/限流
/bq-log
/Web框架
/业务数据加密机加密
等可配置自动注入;bq-service-gateway
:基于spring-boot-starter-webflux
(Netty,NIO),已开源 ,提供了Jwt Token安全校验能力,包括接口完整性校验
/接口数据加密
/Jwt Token合法性校验等;bq-service-auth
:基于spring-security-oauth2-authorization-server
,已开源 ,提供了JwtToken生成和刷新的能力;bq-service-biz
:业务微服务参考样例,已开源 ;
+-------------------+
| Web/App Client |
| |
+-------------------+
|
|
v
+--------------------------------------------------------------------+
| | Based On K8S |
| |1 |
| v |
| +-------------------+ 2 +-------------------+ |
| | bq-service-gateway| +-------> | bq-service-auth | |
| | | | | |
| +-------------------+ +-------------------+ |
| |3 |
| +-------------------------------+ |
| v v |
| +-------------------+ +-------------------+ |
| | bq-service-biz1 | | bq-service-biz2 | |
| | | | | |
| +-------------------+ +-------------------+ |
| |
+--------------------------------------------------------------------+
说明:
bq-service-gateway
:基于SpringCloud-Gateway
,用作JwtToken鉴权,并提供了接口、数据加解密的安全保障能力;bq-service-auth
:基于spring-security-oauth2-authorization-server
,提供了JwtToken生成和刷新的能力;bq-service-biz
:基于spring-boot-starter-web
,业务微服务参考样例;k8s
在上述微服务架构中,承担起了服务注册和服务发现的作用,鉴于k8s
云原生环境构造较为复杂,实际开源的代码时,以Nacos
(为主)/Eureka
做服务注册和服务发现中间件;- 以上所有服务都以docker容器作为载体,确保服务有较好地集群迁移和弹性能力,并能够逐步平滑迁移至k8s的终极目标;
- 逻辑架构不等同于物理架构(部署架构),实际业务部署时,还有DMZ区和内网区,本逻辑架构做了简化处理;
加密机
:硬件+软件的加密设备,主要用来加密保护非常重要的信息,通常应用在政府、金融、保险、银行等部门/企业内非常重要的业务系统中。
加密机
不会对外暴露秘钥,适合对非常重要的内部数据(如:数据库表个人数据等)做加密安全处理(包括摘要、加密、解密、签名和验签等);加密机
不适合对接口做加密安全处理,因为接口加解密需要双方都要有秘钥;加密机
性能瓶颈较大,不适合对海量数据、高并发的微服务数据做安全处理;加密机
属于特种安全设备(国内都是国密算法的加密机),非常昂贵,本微服务解决方案只是通过软件来模拟实现;加密器
:纯软件实现的加密算法集合,主要用来保护相对重要的业务数据,是加密机
使用场景的补充,可通用于传统/微服务架构的系统中。
加密器
可以交换秘钥,适合对接口、业务配置参数做加密安全处理理(包括摘要、加密、解密、签名和验签等);加密器
的秘钥需要通过加密机
来加密,或者间接通过加密机
来加密(如:本微服务解决方案设计为:加密机
加密jasypt
秘钥->jasypt
加密加密器
秘钥->加密器
加密接口数据);加密器
仍然存在性能瓶颈,但是不受硬件集群的限制,可以通过分布式多节点、多线程去提升并发;SpringBoot
是在Spring框架的基础上,做了非常好的封装和三方件集成,使服务依赖一体化、配置集中化(基本上都有默认值)了,从而让微服务的开发变得简洁,但是简洁并不一定简单。另外,SpringBoot
是当下微服务开发的基础代码骨架,无论SpringCloud
还是SpringSecurity-OAuth
都是以之为基础代码,但SpringBoot
只是实现了多个服务实例的开发部署,没有解决多个服务实例之间的业务负载和交互。SpringCloud
就是在SpringBoot
代码骨架的基础上,通过云化
(分布式)的方式解决了服务路由、服务注册、服务发现、熔断降级、链路跟踪等SpringBoot
微服务的痛点,使得分布式微服务、云原生微服务变得更加完备。当然,SpringCloud
只是云原生的其中一种解决方案,当下更优雅的云原生方式还要首推K8S
。不仅仅是因为K8S
具备更加简洁的服务注册、服务发现等基础能力,更因为它一揽子方案解决了弹性扩缩容等微服务运维部署管理的难题。SpringSecurity-OAuth2
是基于SpringBoot
的Jwt OAuth2安全认证解决方案;加密机
和加密器
。加密机
因为是模拟实现,所以除了国密加密机外,还可以有模拟的国际加密机。SpringBoot
和SpringCloud
的关系,本章节主要介绍了既不依赖spring-boot-starter-web
(标准的常规SpringBoot项目)和也不依赖spring-boot-starter-webflux
(SpringCloud-Gateway项目)bq-service-biz
)/认证服务(bq-service-auth
)/鉴权网关(bq-service-gateway
)等各种微服务。微服务的架构设计参见Java开源接口微服务代码框架 。bq-boot-root
基础依赖:<dependency>
<groupId>com.biuqugroupId>
<artifactId>bq-boot-rootartifactId>
<version>1.0.4version>
dependency>
bq-boot-base
基础依赖:<dependency>
<groupId>com.biuqugroupId>
<artifactId>bq-boot-baseartifactId>
<version>1.0.4version>
dependency>
- 二者的差异是后者多了
spring-boot-starter-web
相关的依赖。- 大家也可以直接通过开源的业务服务(
bq-service-biz
)/认证服务(bq-service-auth
)/鉴权网关服务(bq-service-gateway
)代码来看整个微服务解决方案。
SpringBoot
中的自动注入配置服务为EncryptHsmConfigurer ,如下所示:@Configuration
public class EncryptHsmConfigurer
{
@Bean("hsmBatchKey")
@ConfigurationProperties(prefix = "bq.encrypt.hsm")
public List<EncryptorKey> hsmBatchKey()
{
List<EncryptorKey> batchKey = new ArrayList<>(Const.TEN);
return batchKey;
}
/**
* 注入加密机的配置秘钥信息
*
* @return 加密机的配置秘钥信息
*/
@Bean(EncryptorConst.HSM_KEYS)
public EncryptorKeys hsmKeys(@Qualifier("hsmBatchKey") List<EncryptorKey> batchKey)
{
EncryptorKeys keys = new EncryptorKeys();
keys.setKeys(batchKey);
keys.setGm(this.gm);
return keys;
}
/**
* 注入加密机服务门面
*
* @param hsmKeys 加密机的配置秘钥信息
* @return 加密机服务门面
*/
@Bean(EncryptorConst.HSM_SERVICE)
public HsmFacade hsmFacade(@Qualifier(EncryptorConst.HSM_KEYS) EncryptorKeys hsmKeys)
{
return new HsmFacade(hsmKeys);
}
/**
* 注入业务安全服务
*
* @param hsmFacade 加密机服务
* @return 业务安全服务
*/
@Bean
public BizHsmFacade hsmBizFacade(@Qualifier(EncryptorConst.HSM_SERVICE) HsmFacade hsmFacade)
{
return new BizHsmFacade(hsmFacade);
}
/**
* 对配置文件中加密的默认类型(国密/国际加密)
*/
@Value("${bq.encrypt.gm:true}")
private boolean gm;
}
bq:
encrypt:
#默认加密算法(true表示国密)
gm: true
#模拟的加密机(正常情况下,加密机的秘钥是在加密机服务中,此处是不用配置的)
hsm:
- algorithm: SM4Hsm
pri: e9c9ba0326f00c39254ee7675907514a
- algorithm: SM2Hsm
pri: 3081930201...
pub: 3059301306072a...
- algorithm: SM3Hsm
- algorithm: GmIntegrityHsm
- algorithm: AESHsm
pri: 7c9726e56ce9bc28bf9c92c264ce4e520f16b858078e4b887f17439c97e137d6
- algorithm: RSAHsm
pri: 308204bc02...
pub: 30820122300d06092a...
- algorithm: SHAHsm
- algorithm: UsIntegrityHsm
- 可以通过配置的
gm
为true
或false
来切换国密加密机和非国密加密机,默认为国密加密机;- 真正的加密机的秘钥是存储在专有硬件中的,不会明文暴露,此处是模拟,才会有明文的秘钥,这种不安全的设计在实际项目中并不会存在;
@Test
public void getEncryptHsm()
{
HsmFacade hsm = new HsmFacade(encryptorKeys);
Assert.assertNotNull(hsm.getEncryptHsm());
System.out.println("gm:" + encryptorKeys.isGm());
for (EncryptorKey key : encryptorKeys.getKeys())
{
String format = "Algorithm[%s],pri[%s],pub[%s],secret[%s].";
String pri = (key.getPri() == null ? null : key.getPri());
String pub = (key.getPub() == null ? null : key.getPub());
String secret = (key.getSecret() == null ? null : key.getSecret());
String log = String.format(format, key.getAlgorithm(), pri, pub, secret);
System.out.println(log);
}
}
${bq.encrypt.gm}
来选择加密算法的。其Jasypt加密秘钥的生成代码HsmFacadeTest 为:@Test
public void testJasyptKey()
{
String hsmKey = "e9c9ba0326f00c39254ee7675907514a";
String jasyptKey = "f056513b001bda32d80d1c6da4e59e0e";
List<EncryptorKey> keys = new ArrayList<>(32);
EncryptorKey sm4Key = new EncryptorKey();
sm4Key.setPri(hsmKey);
sm4Key.setAlgorithm(EncryptorFactory.SM4Hsm.getAlgorithm());
keys.add(sm4Key);
EncryptorKeys encKeys = new EncryptorKeys();
encKeys.setGm(true);
encKeys.setKeys(keys);
HsmFacade hsm = new HsmFacade(encKeys);
//1.对jasypt秘钥加密验证
String encJasyptKey = hsm.encrypt(jasyptKey);
System.out.println("jasypt dec key:" + jasyptKey + ",enc key:" + encJasyptKey);
}
@EnableEncryptableProperties
,如:bq-service-biz启动类 /bq-service-auth启动类 /@Configuration
public class JasyptEncryptConfigurer
{
/**
* 配置自动加解密的处理器
*
* @return 加解密处理器
*/
@Bean("jasyptStringEncryptor")
public StringEncryptor getEncryptor()
{
String confKey = this.key;
//兼容有加密机的场景(加密机会对配置文件的加密key进行加密)
if (null != this.hsmFacade)
{
//解密出真实的配置key
confKey = this.hsmFacade.decrypt(this.key);
}
BaseSecureSingleEncryption encryption;
if (this.gm)
{
encryption = EncryptionFactory.SecureSM4.createAlgorithm();
}
else
{
encryption = EncryptionFactory.SecureAES.createAlgorithm();
}
return new JasyptEncryptor(encryption, confKey);
}
/**
* 注入加密机(有才注入,否则忽略)
*/
@Autowired(required = false)
private HsmFacade hsmFacade;
/**
* 对配置文件是否为国密
*/
@Value("${bq.encrypt.gm:true}")
private boolean gm;
/**
* 对配置文件加密的sm4 key
*/
@Value("${bq.encrypt.enc}")
private String key;
}
上述jasypt自动注入的配置服务虽然可以同时支持有加密机和没有加密机2种场景,也支持国密加密机和国际加密算法加密机,但是要注意jasypt秘钥要与加密机的加密算法匹配。
public class JasyptEncryptor implements StringEncryptor
{
public JasyptEncryptor(BaseSecureSingleEncryption encryption, String key)
{
this.encryption = encryption;
this.key = Hex.decode(key);
}
@Override
public String encrypt(String s)
{
byte[] encryptBytes = this.encryption.encrypt(s.getBytes(StandardCharsets.UTF_8), key, null);
return Hex.toHexString(encryptBytes);
}
@Override
public String decrypt(String s)
{
String enc = s;
boolean keyExists = s.toLowerCase(Locale.US).startsWith(KEY_PREFIX);
if (keyExists)
{
enc = s.substring(KEY_PREFIX.length());
}
byte[] decryptBytes = this.encryption.decrypt(Hex.decode(enc), key, null);
return new String(decryptBytes, StandardCharsets.UTF_8);
}
/**
* 秘钥key的前缀
*/
private static final String KEY_PREFIX = "[key]";
/**
* 对称加密算法
*/
private final BaseSecureSingleEncryption encryption;
/**
* 全局的秘钥KEY
*/
private final byte[] key;
}
- Jasypt对Spring配置参数加密时,框架默认需要
ENC(...)
包裹进行区分密文和明文(没有就表示明文);- Jasypt加密配置参数时,存在2种情况:1.秘钥类,是Hex(16进制);2.密码类,直接是字符串;当前采取的策略是:秘钥类在
ENC(...)
包裹的基础上再添加[key]
,综合效果为:ENC([key]...)
表示Hex加密,ENC(...)
表示常规字符串加密;
@Test
public void testDbPwd()
{
String dbPwd = "postgres";
BaseSecureSingleEncryption handler = new Sm4SecureEncryption();
String key = "f056513b001bda32d80d1c6da4e59e0e";
JasyptEncryptor jasyptEncryptor = new JasyptEncryptor(handler, key);
String encDbPwd = jasyptEncryptor.encrypt(dbPwd);
String decDbPwd = jasyptEncryptor.decrypt(encDbPwd);
System.out.println(String.format("Jasypt encrypt db[%s]enc:ENC(%s),dec:%s", dbPwd, encDbPwd, decDbPwd));
}
运行结果为:Jasypt encrypt db[postgres]enc:ENC(02488027c31ba144f5f3a9b6c03de5559c323b8e220fb221),dec:postgres
则对应的yaml数据库参数配置 为:spring:
mvc:
log-request-details: true
datasource:
driver-class-name: org.postgresql.Driver
initialSize: 5
maxActive: 20
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
maxWait: 60000
minEvictableIdleTimeMillis: 300000
minIdle: 5
#回收的超时时间(单位:s)
removeAbandonedTimeout: 180
#回收时打印连接的异常信息
logAbandoned: true
testOnBorrow: false
testOnReturn: false
testWhileIdle: true
#连接在连接池中的最小生存时间
minEvictableIdleTimeMills: 60000
timeBetweenEvictionRunsMillis: 60000
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:postgresql://localhost:5432/postgres?&schema=public&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
username: postgres
password: ENC(02488027c31ba144f5f3a9b6c03de5559c323b8e220fb221)
#password: postgres
validationQuery: SELECT 'test' as txt
@Test
public void testEncSecurityByEnc()
{
BaseSecureSingleEncryption handler = new Sm4SecureEncryption();
String key = "f056513b001bda32d80d1c6da4e59e0e";
JasyptEncryptor jasyptEncryptor = new JasyptEncryptor(handler, key);
String keyFormat = "[key]%s";
String sm4Secure = "249ffc39f1dd696d0251e520f84d1650";
String sm4EncHex = String.format(keyFormat, jasyptEncryptor.encrypt(sm4Secure));
String sm4DecHex = jasyptEncryptor.decrypt(sm4EncHex);
System.out.println(String.format("SecureSM4[%s]enc:ENC(%s),dec:%s", sm4Secure, sm4EncHex, sm4DecHex));
String sm4 = "ad8a8b7dd5d37914372d898b26f79bba";
String sm4EncHex2 = String.format(keyFormat, jasyptEncryptor.encrypt(sm4));
String sm4DecHex2 = jasyptEncryptor.decrypt(sm4EncHex2);
System.out.println(String.format("SM4[%s]enc:ENC(%s),dec:%s", sm4, sm4EncHex2, sm4DecHex2));
String sm2Pri = "3081930...";
String sm2Pub = "305930130...";
String sm2PriEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(sm2Pri));
String sm2PriDecHex = jasyptEncryptor.decrypt(sm2PriEncHex);
System.out.println(String.format("SM2.pri[%s]enc:ENC(%s),dec:%s", sm2Pri, sm2PriEncHex, sm2PriDecHex));
String sm2PubEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(sm2Pub));
String sm2PubDecHex = jasyptEncryptor.decrypt(sm2PubEncHex);
System.out.println(String.format("SM2.pub[%s]enc:ENC(%s),dec:%s", sm2Pub, sm2PubEncHex, sm2PubDecHex));
String aesSecure = "a28e26046dc3f4bbbf1971c8ffd41d79647bbd9886f12c73d280c11e89fb189c";
String aesSecureEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(aesSecure));
String aesSecureDecHex = jasyptEncryptor.decrypt(aesSecureEncHex);
System.out.println(
String.format("SecureAES[%s]enc:ENC(%s),dec:%s", aesSecure, aesSecureEncHex, aesSecureDecHex));
String aes = "2964898070a1a5edfea7e880db767090a676bcc058b686ea9496b30d88e17a3a";
String aesEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(aes));
String aesDecHex = jasyptEncryptor.decrypt(aesEncHex);
System.out.println(String.format("AES[%s]enc:ENC(%s),dec:%s", aes, aesEncHex, aesDecHex));
String rsaPri = "308204bd020...";
String rsaPub = "30820122...";
String rsaPriEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(rsaPri));
String rsaPriDecHex = jasyptEncryptor.decrypt(rsaPriEncHex);
System.out.println(String.format("RSA.pri[%s]enc:ENC(%s),dec:%s", rsaPri, rsaPriEncHex, rsaPriDecHex));
String rsaPubEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(rsaPub));
String rsaPubDecHex = jasyptEncryptor.decrypt(rsaPubEncHex);
System.out.println(String.format("RSA.pub[%s]enc:ENC(%s),dec:%s", rsaPub, rsaPubEncHex, rsaPubDecHex));
String rsaPri2 = "308204bf...";
String rsaPub2 = "308201223...";
String rsaPriEncHex2 = String.format(keyFormat, jasyptEncryptor.encrypt(rsaPri2));
String rsaPriDecHex2 = jasyptEncryptor.decrypt(rsaPriEncHex2);
System.out.println(String.format("RSA.pri[%s]enc:ENC(%s),dec:%s", rsaPri2, rsaPriEncHex2, rsaPriDecHex2));
String rsaPubEncHex2 = String.format(keyFormat, jasyptEncryptor.encrypt(rsaPub2));
String rsaPubDecHex2 = jasyptEncryptor.decrypt(rsaPubEncHex2);
System.out.println(String.format("RSA.pub[%s]enc:ENC(%s),dec:%s", rsaPub2, rsaPubEncHex2, rsaPubDecHex2));
String pgpPri = "9503c604...";
String pgpPub = "99010d04...";
String pgpPwd = "p0g1p2U4!";
String pgpPriEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(pgpPri));
String pgpPriDecHex = jasyptEncryptor.decrypt(pgpPriEncHex);
System.out.println(String.format("PGP.pri[%s]enc:ENC(%s),dec:%s", pgpPri, pgpPriEncHex, pgpPriDecHex));
String pgpPubEncHex = String.format(keyFormat, jasyptEncryptor.encrypt(pgpPub));
String pgpPubDecHex = jasyptEncryptor.decrypt(pgpPubEncHex);
System.out.println(String.format("PGP.pub[%s]enc:ENC(%s),dec:%s", pgpPub, pgpPubEncHex, pgpPubDecHex));
String pgpPwdEncHex = jasyptEncryptor.encrypt(pgpPwd);
String pgpPwdDecHex = jasyptEncryptor.decrypt(pgpPwdEncHex);
System.out.println(String.format("PGP.pwd[%s]enc:ENC(%s),dec:%s", pgpPwd, pgpPwdEncHex, pgpPwdDecHex));
}
bq:
json:
snake-case: true
encrypt:
#默认加密算法(true表示国密)
gm: true
#经过jasypt加密的适用于本地加密和对外交互数据加密的加密器秘钥
security:
- algorithm: SecureSM4
pri: ENC([key]f7222de...)
- algorithm: SM4
pri: ENC([key]54952...)
- algorithm: SM2
pri: ENC([key]61835c8...)
pub: ENC([key]0b74f34...)
- algorithm: SM3
- algorithm: GM
- algorithm: SecureAES
pri: ENC([key]6916ae6...)
- algorithm: AES
pri: ENC([key]a6b7ec...)
- algorithm: RSA
pri: ENC([key]23708c8...)
pub: ENC([key]c1ae5a5...)
- algorithm: SHA-512
- algorithm: US
- algorithm: PGP
pri: ENC([key]29ff6fa2...)
pub: ENC([key]c315ac73...)
kid: pgpUser01
pwd: ENC(e71aebdc7b5e9e62224e4a2f75954fa702f785c85e45f863ac)
expire: 33219557748024
加密器具备对接口的不同客户配置不同的秘钥的能力,会在后面的接口加密章节介绍。
存储时加密后签名,查询时验签后解密
。鉴于加密机的处理逻辑可能让人比较费解,我详细讲下三级等保和商密通用的安全要求:
- 对个人隐私数据、重要的业务数据做
加密性
和完整性
保护;
加密性
:是针对1条数据(如:1个用户,包括了姓名、手机号、用户名、密码等)的单个字段而言的,包括可逆加密
和不可逆加密
,还包括了可逆加密
的解密;
可逆加密
:是指获取加密的数据时,还需要把数据还原了,如:姓名字段。一般采用AES256/SM4加密算法。不可逆加密
:是指数据不需要还原了,直接保存安全的摘要值即可,如:密码字段。一般采用SHA512/SM3摘要算法。另外,字段的不可逆加密并不是绝对的,需要根据实际业务场景来。安全的要求是一定要加密,并要做到安全问题最小化。
完整性
:是针对1条数据的多个字段而言的(如上的用户例子中,姓名、手机号、用户名这3个字段任意字段不得篡改),存储数据时需要添加完整性保护,吐出数据时,需要校验关联字段没有被篡改。一般是使用SHA512/SM3摘要算法对拼接的多个字段数据做摘要,然后再使用RSA2048/SM2对摘要做签名。加密性
和完整性
需要同时满足时,是要先做加密性,再针对加密性数据做完整性保护。
@Component
@Aspect
public class EncSecurityAop extends BaseAop
{
@Before(BEFORE_PATTERN)
@Override
public void before(JoinPoint joinPoint)
{
super.before(joinPoint);
}
@AfterReturning(value = AFTER_PATTERN, returning = "result")
@Override
public void after(JoinPoint joinPoint, Object result)
{
super.after(joinPoint, result);
}
@Override
protected void doBefore(Method method, Object[] args)
{
List<BaseSecurity> models = getModels(method, args, DisableSecurityAnn.class);
this.bizHsm.before(models);
}
@Override
protected void doAfter(Method method, Object[] args, Object result)
{
List<BaseSecurity> models = getModels(method, result, DisableSecurityAnn.class);
this.bizHsm.after(models);
}
/**
* 启用安全策略的注解
*/
private static final String ENABLE_SECURITY = "@annotation(com.biuqu.annotation.EnableSecurityAnn) && ";
/**
* 需要在dao的get方法
*/
private static final String GET_DAO = "(execution (* com.biuqu.boot.dao.*.*SecDao.*get*(..)))";
/**
* 需要在dao的add方法
*/
private static final String ADD_DAO = "(execution (* com.biuqu.boot.dao.*.*SecDao.*add*(..)))";
/**
* 需要在service的get方法
*/
private static final String GET_SVC =
ENABLE_SECURITY + "(execution (* com.biuqu.service.BaseBizService+.*get*(..)))";
/**
* 需要在service的add方法
*/
private static final String ADD_SVC =
ENABLE_SECURITY + "(execution (* com.biuqu.service.BaseBizService+.*add*(..)))";
/**
* 注入业务安全管理器
*/
@Autowired
private BizHsmFacade bizHsm;
}
- 此切面需要了解
MyBatis
的实现原理,MyBatis
是根据xml mapper动态生成DAO代理类,不是实现类,所以此处需要切入的是类名而不是实现类名。- 此切面考虑了在复杂业务场景下,DAO切面无法满足需求时,可通过自定义Service的方式去实现。
@Slf4j
public final class BizHsmFacade
{
public BizHsmFacade(HsmFacade hsm)
{
this.hsm = hsm;
}
/**
* 前置安全加密和签名
*
* @param model 业务模型
* @param 业务模型的类型
*/
public <T extends BaseSecurity> void before(T model)
{
//1.加密
this.beforeEncryption(model, EncryptionSecurityAnn.class);
this.beforeEncryption(model, FileSecurityAnn.class);
this.beforeEncryption(model, HashSecurityAnn.class);
//2.签名
this.beforeIntegrity(model, IntegritySecurityAnn.class);
}
/**
* 前置安全加密和签名
*
* @param models 批量业务模型
* @param 业务模型的类型
*/
public <T extends BaseSecurity> void before(List<T> models)
{
for (T model : models)
{
this.before(model);
}
}
/**
* 批量的前置安全签名
*
* @param models 批量业务模型
* @param 业务模型类型
*/
public <T extends BaseSecurity> void beforeIntegrity(List<T> models)
{
for (T model : models)
{
this.beforeIntegrity(model, IntegritySecurityAnn.class);
}
}
/**
* 后置安全解密和验签
*
* @param model 业务模型
* @param 业务模型的类型
*/
public <T extends BaseSecurity> void after(T model)
{
//1.验签
this.afterIntegrity(model, IntegritySecurityAnn.class);
//2.解密
this.afterEncryption(model, EncryptionSecurityAnn.class);
this.afterEncryption(model, FileSecurityAnn.class);
}
/**
* 后置安全解密和验签
*
* @param models 批量业务模型
* @param 业务模型的类型
*/
public <T extends BaseSecurity> void after(List<T> models)
{
for (T model : models)
{
this.after(model);
}
}
/**
* 后置安全验签
*
* @param models 批量业务模型
* @param 业务模型的类型
*/
public <T extends BaseSecurity> void afterIntegrity(List<T> models)
{
for (T model : models)
{
this.afterIntegrity(model, IntegritySecurityAnn.class);
}
}
/**
* 前置安全加密(数据机密性)
*
* @param model 业务模型
* @param annClazz 业务模型上的属性注解
* @param 业务模型类型
* @param 业务模型上的属性注解类型
*/
private <T extends BaseSecurity, A extends Annotation> void beforeEncryption(T model, Class<A> annClazz)
{
Set<Field> fields = ReflectionUtil.getFields(model.getClass(), annClazz);
for (Field field : fields)
{
Object value = ReflectionUtil.getField(model, field.getName());
if (!(value instanceof String))
{
return;
}
String newValue = value.toString();
if (annClazz == FileSecurityAnn.class)
{
this.beforeFileEncryption(model, field.getName());
return;
}
else if (annClazz == EncryptionSecurityAnn.class)
{
ReflectionUtil.updateField(model, field.getName(), hsm.encrypt(newValue));
}
else if (annClazz == HashSecurityAnn.class)
{
ReflectionUtil.updateField(model, field.getName(), hsm.hash(newValue));
}
}
}
/**
* 前置安全签名(数据完整性)
*
* @param model 业务模型
* @param annClazz 业务模型的方法注解
* @param 业务模型类型
* @param 业务模型上的方法注解类型
*/
private <T extends BaseSecurity, A extends Annotation> void beforeIntegrity(T model, Class<A> annClazz)
{
Set<Method> methods = ReflectionUtil.getMethods(model.getClass(), annClazz);
for (Method method : methods)
{
if (INTEGRITY_KEY.equalsIgnoreCase(method.getName()))
{
String integrity = model.toIntegrity();
model.setSecKey(hsm.sign(integrity));
}
}
}
/**
* 前置文件加密
*
* @param model 业务模型
* @param name 文件路径属性名称
* @param 业务模型类型
*/
private <T extends BaseSecurity> void beforeFileEncryption(T model, String name)
{
Class<? extends BaseSecurity> clazz = model.getClass();
Map<String, String> fields = ReflectionUtil.getFields(clazz, FileSecurityAnn.class, FileDataSecurityAnn.class);
String pathValue = ReflectionUtil.getField(model, name);
Object dataValue = ReflectionUtil.getField(model, fields.get(name));
this.encryptFile(pathValue, dataValue);
}
/**
* 后置文件解密
*
* @param model 业务模型
* @param name 文件路径属性名称
* @param 业务模型类型
*/
private <T extends BaseSecurity> void afterFileEncryption(T model, String name)
{
Class<? extends BaseSecurity> clazz = model.getClass();
Map<String, String> fields = ReflectionUtil.getFields(clazz, FileSecurityAnn.class, FileDataSecurityAnn.class);
//获取文件路径和文件内容对应的类型
String pathValue = ReflectionUtil.getField(model, name);
Field dataField = ReflectionUtils.findField(model.getClass(), fields.get(name));
String dataType = dataField.getGenericType().getTypeName();
//获取解密后的文件内容并更新至业务模型中
Object newData = this.decryptFile(pathValue, dataType);
ReflectionUtil.updateField(model, dataField.getName(), newData);
//返回文件内容后,把路径置空
ReflectionUtil.updateField(model, name, null);
}
/**
* 后置安全解密(数据机密性)
*
* @param model 业务模型
* @param annClazz 业务模型上的属性注解
* @param 业务模型类型
* @param 业务模型上的属性注解类型
*/
private <T extends BaseSecurity, A extends Annotation> void afterEncryption(T model, Class<A> annClazz)
{
Set<Field> fields = ReflectionUtil.getFields(model.getClass(), annClazz);
for (Field field : fields)
{
Object value = ReflectionUtil.getField(model, field.getName());
if (!(value instanceof String))
{
return;
}
String newValue = value.toString();
if (annClazz == FileSecurityAnn.class)
{
this.afterFileEncryption(model, field.getName());
return;
}
else if (annClazz == EncryptionSecurityAnn.class)
{
ReflectionUtil.updateField(model, field.getName(), hsm.decrypt(newValue));
}
}
}
/**
* 后置安全验签(数据完整性)
*
* @param model 业务模型
* @param annClazz 业务模型上的属性注解
* @param 业务模型类型
* @param 业务模型上的属性注解类型
*/
private <T extends BaseSecurity, A extends Annotation> void afterIntegrity(T model, Class<A> annClazz)
{
Set<Method> methods = ReflectionUtil.getMethods(model.getClass(), annClazz);
for (Method method : methods)
{
if (INTEGRITY_KEY.equalsIgnoreCase(method.getName()))
{
String integrity = model.toIntegrity();
boolean result = hsm.verify(integrity, model.getSecKey());
if (!result)
{
throw new CommonException(ErrCodeEnum.SIGNATURE_ERROR.getCode());
}
//签名成功后,删除签名值
model.setSecKey(null);
}
}
}
/**
* 加密文件
*
* @param path 文件路径
* @param data 文件内容
*/
private void encryptFile(String path, Object data)
{
if (data == null)
{
log.error("No data to encrypt:{}.", path);
return;
}
byte[] dataBytes = null;
if (data instanceof byte[])
{
dataBytes = (byte[])data;
}
else if (data instanceof String)
{
dataBytes = data.toString().getBytes(StandardCharsets.UTF_8);
}
if (dataBytes == null)
{
log.error("encrypt file error:{}.", path);
return;
}
byte[] encryptBytes = hsm.getEncryptHsm().encrypt(dataBytes);
FileUtil.write(encryptBytes, path);
}
/**
* 解密文件
*
* @param path 文件路径
* @param dataType 文件字段的类型
*/
private Object decryptFile(String path, String dataType)
{
byte[] encryptBytes = FileUtil.read(path);
byte[] data = hsm.getEncryptHsm().decrypt(encryptBytes);
if (STRING_TYPE.equalsIgnoreCase(dataType))
{
return new String(data);
}
else if (BYTE_ARRAY_TYPE.equalsIgnoreCase(dataType))
{
return data;
}
return null;
}
/**
* String类型
*/
private static final String STRING_TYPE = "java.lang.String";
/**
* 数组类型
*/
private static final String BYTE_ARRAY_TYPE = "byte[]";
/**
* 完整性方法名
*/
private static final String INTEGRITY_KEY = "toIntegrity";
/**
* 加密机
*/
private final HsmFacade hsm;
}
以bq-service-auth
认证服务的用户信息创建和查询为例来说明加密机对数据库表数据的加密处理。
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.biuqu.boot.dao.auth.ClientResourceSecDao">
<insert id="add" parameterType="ClientResource">
INSERT INTO sys_access(id, app_id, app_key, app_name, status, create_time, expire_time, sec_key)
VALUES (#{model.id}, #{model.appId}, #{model.appKey}, #{model.appName}, #{model.status}, #{model.createTime},
#{model.expireTime}, #{model.secKey})
insert>
<select id="get" resultType="ClientResource">
SELECT id,
app_id,
app_key,
app_name,
status,
create_time,
expire_time,
sec_key
FROM sys_access
WHERE app_id = #{model.appId}
AND status = '1'
AND expire_time > trunc(extract(epoch from now()) * 1000)
select>
mapper>
@Data
public class ClientResource extends BaseSecurity
{
@IntegritySecurityAnn
@Override
public String toIntegrity()
{
List<Object> keys = Lists.newArrayList();
keys.add(appId);
keys.add(appKey);
keys.add(appName);
keys.add(expireTime);
return StringUtils.join(keys, Const.SECURITY_LINK);
}
/**
* 客户的唯一标识
*/
private String appId;
/**
* 客户key
*/
@EncryptionSecurityAnn
private String appKey;
/**
* 客户名称
*/
@EncryptionSecurityAnn
private String appName;
/**
* 账号过期时间
*/
private long expireTime;
/**
* 账号创建时间
*/
private long createTime;
/**
* 账号状态(状态类型参见{@link com.biuqu.model.StatusType})
*/
private int status;
/**
* 客户能够访问的资源列表
*/
private Set<String> resources;
}
@Slf4j
@RestController
public class ClientResourceController
{
@PostMapping("/auth/user/add")
public ResultCode<ClientResource> execute(@RequestBody ClientResource client)
{
client.setId(IdUtil.uuid());
client.setCreateTime(System.currentTimeMillis());
client.setExpireTime(client.getCreateTime() + TimeUnit.DAYS.toMillis(365));
client.setStatus(StatusType.ENABLE.getStatus());
log.info("current user:{}", JsonUtil.toJson(client));
int code = dao.add(client);
log.info("add user result:{}", code);
ClientResource result = dao.get(client);
log.info("from db user:{}", JsonUtil.toJson(result));
result.setAppKey(null);
return ResultCode.ok(result);
}
@PostMapping("/auth/user/get")
public ResultCode<ClientResource> get(@RequestBody ClientResource client)
{
log.info("current user:{}", JsonUtil.toJson(client));
ClientResource result = dao.get(client);
log.info("from db user:{}", JsonUtil.toJson(result));
return ResultCode.ok(result);
}
/**
* dao操作
*/
@Autowired
private BizDao<ClientResource> dao;
}
curl --location 'http://localhost:9991/auth/user/add' \
--header 'Content-Type: application/json' \
--data '{
"app_id":"app001",
"app_key":"hao123",
"app_name":"bq-app",
"expire_time":1676800613607
}'
select * from sys_access;
,会发现新增了加密和完整性保护的用户数据:[
{
"id": "a4b42fa45e6f4dd8a942c34c62a6bf57",
"app_name": "2a6fd05579481bfa4b08ebe9251a7f88eb376f1ea6fe",
"app_id": "app001",
"app_key": "4b649584514495a77a50f72495c7f31351b1b493cb40",
"status": 1,
"expire_time": 1715348560590,
"create_time": 1683812560590,
"sec_key": "3045022100b5b41861fb3b87fd6e0f1cfce423eae5db77672fe5cde4e63c11a7fe58d2bc52022040d8e0de733b10baa64cb843071e577c1a998cb60e84b844f0e53688ddf4adb1"
}
]
curl --location 'http://localhost:9991/auth/user/get' \
--header 'Content-Type: application/json' \
--data '{
"app_id":"app001"
}'
{
"code": "100001",
"msg": "通过",
"data": {
"start": 0,
"id": "a4b42fa45e6f4dd8a942c34c62a6bf57",
"app_id": "app001",
"app_key": "hao123",
"app_name": "bq-app",
"expire_time": 1715348560590,
"create_time": 1683812560590,
"status": 1
},
"cost": 0
}
结合上一章节可知,查询的数据已自动做了完整性校验和解密。
加密器在SpringBoot
中的自动注入配置服务为EncryptSecurityConfigurer ,如下所示:
@Slf4j
@Configuration
public class EncryptSecurityConfigurer
{
@Bean("securityBatchKey")
@ConfigurationProperties(prefix = "bq.encrypt.security")
public List<EncryptorKey> securityBatchKey()
{
List<EncryptorKey> batchKey = new ArrayList<>(Const.TEN);
return batchKey;
}
/**
* 注入安全加密的配置秘钥信息
*
* @return 安全加密的配置秘钥信息
*/
@Bean(EncryptorConst.SECURITY_KEYS)
public EncryptorKeys securityKeys(@Qualifier("securityBatchKey") List<EncryptorKey> batchKey)
{
EncryptorKeys keys = new EncryptorKeys();
keys.setKeys(batchKey);
keys.setGm(this.gm);
return keys;
}
/**
* 注入安全加密服务门面
*
* @param securityKeys 安全加密的配置秘钥信息
* @return 安全加密服务门面
*/
@Bean(EncryptorConst.SECURITY_SERVICE)
public SecurityFacade securityFacade(@Qualifier(EncryptorConst.SECURITY_KEYS) EncryptorKeys securityKeys)
{
securityKeys.setGm(gm);
return new SecurityFacade(securityKeys);
}
/**
* 客户安全服务
*
* @param securityFacade 本地秘钥加密器的门面
* @return 客户安全服务
*/
@Bean
public ClientSecurity clientSecurity(@Qualifier(EncryptorConst.SECURITY_SERVICE) SecurityFacade securityFacade)
{
return new ClientSecurityImpl(securityFacade);
}
/**
* 对配置文件中加密的默认类型(国密/国际加密)
*/
@Value("${bq.encrypt.gm:true}")
private boolean gm;
}
加密器的自动注入配置同加密机的自动注入配置基本类似,不同的是在加密器门面注入的基础上,还额外注入了
ClientSecurity
用于简化接口加解密的调用。
对应的yaml配置 同加密机类似,略:
可以通过配置的
gm
为true
或false
来切换国密加密机和非国密加密器,默认为国密加密器;
加密器的秘钥如前文所述,是被Jasypt组件加密的。
秘钥生成的测试类SecurityFacadeTest ,代码略。
spring-security-oauth2-authorization-server
来生成JwtToken的,其中比较安全的加密算法是RSA,我们就RSA2048来扩展框架。bq-service-auth
的应用。但是当下spring-security-oauth2-authorization-server
框架还不支持国密算法SM2,只能使用RSA。Basic认证
的UserName/Password进行加密;@Slf4j
@Component
public class SecureBodyGatewayFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
//1.解析出该请求的摘要配置和加密配置
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
EncryptConfig encryptConf = this.match(url);
//2.没有加密配置则直接放过请求
if (null == encryptConf)
{
return chain.filter(exchange);
}
//3.缓存加密参数配置至全局缓存中,并构造响应对象随时接收响应结果并加密
String encId = request.getHeaders().getFirst(GatewayConst.HEADER_ENC_ID);
ServerHttpResponse response = exchange.getResponse();
if (encryptConf.needEnc())
{
exchange.getAttributes().put(GatewayConst.ENC_RESPONSE_ALG_KEY, encryptConf.getEnc());
if (StringUtils.isEmpty(encId))
{
encId = encryptConf.getEnc();
}
final String encAlg = encId;
response = new FluxResponseWrapper(exchange.getResponse())
{
@Override
protected byte[] doService(byte[] data)
{
//明文的业务结果
String result = new String(data, StandardCharsets.UTF_8);
log.info("before encrypt response body:{}", result);
EncResult encResult = new EncResult();
encResult.setResult(clientEncryptor.encrypt(encAlg, result));
String encJson = JsonUtil.toJson(encResult, snakeCase);
log.info("after encrypt response body:{}", encJson);
return encJson.getBytes(StandardCharsets.UTF_8);
}
};
}
String body = exchange.getAttribute(GatewayConst.BODY_CACHE_KEY);
//4.对请求数据做解密(包括替换请求body数据,替换请求header的body长度)
byte[] data = body.getBytes(StandardCharsets.UTF_8);
if (encryptConf.needDec())
{
if (StringUtils.isEmpty(encId))
{
encId = encryptConf.getDec();
}
log.info("**[{}]body encrypted[{}]={}", url, body, clientEncryptor.encrypt(encId, body));
EncParam param = JsonUtil.toObject(body, EncParam.class, snakeCase);
if (null == param || StringUtils.isEmpty(param.getParam()))
{
log.error("[{}]decrypt data error.", url);
return this.writeSecErr(exchange, encId, ErrCodeEnum.VALID_ERROR.getCode(), snakeCase);
}
String decBody = clientEncryptor.decrypt(encId, param.getParam());
log.info("[{}]decrypt body:{}", url, decBody);
data = decBody.getBytes(StandardCharsets.UTF_8);
}
ServerHttpRequest requestWrapper = FluxRequestWrapper.wrap(request, encryptConf.getRedirect(), data);
return chain.filter(exchange.mutate().request(requestWrapper).response(response).build());
}
/**
* 回写异常结果
*
* @param exchange server对象(包含request和response)
* @param encAlg 加密算法
* @param code 错误码
* @param snake 驼峰转换
* @return 标准的异常结果对象
* @secMgr 本地秘钥的加密服务
*/
private Mono<Void> writeSecErr(ServerWebExchange exchange, String encAlg, String code, boolean snake)
{
//1.构造常规的返回结果json
ResultCode<?> resultCode = ResultCode.error(code);
long start = Long.parseLong(exchange.getAttribute(GatewayConst.START_CACHE_KEY).toString());
resultCode.setCost(System.currentTimeMillis() - start);
String json = JsonUtil.toJson(resultCode, snake);
//2.如果设置了返回结果加密时,则要先对返回结果json加密
String enc = exchange.getAttribute(GatewayConst.ENC_RESPONSE_ALG_KEY);
if (!StringUtils.isEmpty(enc))
{
EncResult encResult = new EncResult();
encResult.setResult(clientEncryptor.encrypt(encAlg, json));
json = JsonUtil.toJson(encResult, snake);
}
//3.构造最终的json返回结果
return ServerUtil.writeErr(exchange, json);
}
/**
* 注入安全服务服务
*/
@Autowired
private ClientSecurity clientEncryptor;
}
- 接口加解密是需要双方协商的,一般采取RSA/SM2加密算法。不同的客户,秘钥不同,所以需要注入
ClientSecurity
服务。- 不同客户秘钥不同时的调用示例可参见
3.1.2 加密器做OAuth2 Client认证数据的加解密
章节的说明。
@Slf4j
@Component
public class IntegrityCheckGatewayFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
//1.解析出该请求的摘要配置和加密配置
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
boolean signed = checkConf.needSign(url);
PathMatcher pathMatcher = new AntPathMatcher();
boolean ignore = this.whitelist.stream().anyMatch(pattern -> pathMatcher.match(pattern, url));
//2.没有摘要或者在白名单里面的请求则直接放过请求
if (!signed || ignore)
{
return chain.filter(exchange);
}
//3.做完整性校验(使用加密器门面的默认摘要算法)
String body = exchange.getAttribute(GatewayConst.BODY_CACHE_KEY);
boolean result = checkIntegrity(request, body);
if (!result)
{
log.error("[{}]check integrity failed.", url);
return ServerUtil.writeErr(exchange, ErrCodeEnum.SIGNATURE_ERROR.getCode(), snakeCase);
}
log.info("[{}]check integrity successfully.", url);
return chain.filter(exchange);
}
/**
* 使用本地秘钥的默认加密器做摘要验证
*
* 拼接header认证头和body: `${Authorization}|${body}`,字段不存在或者为空时,使用空串代替
*
* @param request 请求对象
* @param body 缓存的body
* @return true表示检验通过
*/
private boolean checkIntegrity(ServerHttpRequest request, String body)
{
String sign = request.getHeaders().getFirst(GatewayConst.HEADER_INTEGRITY);
if (StringUtils.isEmpty(sign))
{
return false;
}
String auth = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.isEmpty(auth))
{
auth = StringUtils.EMPTY;
}
StringBuilder builder = new StringBuilder();
builder.append(auth);
String encId = request.getHeaders().getFirst(GatewayConst.HEADER_ENC_ID);
if (StringUtils.isEmpty(encId))
{
encId = StringUtils.EMPTY;
}
builder.append(Const.JOIN).append(encId);
if (StringUtils.isEmpty(body))
{
body = StringUtils.EMPTY;
}
builder.append(Const.JOIN).append(body);
String integrity = this.securityFacade.hash(builder.toString());
log.info("current signature:{},src:{}", integrity, sign);
return sign.equals(integrity);
}
/**
* 注入安全服务服务
*/
@Autowired
private SecurityFacade securityFacade;
/**
* 是否驼峰式json(默认支持)
*/
@Value("${bq.json.snake-case:true}")
private boolean snakeCase;
}
网关过滤器需要考虑加载顺序,参数配置,以及缓存请求报文等问题,此处仅需关注
SecurityFacade
的使用即可。
@Slf4j
@Component
public class SecureAuthGatewayFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
//解析出该请求的摘要配置和加密配置
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
//配置转发后,对header中的认证头做校验和解密
String encId = request.getHeaders().getFirst(GatewayConst.HEADER_ENC_ID);
if (authConf.getUrl().equals(url) && this.authConf.needDec())
{
String encAlg = encId;
if (StringUtils.isEmpty(encAlg))
{
encAlg = this.authConf.getDec();
}
String authorization = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
log.info("current auth encrypt[{}][{}]=[{}].", encAlg, authorization,
clientEncryptor.encrypt(encAlg, authorization));
String decAuth = clientEncryptor.decrypt(encAlg, authorization);
if (StringUtils.isEmpty(decAuth))
{
log.error("[{}]decrypt auth header failed.", url);
return ServerUtil.writeErr(exchange, ErrCodeEnum.SIGNATURE_ERROR.getCode(), snakeCase);
}
HttpHeaders headers = new HttpHeaders();
headers.put(HttpHeaders.AUTHORIZATION, Lists.newArrayList(decAuth));
String body = exchange.getAttribute(GatewayConst.BODY_CACHE_KEY);
if (StringUtils.isEmpty(body))
{
body = StringUtils.EMPTY;
}
byte[] data = body.getBytes(StandardCharsets.UTF_8);
request = FluxRequestWrapper.wrap(request, authConf.getRedirect(), headers, data);
}
return chain.filter(exchange.mutate().request(request).build());
}
/**
* 注入安全服务服务
*/
@Autowired
private ClientSecurity clientEncryptor;
}
认证接口加解密同报文加解密是类似的情况,一般采取RSA/SM2加密算法,也需要注入
ClientSecurity
服务。
public class ClientSecurityImpl implements ClientSecurity
{
public ClientSecurityImpl(SecurityFacade securityFacade)
{
this.securityFacade = securityFacade;
}
@Override
public String encrypt(String algName, String data)
{
Encryptor encryptor = securityFacade.getEncryptor(algName);
if (encryptor instanceof PgpEncryptor)
{
if (encryptor == ((BaseEncryptSecurity)securityFacade.getEncryptSecurity()).getPgpEncryptor())
{
return securityFacade.pgpEncrypt(data);
}
PgpEncryptor encEncryptor = (PgpEncryptor)encryptor;
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
byte[] encBytes = encEncryptor.encrypt(dataBytes, null);
return new String(encBytes, StandardCharsets.UTF_8);
}
else if (encryptor instanceof EncryptEncryptor)
{
EncryptEncryptor encEncryptor = (EncryptEncryptor)encryptor;
byte[] encBytes = encEncryptor.encrypt(data.getBytes(StandardCharsets.UTF_8), null);
return Hex.toHexString(encBytes);
}
return null;
}
@Override
public String decrypt(String algName, String data)
{
Encryptor encryptor = securityFacade.getEncryptor(algName);
if (encryptor instanceof PgpEncryptor)
{
if (encryptor == ((BaseEncryptSecurity)securityFacade.getEncryptSecurity()).getPgpEncryptor())
{
return securityFacade.pgpDecrypt(data);
}
PgpEncryptor encEncryptor = (PgpEncryptor)encryptor;
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
byte[] decBytes = encEncryptor.decrypt(dataBytes, null);
return new String(decBytes, StandardCharsets.UTF_8);
}
else if (encryptor instanceof EncryptEncryptor)
{
EncryptEncryptor encEncryptor = (EncryptEncryptor)encryptor;
byte[] decBytes = encEncryptor.decrypt(Hex.decode(data), null);
return new String(decBytes, StandardCharsets.UTF_8);
}
return null;
}
/**
* 真实的本地秘钥加密门面
*/
private final SecurityFacade securityFacade;
}
curl --location --request POST 'http://localhost:9992/oauth/enc/token?scope=read&grant_type=client_credentials' \
--header 'bq-integrity: ef5b4373e5c24c2c0ccd6700a3e9b70f2b3a80268f9d4b1cc5bf8740e5dd81ca' \
--header 'bq-enc: app001' \
--header 'Authorization: 04d726fd8806b1c0763fead3a90592e592d8abb8290a6b638208c19977ae2d52e3553e507ad72f7e62b33a70fdaca4d8416ec091a59fe7eb8246745b5b6c88c15db580847186e895b29b47a569d2fc4e70e1e63e44aa529b396db710663d9a0c0c0f3a171885f74e0d8eec10469cb757d02b57a850547dc03bfa23'
{
"code": "100001",
"msg": "通过",
"data": {
"access_token": "eyJraWQiOiI2ZTNjNmYzMWI2ODk0MjU0YWUwY2Q4ODdkZWFmMzMxOCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhcHAwMDEiLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTk5MSIsInJlc291cmNlcyI6WyJcL2F1dGhcL3d4Il0sInNvdXJjZV90eXBlIjoiU0RLIiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImF1ZCI6ImFwcDAwMSIsIm5iZiI6MTY4Njc0NzA1Miwic2NvcGUiOlsicmVhZCJdLCJqd3RfdHlwZSI6InRva2VuIiwiZXhwIjoxNjg2NzQ4ODUyLCJpYXQiOjE2ODY3NDcwNTIsImp0aSI6IjZhMzVmYjhlYTkwOTQ2NDA5MTU2N2ZjNzgxZDJlZDI5In0.Vu4SsIvnj_pGtLgZhFpTaoGroQQBwjXpz-fn0oUcg-5Ox41bPrQLycQbNe64IOHiceQ7Spl6wplzl1kF5DiAOeFceHWSD1-uR8el_ZHM5M6uh4gyYTsoZ2kx0Fv98SkxlU7bc4aZv5SdFZ206fCmqlsZ3qLMaie_UojR0yxchublsU9f_Av8D1x2JaU01qVfNRAyFS2GcGtwimulf2n7QrHwHXza4K5fiEfaCew3d1LFwhtNRxoLGKMbS1rZUYW0VpBkR3C6nF4JtY7fDV_-Xgn3QZPeaRn05yuo_cs_tJ03BNLyZ73Cgz1UQ60B4TKLpOBS4mhNMBsb9kwIV_8WvA",
"token_type": "Bearer",
"scope": "read",
"refresh_token": "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhcHAwMDEiLCJhdWQiOiJhcHAwMDEiLCJuYmYiOjE2ODY3NDcwNTIsInNjb3BlIjpbInJlYWQiXSwiand0X3R5cGUiOiJyZWZyZXNoIiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0Ojk5OTEiLCJzb3VyY2VfdHlwZSI6IlNESyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJleHAiOjE2ODY3NTA2NTIsImlhdCI6MTY4Njc0NzA1MiwianRpIjoiNGNmNDM3MGNiZjgxNDI5MmI4MGM0MWQ5YjZlN2YxYWIifQ.LYSxVSawAav957V34NAuMqaf8J7kIJXerjGBi7d6JR99EepV9UPm-7brRfq89Myw7Wpvjv7M2JbBTKh3YAbjGpCv40W6zlRDpPr8GW8XSDPGycXwFMXU6u7YuYJlwaG4HS4U-6jTWJx2ozwjDckr3q4AmXKE4kXvndwzD4sDalhiA1Sw5EpJoLjEpxJQmHzrM2Y2RoHsxRJd906pfhW52i4VnRzXU2bUOIzBwAL0bNEL0LqHT86hY8TVMYOeEGSWHKDoY5nvxLLA_RC5qxl32w5CsaCxc7z3Yfv1dGHSun8RLO-HDzHSxf03iTnF3DI5zOf4qbr8mGHuR6vzdHbGUQ",
"client_id": "app001",
"jti": "6a35fb8ea909464091567fc781d2ed29",
"resources": [
"/auth/wx"
],
"expires_in": 1799
},
"cost": 0
}
至此,说明根据客户id来设置不同的秘钥完全可行。
name
字段设置不同的标记即可(最好用appId)。bq:
encrypt:
#默认加密算法(true表示国密)
gm: true
#经过jasypt加密的适用于本地加密和对外交互数据加密的加密器秘钥
security:
- algorithm: SM2
name: app001
pri: ENC([key]61835c846...)
pub: ENC([key]0b74f3419...)
- algorithm: SM2
pri: ENC([key]61835c846...)
pub: ENC([key]0b74f341...)