日常开发中经常会遇到用户敏感数据加密及脱敏入库的需求
一、需求
原始需求:数据在保存时进行加密或脱敏处理,对于加密数据取出时解密避免泄露用户敏感信息
需求分析:数据从前端通过http协议传输到后端经过业务逻辑处理后存入数据库,其中经历三个环节
1.前后端之间传输,是否需要加密,如果需要的话可以使用HTTPS加密;
2.数据到达后端服务,一般需要经过一些业务处理,此时加密没有意义;
3.数据经过处理后一般需要落库,在落库前需要对敏感数据进行加密;
入库前最后一步完成对数据加密,防止数据泄露暴露用户隐私
加密算法:加密算法有对称加密和非对称加密,考虑加密和解密效率选用对称算法加密
加密字段:加密字段具有不确定性,应该在数据库表结构设计时就确定需要加密脱敏的字段
tips:
1.某个字段加密后,该字段存取性能下降,加密字段越多性能下降就越多,需要注意
2.字段被加密后,该字段索引就没有太大意义,比如对手机号加密或脱敏,原来根据手机号设置的唯一索引可能就失效了
3.一些SQL查询可能也无法实现,如果是加密则原来的精确查询或普通查询都会失效,如果是脱敏的话可以根据手机号后四位进行查询,具体需要在设计上就定好规则。
4.如果是加密的话,原字段长度需要扩充,加密后的密文肯定比原文长
5.不要对主键加密
6.有时为了减少关联查询,会对表设计冗余字段。如果加密后需要考虑冗余数据的加密处理
二、实现方案
通过MyBatis加密脱敏数据,一般有两种方案:
1.使用拦截器对insert和update类型语句拦截,通过注解标注需要加密或脱敏的字段。读取加密数据时拦截query类型SQL,解密后放入字段 2.使用Mybatis类型转换器TypeHandler实现
2.1 拦截器加密脱敏字段
2.1.1 定义注解
为了标记需要加密和脱敏的字段我们可以设计两个注解来实现:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
/**
* 加密算法
* @return
*/
String type() default "";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Desensitize {
/**
* 前几位不脱敏
* @return
*/
int first() default 0;
/**
* 后几位不脱敏
* @return
*/
int last() default 0;
}
1.Encrypt注解为了加密使用,通过type可以设置不同的加密算法
2.Desensitize注解为了脱敏使用,通过first和last制定字符串前几位和后几位不需要加密
2.1.2 定义拦截器
在拦截器中拦截,判断目标方法是否是insert或update类型,如果是获取入参对象遍历其字段及字段上的注解是否包含目标注解,如果有则进行相应的业务处理。
Intercepts:注解需要一个Signature(拦截点)参数数组,通过Signature制定拦截那个对象里的哪些方法
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
/**
* 定义拦截点
* 只有符合拦截点的条件才会进入到拦截器
*/
Signature[] value();
}
Signature:制定需要拦截哪个2类对象的哪个方法
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
*/
Class<?> type();
/**
* 在定义拦截类的基础之上,在定义拦截的方法
*/
String method();
/**
* 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
* JAVA里面方法可能重载,故注意参数的类型和顺序
*/
Class<?>[] args();
}
@Slf4j
@RequiredArgsConstructor
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), @Signature(type = Executor.class, method = "queryCursor", args = {MappedStatement.class, Object.class, RowBounds.class})})
public class EncryptFieldInterceptor implements Interceptor {
/**
* 参数数组的长度
*/
private static final int ARGS_LENGTH = 6;
/**
* 拦截方法
* @param invocation 调用信息
* @return 调用结果
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1.获取参数MappedStatement,唯一编号:`${namespace}.${id}`记录该接口各种信息
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// 2.获取BoundSql,记录了几乎完整的SQL,参数使用占位符 ? 代替
BoundSql boundSql = null;
if (invocation.getArgs().length == ARGS_LENGTH) {
boundSql = (BoundSql) invocation.getArgs()[ARGS_LENGTH - 1];
} else {
Object parameter = invocation.getArgs()[1];
boundSql = mappedStatement.getBoundSql(parameter);
}
// 3.判断SQL是哪种类型
if (mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT)) {
// 4.获取入参对象class及字段
Class<?> javaType = boundSql.getParameterObject().getClass();
Field[] fields = javaType.getDeclaredFields();
// 5.遍历字段及其注解,如果有目标注解进行相应处理
for (Field field : fields) {
Annotation[] annotations = field.getAnnotations();
field.setAccessible(true);
Object originField = field.get(boundSql.getParameterObject());
if (originField != null && annotations.length > 0) {
for (Annotation annotation : annotations) {
if (annotation instanceof Encrypt) {
String encryptField = EncryptUtils.encrypt((String) originField);
field.set(boundSql.getParameterObject(), encryptField);
} else if (annotation instanceof Desensitize) {
int first = ((Desensitize) annotation).first();
int last = ((Desensitize) annotation).last();
String desensitizeField = EncryptUtils.desensitize((String) originField, first, last);
field.set(boundSql.getParameterObject(), desensitizeField);
}
}
}
field.setAccessible(false);
}
}
// 6.执行目标方法
return invocation.proceed();
}
/**
* 应用插件。如应用成功,则会创建目标对象的代理对象
*
* @param target 目标对象
* @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现
*/
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
/**
* 设置拦截器属性
*
* @param properties 属性
*/
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}