最近公司项目要过等保,需要对如身份证信息、手机号、真实姓名等的敏感数据进行加密数据库存储,但在业务代码中对敏感信息进行手动加解密则十分不优雅,甚至会存在错加密、漏加密、业务人员需要知道实际的加密规则等的情况。由于这是一个技改类需求,与业务无关,考虑用自定义注解+aop来做。这样做对业务代码没有侵入,并且后期扩展非常方便。
参考了很多博客,比如自定义注解+拦截器加解密,相信你也一定看到很多这样的文章,都是粘贴复制的,在实际使用中发现了一个严重的bug:
当你在操作同一对象时,会重复加密导致解密出错。
例如:调用service的saveOrUpdate 方法插入一个数据库没有的user对象,会执行两次sql语句,导致加密了两次。
解决方法:
加密时拼上唯一标识前缀“sensitive_”,执行sql语句时,如果敏感字段的值有这个前缀,说明已经加密过,不再执行加密操作。
aes:
key: #你的key
/
* 敏感信息类注解
*/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
/
* 敏感字段注解
*/
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveFiled {
}
public interface EncryptUtil {
/**
*
* @param aesFields paramsObject所申明的字段
* @param paramsObject mapper中paramsType的实例
* @param
* @return
* @throws IllegalAccessException 字段不可访问的异常
* 这里为了写这个接口为了以后可以拓展掐他的加密类型
*/
<T> T aesEncrypt(Field[] aesFields, T paramsObject) throws Exception;
}
@Component
public class AESEncrypt implements EncryptUtil{
@Value("${aes.key}")
private String key;
/**
*
* @param aesFields paramsObject所申明的字段
* @param paramsObject mapper中paramsType的实例
* @param
* @return
* @throws IllegalAccessException 字段不可访问的异常
*/
@Override
public <T> T aesEncrypt(Field[] aesFields, T paramsObject) throws Exception {
for (Field aesField : aesFields) {
//取出所有被EncryptDecryptFiled注解的字段
SensitiveFiled filed = aesField.getAnnotation(SensitiveFiled.class);
if (!Objects.isNull(filed)) {
//将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
aesField.setAccessible(true);
Object object = aesField.get(paramsObject);
//这里暂时只对String类型来加密
if (object instanceof String) {
String value = (String) object;
String encrypt = value;
//修改: 如果有标识则不加密,没有则加密并加上标识前缀
if(!value.startsWith(AESUtil.KEY_SENSITIVE)) {
encrypt = AESUtil.encrypt(value, key);
encrypt = AESUtil.KEY_SENSITIVE + encrypt;
}
//开始对字段加密使用自定义的AES加密工具
aesField.set(paramsObject, encrypt);
}
}
}
return paramsObject;
}
}
public interface DecryptUtil {
/**
* 解密
*
* @param result
* @param
* @return
* @throws IllegalAccessException
*/
<T> T decrypt(T result) throws Exception;
@Component
public class AESDecrypt implements DecryptUtil {
@Value("${aes.key}")
private String key;
/**
* 解密
*
* @param result
* @param
* @return
* @throws IllegalAccessException
*/
@Override
public <T> T decrypt(T result) throws Exception {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
//去除所有被EncryptDecryptFiled注解的字段
SensitiveFiled sensitiveFiled = declaredField.getAnnotation(SensitiveFiled.class);
if (!Objects.isNull(sensitiveFiled)) {
//将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
declaredField.setAccessible(true);
//这里的result就相当于是字段的访问器
Object object = declaredField.get(result);
//只支持String解密
if (object instanceof String) {
String value = (String) object;
//修改:没有标识则不解密
if(value.startsWith(AESUtil.KEY_SENSITIVE)) {
value = value.substring(10);
value = AESUtil.decrypt(value, key);
}
//对注解在这段进行逐一解密
declaredField.set(result, value);
}
}
}
return result;
}
}
@Slf4j
@Component
/**
* @Intercepts注解开启拦截器
* type 属性指定当前拦截器使用StatementHandler 、ResultSetHandler、ParameterHandler,Executor的一种
* method 属性指定使用以上四种类型的具体方法(可进入class内部查看其方法)。
* args 属性指定预编译语句
*/
@Intercepts({
//@Signature注解定义拦截器的实际类型
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class EncryptInterceptor implements Interceptor {
private final AESEncrypt encryptUtil;
@Autowired
public EncryptInterceptor(AESEncrypt encryptUtil) {
this.encryptUtil = encryptUtil;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
//若指定ResultSetHandler ,这里则能强转为ResultSetHandler
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
//获取参数对象,即mapper中paramsType的实例
Field paramsFiled = parameterHandler.getClass().getDeclaredField("parameterObject");
//将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
paramsFiled.setAccessible(true);
//取出实例
Object parameterObject = paramsFiled.get(parameterHandler);
if (parameterObject != null) {
Class<?> parameterObjectClass = parameterObject.getClass();
//校验该实例的类是否被@SensitiveData所注解
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
if (Objects.nonNull(sensitiveData)) {
//取出当前类的所有字段,传入加密方法
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
encryptUtil.aesEncrypt(declaredFields, parameterObject);
}
}
//获取原方法的返回值
return invocation.proceed();
}
/**
* 一定要配置,加入此拦截器到拦截器链
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
@Slf4j
@Component
/**
* 这里是对找出来的字符串结果集进行解密所以是ResultSetHandler
* args是指定预编译语句
*/
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {
@Autowired
private AESDecrypt aesDecrypt;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//取出查询的结果
Object resultObject = invocation.proceed();
if (Objects.isNull(resultObject)) {
return null;
}
//基于selectList
if (resultObject instanceof ArrayList) {
ArrayList resultList = (ArrayList) resultObject;
if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
for (Object result : resultList) {
//逐一解密
aesDecrypt.decrypt(result);
}
}
//基于selectOne
}else {
if (needToDecrypt(resultObject)) {
aesDecrypt.decrypt(resultObject);
}
}
return resultObject;
}
/**
* 对单个结果集判空的一个方法
* @param object
* @return
*/
private boolean needToDecrypt(Object object) {
Class<?> objectClass = object.getClass();
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
return Objects.nonNull(sensitiveData);
}
/**
* 将此过滤器加入到过滤器链当中
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
@Component
public class AESUtil {
private static final String KEY_ALGORITHM = "AES";
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";//默认的加密算法
public static final String KEY_SENSITIVE = "sensitive_";
/**
* AES 加密操作
*
* @param content 待加密内容
* @return 返回Base64转码后的加密数据
*/
public static String encrypt(String content, String key) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);// 创建密码器
byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));// 初始化为加密模式的密码器
byte[] result = cipher.doFinal(byteContent);// 加密
//Base64是一种基于64个可打印字符来表示二进制数据的表示方法。
return Base64Utils.encodeToString(result);//通过Base64转码返回
} catch (Exception ex) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* AES 解密操作
*
* @param content
* @return
*/
public static String decrypt(String content, String key) {
try {
//实例化
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
//使用密钥初始化,设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));
//执行操作
//Base64是一种基于64个可打印字符来表示二进制数据的表示方法。
byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content));
return new String(result, StandardCharsets.UTF_8);
} catch (Exception ex) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* 生成加密秘钥
*
* @return
*/
private static Key getSecretKey(String key) throws NoSuchAlgorithmException {
//返回生成指定算法密钥生成器的 KeyGenerator 对象
KeyGenerator kg = null;
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(key.getBytes());
try {
kg = KeyGenerator.getInstance(KEY_ALGORITHM);
//AES 要求密钥长度为 128
kg.init(128, random);
//生成一个密钥
SecretKey secretKey = kg.generateKey();
return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);// 转换为AES专用密钥
} catch (NoSuchAlgorithmException ex) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}