问题
在项目中需要对用户敏感数据进行脱敏处理,例如身份号、手机号等信息进行加密再入库。
解决思路
- 就是:一种最简单直接的方式,在所有涉及数据敏感的查询到对插入时进行密码加解密
- 方法二:有方法一到出现对所有重大问题的影响,需要考虑到问题的出现,并且需要考虑可能出现的组员时添加数据的方法。
最后决定采用mybatis的插件在mybatis的SQL执行和结果填充操作上进行切入。上层业务调用不再需要考虑数据的加敏同时也保证了数据的加解密
Mybatis 插件原理
Mybatis 的是通过拦截器实现的,Mabatis 支持对当事人进行拦截
实现
- 设置对参数中带有敏感参数字段的数据时进行加密
- 对返回的结果进行解密处理
根据不同的要求,我们只需要对ParameterHandler
和ResultSetHandler
进行切入。
定义特定注解,在切入时需要检查字段中是否包含注解来是否加解密
加注解
定义SensitiveData
注解
import java.lang.annotation.*; /** * 该注解定义在类上 * 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解 * 这个注解要配合EncryptTransaction注解 **/ @Inherited @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface SensitiveData { }
定义EncryptTransaction注解
import java.lang.annotation.*; /** * 该注解有两种使用方式 * ①:配合@SensitiveData加在类中的字段上 * ②:直接在Mapper中的方法参数上使用 **/ @Documented @Inherited @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptTransaction { }
加解密工具类
解密
public interface IDecryptUtil { /** * 解密 * * @param result resultType的实例 * @return T * @throws IllegalAccessException 字段不可访问异常 */ <T> T decrypt(T result) throws IllegalAccessException; }
加密接口
package sicnu.cs.ich.common.interceptor.transaction.service; import java.lang.reflect.Field; public interface IEncryptUtil { /** * 加密 * * @param declaredFields 加密字段 * @param paramsObject 对象 * @param <T> 入参类型 * @return 返回加密 * @throws IllegalAccessException 不可访问 */ <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException; } package sicnu.cs.ich.common.interceptor.transaction.service.impl; import org.springframework.stereotype.Component; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.lang.reflect.Field; import java.util.Objects; @Component public class DecryptImpl implements IDecryptUtil { /** * 解密 * * @param result resultType的实例 */ @Override public <T> T decrypt(T result) throws IllegalAccessException { //取出resultType的类 Class<?> resultClass = result.getClass(); Field[] declaredFields = resultClass.getDeclaredFields(); for (Field field : declaredFields) { //取出所有被DecryptTransaction注解的字段 EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); Object object = field.get(result); //String的解密 if (object instanceof String) { String value = (String) object; //对注解的字段进行逐一解密 try { field.set(result, DBAESUtil.decrypt(value)); } catch (Exception e) { e.printStackTrace(); } } } } return result; } }
加密实现类
package sicnu.cs.ich.common.interceptor.transaction.service.impl; import com.fasterxml.jackson.databind.ObjectReader; import org.springframework.stereotype.Component; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.io.ObjectInputStream; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Objects; import java.util.Random; @Component public class EncryptUtilImpl implements IEncryptUtil { @Override public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException { //取出所有被EncryptTransaction注解的字段 for (Field field : declaredFields) { EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); Object object = field.get(paramsObject); //暂时只实现String类型的加密 if (object instanceof String) { String value = (String) object; //加密 try { field.set(paramsObject, DBAESUtil.encrypt(value)); } catch (Exception e) { e.printStackTrace(); } } } } return paramsObject; } }
模拟类
package sicnu.cs.ich.common.interceptor.transaction.service.impl; import org.springframework.stereotype.Component; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.lang.reflect.Field; import java.util.Objects; @Component public class DecryptImpl implements IDecryptUtil { /** * 解密 * * @param result resultType的实例 */ @Override public <T> T decrypt(T result) throws IllegalAccessException { //取出resultType的类 Class<?> resultClass = result.getClass(); Field[] declaredFields = resultClass.getDeclaredFields(); for (Field field : declaredFields) { //取出所有被DecryptTransaction注解的字段 EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); Object object = field.get(result); //String的解密 if (object instanceof String) { String value = (String) object; //对注解的字段进行逐一解密 try { field.set(result, DBAESUtil.decrypt(value)); } catch (Exception e) { e.printStackTrace(); } } } } return result; } }
推荐一个开源免费的 Spring Boot 最全教程:
插件实现
参数插件ParameterInterceptor
切入mybatis设置参数时对敏感数据进行加密
Mybatis插件的使用就是通过实现Mybatis中的Interceptor
接口
再@Intercepts
注解
// 使用mybatis插件时需要定义签名 // type标识需要切入的Handler // method表示要要切入的方法 @Intercepts({ @Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class), }) package sicnu.cs.ich.common.interceptor.transaction; import com.baomidou.mybatisplus.core.MybatisParameterHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData; import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.sql.PreparedStatement; import java.util.*; @Slf4j // 注入Spring @Component @Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class), }) public class ParameterInterceptor implements Interceptor { @Autowired private IEncryptUtil IEncryptUtil; @Override public Object intercept(Invocation invocation) throws Throwable { //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler //若指定ResultSetHandler ,这里则能强转为ResultSetHandler MybatisParameterHandler parameterHandler = (MybatisParameterHandler) invocation.getTarget(); // 获取参数对像,即 mapper 中 paramsType 的实例 Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject"); parameterField.setAccessible(true); //取出实例 Object parameterObject = parameterField.get(parameterHandler); // 搜索该方法中是否有需要加密的普通字段 List<String> paramNames = searchParamAnnotation(parameterHandler); if (parameterObject != null) { Class<?> parameterObjectClass = parameterObject.getClass(); //对类字段进行加密 //校验该实例的类是否被@SensitiveData所注解 SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class); if (Objects.nonNull(sensitiveData)) { //取出当前当前类所有字段,传入加密方法 Field[] declaredFields = parameterObjectClass.getDeclaredFields(); IEncryptUtil.encrypt(declaredFields, parameterObject); } // 对普通字段进行加密 if (!CollectionUtils.isEmpty(paramNames)) { // 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射 Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql"); boundSqlField.setAccessible(true); BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler); PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0]; // 改写参数 processParam(parameterObject, paramNames); // 改写的参数设置到原parameterHandler对象 parameterField.set(parameterHandler, parameterObject); parameterHandler.setParameters(ps); } } return invocation.proceed(); } private void processParam(Object parameterObject, List<String> params) throws Exception { // 处理参数对象 如果是 map 且map的key 中没有 tenantId,添加到参数map中 // 如果参数是bean,反射设置值 if (parameterObject instanceof Map) { @SuppressWarnings("unchecked") Map<String, String> map = ((Map<String, String>) parameterObject); for (String param : params) { String value = map.get(param); map.put(param, value==null?null:DBAESUtil.encrypt(value)); } // parameterObject = map; } } private List<String> searchParamAnnotation(ParameterHandler parameterHandler) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException { Class<MybatisParameterHandler> handlerClass = MybatisParameterHandler.class; Field mappedStatementFiled = handlerClass.getDeclaredField("mappedStatement"); mappedStatementFiled.setAccessible(true); MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler); String methodName = mappedStatement.getId(); Class<?> mapperClass = Class.forName(methodName.substring(0, methodName.lastIndexOf('.'))); methodName = methodName.substring(methodName.lastIndexOf('.') + 1); Method[] methods = mapperClass.getDeclaredMethods(); Method method = null; for (Method m : methods) { if (m.getName().equals(methodName)) { method = m; break; } } List<String> paramNames = null; if (method != null) { Annotation[][] pa = method.getParameterAnnotations(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < pa.length; i++) { for (Annotation annotation : pa[i]) { if (annotation instanceof EncryptTransaction) { if (paramNames == null) { paramNames = new ArrayList<>(); } paramNames.add(parameters[i].getName()); } } } } return paramNames; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
返回值插件ResultSetInterceptor
package sicnu.cs.ich.common.interceptor.transaction; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData; import java.sql.Statement; import java.util.ArrayList; import java.util.Objects; import java.util.Properties; @Slf4j @Component @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class ResultSetInterceptor implements Interceptor { @Autowired private sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil IDecryptUtil; @Override public Object intercept(Invocation invocation) throws Throwable { //取出查询的结果 Object resultObject = invocation.proceed(); if (Objects.isNull(resultObject)) { return null; } //基于selectList if (resultObject instanceof ArrayList) { @SuppressWarnings("unchecked") ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject; if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) { for (Object result : resultList) { //逐一解密 IDecryptUtil.decrypt(result); } } //基于selectOne } else { if (needToDecrypt(resultObject)) { IDecryptUtil.decrypt(resultObject); } } return resultObject; } private boolean needToDecrypt(Object object) { Class<?> objectClass = object.getClass(); SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class); return Objects.nonNull(sensitiveData); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
使用
注意解在实体类上
import lombok.*; import org.springframework.security.core.userdetails.UserDetails; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData; @With @Builder @Data @NoArgsConstructor @AllArgsConstructor @SensitiveData // 插件只对加了该注解的类进行扫描,只有加了这个注解的类才会生效 public class User implements Serializable { private Integer id; private String username; private String openId; private String password; // 表明对该字段进行加密 @EncryptTransaction private String email; // 表明对该字段进行加密 @EncryptTransaction private String mobile; private Date createTime; private Date expireTime; private Boolean status = true; }
注解在参数上
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; @Mapper public interface UserMapper extends BaseMapper<User> { // 只需要在参数前加上@EncryptTransaction 即可 long countByEmail(@EncryptTransaction @Param("email") String email); long countByMobile(@EncryptTransaction @Param("mobile") String mobile); }im