目的:
对信息进行加密保存到数据库中,在读取数据库时能够看到解密后的数据。
关键点:
- 敏感实体类:比如要对User对象的密码字段进行加密解密,这个User类就是敏感实体类
- 敏感实体类注解:标明目标对象,为了过滤掉其他无关对象
- 敏感实体类的加密解密的字段:标明目标字段,在这个字段对其进行加密解密
- 拦截器上的参数method = "update" :代表着update和insert还有delete操作
步骤:
- 使用mybatis对数据进行增删改查
- 自定义两种注解:
第一种是作用于实体类上的注解,用于拦截器扫描,以找出目标实体类。
第二种是作用于实体类中的字段上的,用于拦截器扫描,以找出需要目标实体类中的目标字段进行加密解密。@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface DecryptField { String value() default ""; } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptField { String value() default ""; } @Target({ElementType.TYPE})//该参数代表是作用在类、方法上的 @Retention(RetentionPolicy.RUNTIME) public @interface SensitiveBean { String value() default ""; }
- 添加mybatis的拦截器(org.apache.ibatis.plugin.Interceptor),并在xml中注册该拦截器,实现Interceptor这个接口,并添加注解,拦截增删改查
<plugins> <!-- 加密解密的拦截器 --> <plugin interceptor="com.wisdom.scm.common.persistence.interceptor.AESInterceptor" /> </plugins>
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) })
这个注解代表着拦截执行器Executor的所有查询操作和所有更新操作(insert,update,delete)
- 在这个接口中需要做许多业务的操作
首先需要明白拦截器拦截的参数 invocation
这个参数可以获取到这条sql语句的所有信息,包括传入参数和sql语句,甚至语句的返回值。
所以,接下来的业务步骤就是判断该sql对象是否存在返回值,如果有,返回值的对象是否是敏感实体类。
以及该sql是不是更新操作,如果是,对该对象上的敏感字段进行加密
①首先需要对该sql语句进行判断,是否查询操作,这里是获取它的返回值,如果不存在就代表着是非查询操作,存在就对这个对象获取敏感实体类注解,如果不存在敏感实体类注解,就直接pass不操作。
这么做的目的是为了节省性能,如果是对非敏感实体类进行查询操作,这样就能直接在开头就过滤掉了
②到了这一步就是非查询操作了,只需要判断传入参数是否存在敏感实体类的注解,如果是,则代表该传入参数需要进行加密
③调用invocation.proceed()方法进行执行sql语句,该方法可以直接操作sql,并获取返回值
④判断返回值是否为空,为空就直接返回
⑤到了这一步,就代表着对查询操作的返回值进行解密了,以上的操作都已经决定了这里是敏感实体类的结果集。所以就直接对逐条数据进行判断,使用for循环判断对象上的每一个对象是否带有解密的注解,如果有,就对这个字段进行解密。
//以下是拦截器需要实现的方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
// 获取该sql语句的类型,例如update,insert
String methodName = invocation.getMethod().getName();
// 获取该sql语句放入的参数
Object parameter = invocation.getArgs()[1];
// 如果是查询操作,并且返回值不是敏感实体类对象,并且传入参数不为空,就直接调用执行方法,返回这个方法的返回值
// 方法中可以判断这个返回值是否是多条数据,如果有数据,就代表着是select 操作,没有就代表是update insert delete,
// 因为mybatis的dao层不能为非select操作设置返回值
if (statement.getResultMaps().size() > 0) {
// 获取到这个返回值的类属性
Class<?> type = statement.getResultMaps().get(0).getType();
// 返回值没有带敏感实体类对象注解
if (!type.isAnnotationPresent(SensitiveBean.class)) {
// 直接执行语句并返回
return invocation.proceed();
}
}
// 如果该参数为空,就不进行判断敏感实体类,直接执行sql
// 并且
// 如果判断该参数带有敏感实体类的注解,才对这个实体类进行扫描查看是否有加密解密的注解
if (parameter != null && sensitiveBean(parameter)) {
// 如果有就扫描是否是更新操作和是否有加密注解
// 如果是更新或者插入时,就对数据进行加密后保存在数据库
if (StringUtils.equalsIgnoreCase("update", methodName) ||
StringUtils.equalsIgnoreCase("insert", methodName)) {
// 对参数内含注解的字段进行加密
encryptField(parameter);
}
}
// 继续执行sql语句,调用当前拦截的执行方法
Object returnValue = invocation.proceed();
try {
// 当返回值类型为数组集合时,就判断是否需要进行数据解密
if (returnValue instanceof ArrayList<?>) {
List<?> list = (List<?>) returnValue;
// 判断结果集的数据是否为空
if (list == null || list.size() < 1) {
return returnValue;
}
Object object = list.get(0);
// 为空值就返回数据
if (object == null) {
return returnValue;
}
// 判断第一个对象是否有解密注解
Field[] fields = object.getClass().getDeclaredFields();
// 定义一个临时变量
int len;
if (fields != null && 0 < (len = fields.length)) {
for (Object o : list) {
//调用解密,在这个方法中针对某个带有解密注解的字段进行解密
decryptField(o);
}
}
}
} catch (Exception e) {
e.printStackTrace();
return returnValue;
}
return returnValue;
}
/**
* <p>声明这是一个泛型方法,让所有参数都能够调用这个方法扫描带有解密注解的字段,
* 进行解密,然后显示在前端页面中</p>
*
* @param <T>
*/
public <T> void decryptField(T t) {
// 获取对象的域
Field[] declaredFields = t.getClass().getDeclaredFields();
if (declaredFields != null && declaredFields.length > 0) {
// 遍历这些字段
for (Field field : declaredFields) {
// 如果这个字段存在解密注解就进行解密
if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
try {
// 获取这个字段的值
String fieldValue = (String) field.get(t);
// 判断这个字段的数值是否不为空
if (StringUtils.isNotEmpty(fieldValue)) {
// 首先调用一个方法判断这个数据是否是未经过加密的,如果可以解密就进行解密,解密失败就返回元数据
boolean canDecrypt;
canDecrypt = UpdateUtils.judgeDataForView(fieldValue);
if (canDecrypt) {
// 进行解密
String encryptData = Cryptos.aesDecrypt(fieldValue);
if (encryptData.equals("解密失败")) {
logger.error("该字段不是被加密的字段,需要联系管理员进行修改数据");
}
// 将值反射到对象中
field.set(t, encryptData);
} else {
// 不能解密的情况下,就不对这个对象做任何操作,即默认显示元数据
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 查看这个类是否带有敏感实体类注解,有就返回true,否则返回false
*
* @param t
* @param <T>
* @return
*/
public <T> boolean sensitiveBean(T t) {
// 判断是否带有敏感实体类的注解
if (t.getClass().isAnnotationPresent(SensitiveBean.class)) {
logger.info("带有敏感实体类的注解");
return true;
} else {
return false;
}
}
/**
* <p>声明这是一个泛型方法,让所有参数都能够调用这个方法扫描带有加密注解的字段,
* 进行加密,然后存在数据库中</p>
*
* @param <T>
*/
public <T> void encryptField(T t) {
Field[] declaredFields = t.getClass().getDeclaredFields();
if (declaredFields != null && declaredFields.length > 0) {
for (Field field : declaredFields) {
// 查找当字段带有加密注解,并且字段类型为字符串类型
if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) {
field.setAccessible(true);
String fieldValue = null;
try {
// 获取这个值
fieldValue = (String) field.get(t);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// 判断这个值是否为空
if (StringUtils.isNotEmpty(fieldValue)) {
try {
// 不为空时,就进行加密
field.set(t, Cryptos.aesEncrypt(fieldValue));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}
执行方案的充分必要条件
- 保证密钥不进行修改,如果需求更改为需要动态修改密钥,就需要进行二次开发
- 注册拦截器
影响:
- 数据库中的敏感数据得到加密,提高安全性
- 每次执行sql语句时都会触发到拦截器,给后台服务器增加压力
- 在解密数据和加密数据时,会导致用户等待时间增加
针对对象:
所有经由mybatis层执行的sql语句(不包括hibernate和jdbc)
触发的条件:
- 更新、插入、查询、删除等操作
- 目标实体上带有敏感实体类的注解
- 目标字段上带有加密解密的注解
使用的工具:
mybatis拦截器(插件)
使用拦截器+反射+泛型的好处
- 只需要在目标实体类上添加敏感实体类的注解,就能定位这个实体类
- 只需要在对象中的字段上添加加密解密的注解,就能实现数据的加密解密