Mybatis拦截器优雅的实现敏感数据的加解密

最近公司项目要过等保,需要对如身份证信息、手机号、真实姓名等的敏感数据进行加密数据库存储,但在业务代码中对敏感信息进行手动加解密则十分不优雅,甚至会存在错加密、漏加密、业务人员需要知道实际的加密规则等的情况。由于这是一个技改类需求,与业务无关,考虑用自定义注解+aop来做。这样做对业务代码没有侵入,并且后期扩展非常方便。

参考了很多博客,比如自定义注解+拦截器加解密,相信你也一定看到很多这样的文章,都是粘贴复制的,在实际使用中发现了一个严重的bug:

当你在操作同一对象时,会重复加密导致解密出错。
例如:调用service的saveOrUpdate 方法插入一个数据库没有的user对象,会执行两次sql语句,导致加密了两次。
Mybatis拦截器优雅的实现敏感数据的加解密_第1张图片

解决方法:

加密时拼上唯一标识前缀“sensitive_”,执行sql语句时,如果敏感字段的值有这个前缀,说明已经加密过,不再执行加密操作。

下面直接上修改后的代码:

  1. 首先在application.yml中配置key:
aes:
  key: #你的key
  1. 定义需要加密解密的敏感信息注解:
    为何PO上也要添加注解:主要是为了提升效率,如果PO上没有@SensitiveData,那就直接跳过加密处理。
 /
 * 敏感信息类注解
 */
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
 /
 * 敏感字段注解
 */
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveFiled {
}

  1. 定义加密接口及其实现类:
    定义加密接口,方便以后拓展加密方法(如AES加密算法拓展支持PBE算法,只需要注入时指定一下便可)
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;
    }
}
  1. 定义解密接口及其实现类:
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;
    }

}
  1. 定义加密解密拦截器
@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) {

    }

}

  1. AES加密工具类:
@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;
    }
  1. 给需要加密的实体类和字段加上注解
  2. 最后别忘记修改数据库敏感字断的长度
  3. 如果数据库已经存在未加密的数据,可以写个测试方法,更新下数据 。

你可能感兴趣的:(工作总结,spring,boot,java,后端,mybatis,mysql)