说明:本文介绍对项目中的数据加密的两种方案;
场景
源自真实的项目需求,需要我们对系统中的敏感数据(如手机号、证件号等)进行加密处理,即存入到数据库中的是加密后的密文数据。加密本身是不难的,选定一款算法,写个工具类,写个加密、解密方法,密钥存代码里就可以。历史数据,就写个一次性代码,读取、加密、写入即可。
难的地方,是如何适应系统功能,比如一个条件查询功能:
-
前端传入的是明文数据,如何匹配数据库中的密文数据;
-
后端返回的是密文数据,如何解密返回给前端才好;
经分析,可以有以下两种方案,这里以用户表中的password字段为例;
加解密算法就用Hutool包中SecureUtil中的这两个方法;
// 加密AES aes = SecureUtil.aes("9D7E445B58C6A275A78165429B42F8D3".getBytes());String encryptHex = aes.encryptHex("123456");System.out.println("密文:" + encryptHex);// 解密String decryptStr = aes.decryptStr(encryptHex);System.out.println("明文:" + decryptStr);
方案一:拦截器
可以使用MyBatis的拦截器,对参数,封装结果集,一进一出进行拦截,分别执行对应的加密、解密逻辑,即对参数进行加密,与数据库进行匹配,对结果集进行解密返回。
首先,创建一个自定义注解,打在实体类对象的属性上,表示该属性需要脱敏处理;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 数据脱敏注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitivityField {
}
User实体类对象,password字段需要脱敏;
import com.hezy.annotation.SensitivityField;
import lombok.Data;/*** 用户实体类*/
@Data
public class User implements Serializable {/*** 用户id*/private String id;/*** 用户名*/private String username;/*** 密码*/@SensitivityFieldprivate String password;/*** 部门ID*/private String deptId;
}
编写Mybatis拦截器类
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.hezy.annotation.SensitivityField;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Properties;/*** Mybatis 拦截器*/
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class MybatisInterceptor implements Interceptor {/*** 密钥*/private static final String SECRET_KEY = "9D7E445B58C6A275A78165429B42F8D3";/*** 拦截器方法*/@Overridepublic Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException, NoSuchFieldException {Object target = invocation.getTarget();// 1.参数处理if (target instanceof ParameterHandler) {// 获取参数ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();Field declaredField = parameterHandler.getClass().getDeclaredField("parameterObject");declaredField.setAccessible(true);Object parameterObject = declaredField.get(parameterHandler);// 对参数进行加密encryption(parameterObject);return invocation.proceed();}// 2.结果集处理if (target instanceof ResultSetHandler) {// 获取结果集Object resultObject = invocation.proceed();if (resultObject instanceof ArrayList) {ArrayList resultList = (ArrayList) resultObject;for (Object o : resultList) {decryption(o);}return resultList;} else {return resultObject.getClass();}}// 3.其他情况return invocation.proceed();}/*** 包装* @param target* @return*/@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}/*** 设置属性* @param properties*/@Overridepublic void setProperties(Properties properties) {System.out.println("This is setProperties method......");}/*** 加密*/private static void encryption(Object parameterObject) throws IllegalAccessException {// 取出参数,即User对象Class<?> parameterObjectClass = parameterObject.getClass();// 取出对象的所有字段Field[] declaredFields = parameterObjectClass.getDeclaredFields();// 遍历,判断是否有敏感字段for (Field field : declaredFields) {SensitivityField annotation = field.getAnnotation(SensitivityField.class);if (annotation == null) {continue;}field.setAccessible(true);// 有,则进行加密,将对象的字段值进行加密AES aes = SecureUtil.aes(SECRET_KEY.getBytes());field.set(parameterObject, new String(aes.encryptHex(field.get(parameterObject).toString().getBytes())));}}/*** 解密*/private static void decryption(Object o) throws IllegalAccessException {// 取出返回的结果集对象的所有字段Field[] declaredFields = o.getClass().getDeclaredFields();// 遍历,判断是否有敏感字段for (Field field : declaredFields) {SensitivityField annotation = field.getAnnotation(SensitivityField.class);if (annotation == null) {continue;}field.setAccessible(true);// 有,则进行解密,将对象的字段值进行解密AES aes = SecureUtil.aes(SECRET_KEY.getBytes());Object value = field.get(o);field.set(o, new String(aes.decryptStr(value.toString())));}}
}
配置类,将拦截器对象装配到IOC容器中
import com.hezy.interceptor.MybatisInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** Mybatis拦截器配置*/
@Configuration
public class MybatisInterceptorConfig {@Beanpublic MybatisInterceptor mybatisInterceptor() {return new MybatisInterceptor();}
}
测试一下,新增一个User对象,如下:
@Testpublic void insertUser() {User user = new User();user.setId("5");user.setUsername("测试");user.setPassword("123456");userMapper.insertUser(user);}
insertUser()方法
@Insert("insert into tb_user (id, username, password, dept_id) values (#{id}, #{username}, #{password}, #{deptId})")void insertUser(User user);
运行,没得问题,明文数据,落库就是密文;
再试下查询(根据密码查询,感觉怪怪的,嘿嘿)
@Testpublic void queryUser() {User user = new User();user.setPassword("123456");System.out.println(userMapper.selectUserByPassword(user));}
selectUserByPassword()方法
User selectUserByPassword(User user);
UserMapper.xml
<select id="selectUserByPassword" resultType="com.hezy.pojo.User">select * from tb_user where password = #{password}</select>
运行,可以看到password字段是密文传入,明文返回的,说明拦截器起作用了。
介绍完,再来评价这两种方案。
方案二:AOP
使用AOP+自定义注解,在需要进行处理的Mapper方法上打上注解,然后在AOP中进行加解密的处理。如下:
自定义注解,加载Mapper的方法上,用于识别需要操作的Mapper方法
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 方法级别的敏感字段处理注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SensitivityMethod {
}
AOP切面,用于处理具体的参数加密、结果集解密逻辑;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.hezy.annotation.SensitivityField;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;/*** 敏感字段处理切面*/
@Aspect
@Component
public class SensitivityAOP {/*** 密钥*/private static final String SECRET_KEY = "9D7E445B58C6A275A78165429B42F8D3";/*** com.hezy.mapper中的所有方法,或者加上@SensitivityMethod注解的方法*/@Pointcut("@annotation(com.hezy.annotation.SensitivityMethod) || execution(* com.hezy.mapper.*.*(..))")public void pt(){}/*** 环绕通知*/@Around("pt()")public Object aroundAop(ProceedingJoinPoint pjp) throws Throwable {// 1.获取方法参数Object[] args = pjp.getArgs();// 2.对参数进行加密if (args != null && args.length > 0) {encryption(args);}// 3.执行Mapper方法Object result = pjp.proceed();// 4.对结果集进行解密if (result == null) {return null;}decryption(result);// 5.返回return result;}/*** 加密*/private static void encryption(Object[] args) throws IllegalAccessException {for (Object arg : args) {// 取出参数的所有字段Field[] declaredFields = arg.getClass().getDeclaredFields();// 遍历字段for (Field declaredField : declaredFields) {SensitivityField annotation = declaredField.getAnnotation(SensitivityField.class);if (annotation == null) {continue;}// 如果是敏感字段,就加密declaredField.setAccessible(true);String string = (String) declaredField.get(arg);AES aes = SecureUtil.aes(SECRET_KEY.getBytes());String encryptHex = aes.encryptHex(string);declaredField.set(arg, encryptHex);}}}/*** 解密*/private static void decryption(Object result) throws IllegalAccessException {// 取出结果集的所有字段Field[] declaredFields = result.getClass().getDeclaredFields();// 遍历字段for (Field field : declaredFields) {SensitivityField annotation = field.getAnnotation(SensitivityField.class);if (annotation == null) {continue;}// 如果是敏感字段,就解密field.setAccessible(true);String string = (String) field.get(result);AES aes = SecureUtil.aes(SECRET_KEY.getBytes());String encryptHex = aes.decryptStr(string);field.set(result, encryptHex);}}
}
细节的地方与方案一差不多,测试一下新增和查询;
(新增)
(查询)
(效果一模一样)
讨论
直接说结论,推荐使用方案二。方案一对所有操作数据库的Mapper方法都拦截,不够灵活,而方案二使用切入点表达式 + 自定义注解,可只对特定方法进行操作。在需要脱敏的表比较多,Mapper方法多的情况下,一个一个加注解可能比较麻烦,但灵活,有操作的空间。
另外,我们这里讨论的加密方案,只涉及到以实体类对象为参数传入,以实体类对象为结果集返回,并没有考虑到仅传递一个敏感字段值为参数,或者返回一个Map对象,Map中存储一个敏感字段值,如 password:833124569c4f911b0181212681c0fee2,这些情况就需要我们根据项目中现存的Mapper方法去写对应的业务逻辑。考虑到扩展性,方案二也要更合适一点。
(仅传递一个敏感字段值)
@Testpublic void queryAll() {String password1 = "123456";System.out.println(userMapper.selectAllByPassword(password1));}
List<User> selectAllByPassword(String password);
<select id="selectAllByPassword" resultType="com.hezy.pojo.User">select * from tb_user where password = #{password}</select>
(无法查到我们想要查询的记录)
而且,方案一依赖于Mybatis框架,方案二用的是Spring的特性
总结
本文介绍了数据加密的两种方案