@Auto-Annotation自定义注解——数据加密篇
自定义通用注解连更系列—连载中…
首页介绍:点这里
前言
前段时间在做密码测评时,遇到一个这样的业务场景,业务系统中的数据需要进行加密存储,在数据传输过程中进行加密传输。这就麻烦了,这数据可不光要加密,肯定也要解密呀,这再加上个传输加解密以及存储加解密,想想都麻烦,于是便想有什么办法能够写一套通用的代码来处理这种业务场景呢。
力求便捷,最先想到的便是自定义注解了,那么我能不能通过定义一个注解来标识哪些接口需要加解密传输,哪些字段需要加解密存储呢。于是数据加密篇便油然而生。
本章加解密实现,主要精确为字段级的加解密处理。
所需依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.69</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
自定义加解密注解@EncryptionFields
加密方法标记注解,做为一个接口加解密标记的作用
/** 加密方法标记注解 标记需加密的接口
* 与@EncryptionFields连用
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptionMethod {
}
数据存储字段加解密注解,做为对字段加解密处理标识符。
- 标识参数是否加解密存储
- 标识结果是否加解密返回
/** 数据存储字段加解密注解
* 与@EncryptionMethod连用
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptionFields {
/**
* 是否将接收到的参数解密存储
*/
boolean decryptStorage() default false;
/**
* 是否将接收到的参数加密存储
*/
boolean encryptStorage() default false;
/**
* 是否将查询到的结果解密返回
*/
boolean decryptReturn() default false;
/**
* 是否将查询到的结果加密返回
*/
boolean encryptReturn() default false;
}
加解密配置类
定义一些配置信息,如:采用哪种加密方式,加密密钥等信息
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "auto.encrypt")
public class EncryptTypeConfig {
/**
* 加密方式
*/
EncryptTypeEnum encryptType = EncryptTypeEnum.PLAIN;
/**
* 请求头公共密钥名称(非对称加密使用)
*/
String pubKeyName;
/**
* 私有密钥key
*/
String priKey;
}
加解密切面类
该类做为加解密核心类,主要对参数进行加解密处理
主要实现逻辑
方法执行前,对参数进行加解密存储
- 参数检查,只支持String类型的参数进行加解密处理
- 是否将接收到的参数解密存储(根据值中{xxx},获取到加密方式进行解密操作)
- 是否将接收到的参数加密存储(加密数据根据配置的加密规则操作,并把规则{xxx}拼接)
- 根据{xxx}中提供的加密方式,获取加解密对象,执行其实现类方法对数据进行加解密处理
方法执行后对结果数据加解密传输
- 参数检查,只支持String类型的参数进行加解密处理
- 是否将返回的结果解密返回(根据值中{xxx},获取到加密方式进行解密操作)
- 是否将查询到的结果加密返回(加密数据根据配置的加密规则操作,并把规则{xxx}拼接)
- 根据{xxx}中提供的加密方式,获取加解密对象,执行其实现类方法对数据进行加解密处理
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Slf4j
@Aspect
@Configuration
@ConditionalOnProperty(prefix = ConditionalConstants.AUTO_ENABLE,name = ConditionalConstants.ENCRYPTION_DATA,havingValue =ConditionalConstants.ENABLE_TRUE)
public class EncryptionAspect {
@Resource
private EncryptTypeConfig encryptTypeConfig;
@Resource
private List<IEncryptTypeStrategy> encryptTypeStrategyList;
@Around(value = "@annotation(encryptionMethod)")
public Object aroundFields(ProceedingJoinPoint jp, EncryptionMethod encryptionMethod) throws Throwable {
//1、方法执行前加密
//获取参数
Object[] args = jp.getArgs();
for (Object arg : args) {
//获取属性
Field[] declaredFields = arg.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
//获取属性上的注解
EncryptionFields encryptionField = declaredField.getAnnotation(EncryptionFields.class);
if (!ObjectUtils.isEmpty(encryptionField)) {
//检查并获取属性值
Object value = checkFieldValue(arg, declaredField);
if (ObjectUtils.isEmpty(value)){
continue;
}
//是否将接收到的参数解密存储
if (encryptionField.decryptStorage()) {
decryptData(arg, declaredField, value);
}
//是否将接收到的参数加密存储
if (encryptionField.encryptStorage()) {
encryptData(arg, declaredField, value);
}
}
}
}
//2、方法执行
Object result = jp.proceed();
//3、方法执行后加解密
if (!ObjectUtils.isEmpty(result)) {
Field[] declaredFields = result.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
//获取属性上的注解
EncryptionFields encryptionField = declaredField.getAnnotation(EncryptionFields.class);
if (!ObjectUtils.isEmpty(encryptionField)) {
//检查并获取属性值
Object value = checkFieldValue(result, declaredField);
if (ObjectUtils.isEmpty(value)){
continue;
}
//是否将查询到的结果解密返回
if (encryptionField.decryptReturn()) {
decryptData(result, declaredField, value);
}
//是否将查询到的结果加密返回
if (encryptionField.encryptReturn()) {
encryptData(result, declaredField, value);
}
}
}
}
return result;
}
/**
* 检查并获取字段属性值
*
* @param obj 参数对象
* @param declaredField 字段属性对象
* @return 结果集
* @throws ServerException 服务异常
* @throws IllegalAccessException 非法访问异常
*/
private Object checkFieldValue(Object obj, Field declaredField) throws IllegalAccessException, ServerException {
declaredField.setAccessible(true);
String name = declaredField.getName();
Object value = declaredField.get(obj);
if (ObjectUtils.isEmpty(value)){
return null;
}
if (!(value instanceof String)) {
throw new ServerException("参数类型有误非字符类型:" + name);
}
return value;
}
/**
* 加密数据(加密根据配置的加密规则操作,并把规则{xxx}拼接)
*
* @param obj 参数对象
* @param declaredField 字段属性对象
* @param value 属性值
* @throws ServerException 服务异常
* @throws IllegalAccessException 非法访问异常
*/
private void encryptData(Object obj, Field declaredField, Object value) throws ServerException, IllegalAccessException {
String key = encryptTypeConfig.getPriKey();
EncryptTypeEnum encryptType = encryptTypeConfig.getEncryptType();
IEncryptTypeStrategy encryptTypeStrategy = getEncryptTypeStrategy(encryptType);
String encryptStr = encryptTypeStrategy.encrypt(String.valueOf(value), key);
String encryptValue = StrPool.DELIM_START + encryptType + StrPool.DELIM_END + encryptStr;
declaredField.set(obj, encryptValue);
}
/**
* 解密数据 (根据值中{xxx},获取到加密方式进行解密操作)
*
* @param obj 参数对象
* @param declaredField 字段属性对象
* @param value 属性值
* @throws ServerException 服务异常
* @throws IllegalAccessException 非法访问异常
*/
private void decryptData(Object obj, Field declaredField, Object value) throws ServerException, IllegalAccessException {
String key = encryptTypeConfig.getPriKey();
EncryptTypeDTO encryptTypeDTO = splitEncryptType(String.valueOf(value));
IEncryptTypeStrategy encryptTypeStrategy = getEncryptTypeStrategy(encryptTypeDTO.getEncryptType());
String decryptStr = encryptTypeStrategy.decrypt(encryptTypeDTO.getEncryptValue(), key);
declaredField.set(obj, decryptStr);
}
/**
* 根据加密方式获取加密策略者
*
* @param encryptType 加密方式
* @return 结果集
* @throws ServerException 服务异常
*/
private IEncryptTypeStrategy getEncryptTypeStrategy(EncryptTypeEnum encryptType) throws ServerException {
IEncryptTypeStrategy encryptTypeStrategy = encryptTypeStrategyList.stream()
.filter(x -> x.isSupport().equals(encryptType))
.findFirst().orElse(null);
if (ObjectUtils.isEmpty(encryptTypeStrategy)) {
throw new ServerException("未找到该加密方式的实现");
}
return encryptTypeStrategy;
}
/**
* 获取加密数据中的加密方式({PLAIN}xxssdfsd)
*
* @param str 加密数据
* @return 结果集
* @throws ServerException 服务异常
*/
private EncryptTypeDTO splitEncryptType(String str) throws ServerException {
String[] split = str.split(StrPool.DELIM_END);
String encryptType = split[0].substring(1);
String encryptValue = split[1];
EncryptTypeEnum encryptTypeEnum = EncryptTypeEnum.valueOf(encryptType);
if (ObjectUtils.isEmpty(encryptTypeEnum)) {
throw new ServerException("加密参数类型有误");
}
return EncryptTypeDTO.builder().encryptType(encryptTypeEnum).encryptValue(encryptValue).build();
}
}
定义加解密接口
定义加解密接口规范
- 是否支持该加密方式
- 加密数据
- 解密数据
/**
* 加密接口
*
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public interface IEncryptTypeStrategy {
/**
* 是否支持该加密方式
*
* @return 结果集
*/
EncryptTypeEnum isSupport();
/**
* 加密数据
*
* @param str 原始数据
* @param key 公钥key(非对称加密使用)
* @return 结果集
*/
String encrypt(String str, String key);
/**
* 解密数据
*
* @param str 已加密数据
* @param key 公钥key(非对称加密使用)
* @return 结果集
*/
String decrypt(String str, String key);
}
加密方式一:明文加密
不对数据做任何加解密处理,明文存储与传输
/** 明文加密
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Component
public class PlainEncryptTypeAction implements IEncryptTypeStrategy{
@Override
public EncryptTypeEnum isSupport() {
return EncryptTypeEnum.PLAIN;
}
@Override
public String encrypt(String str, String key) {
return str;
}
@Override
public String decrypt(String str, String key) {
return str;
}
}
加密方式二:AES加解密
AES加密/解密算法是一种可逆的对称加密算法,这类算法在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥,一般用于服务端对服务端之间对数据进行加密/解密。
作为可逆且对称的块加密,AES加密算法的速度比公钥加密等加密算法快很多,在很多场合都需要AES对称加密,但是要求加密端和解密端双方都使用相同的密钥是AES算法的主要缺点之一。
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Component
public class AesEncryptTypeAction implements IEncryptTypeStrategy{
private AES aes;
@Resource
private EncryptTypeConfig encryptTypeConfig;
@PostConstruct
private void constructAes(){
SecretKey secretKey = SecureUtil.generateKey(EncryptTypeEnum.AES.getValue(),
encryptTypeConfig.getPriKey().getBytes(StandardCharsets.UTF_8));
this.aes = SecureUtil.aes(secretKey.getEncoded());
}
@Override
public EncryptTypeEnum isSupport() {
return EncryptTypeEnum.AES;
}
@Override
public String encrypt(String str, String key) {
str = aes.encryptBase64(str);
return str;
}
@Override
public String decrypt(String str, String key) {
str = aes.decryptStr(str);
return str;
}
public static void main(String[] args) {
SecretKey aes = KeyUtil.generateKey(EncryptTypeEnum.AES.getValue(),128);
String key = HexUtil.encodeHexStr(aes.getEncoded());
System.out.println(key);
}
}
加密方式三:SM2加解密
SM2算法是中国国家密码局推出的国产化算法,是基于椭圆曲线的非对称算法,相对于RSA算法,SM2具有密钥更小,运算速度更快,相同密钥长度下具有更高安全性等优势。
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
* 私钥:5b27a37cf7305146c062f4b59b257d1888e9f5e65c2017b9c48556aaf27d9f5b
* 公钥:02bb7b3cf323f53921f3cf2d05131144d7378c10ceb2fa5bfe3756643722f252a6
*/
@Component
public class Sm2EncryptTypeAction implements IEncryptTypeStrategy{
@Resource
private EncryptTypeConfig encryptTypeConfig;
@Override
public EncryptTypeEnum isSupport() {
return EncryptTypeEnum.SM2;
}
@Override
public String encrypt(String str, String key) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String publicKey = request.getHeader(encryptTypeConfig.getPubKeyName());
str = Sm2Utils.encrypt(publicKey, str);
return str;
}
@Override
public String decrypt(String str, String key) {
str = Sm2Utils.decrypt(encryptTypeConfig.getPriKey(), str);
return str;
}
}
标记加解密接口与属性
@EncryptionFields(decryptStorage = true,encryptStorage = true,decryptReturn = true,encryptReturn = true)
private String name;
@EncryptionFields(decryptStorage = false,encryptStorage = true,decryptReturn = false,encryptReturn = true)
private String age;
@EncryptionMethod
@PostMapping("saveUser")
private void saveUser(User user){
System.out.println("保存用户信息逻辑...");
}
总结
通过加密技术保证信息的机密性、完整性、鉴别性和不可否认性,使用相应的密钥解密后显示出加密前的内容,使信息只对允许可读的接收者可读,以防止私有化信息在网络中被拦截和窃取。
实现数据加解密时只需要确定注解的作用位置和运行时机,以上只是对数据加解密处理的简单实现,我这里抛砖引玉出一个实现思路,当然还可以对其进行扩展,如:考虑对不同数据类型的处理,扩展各种加解密方式DES、SM4等。后续通过不断的迭代优化,形成一套比较完整的加解密处理机制。