前言
- 上篇文章中描述了使用mybatis拦截器过滤加解密请求参数和查询结果:#Mybatis拦截器安全加解密MySQL数据实战
- 当时提了一个局限性问题:
- 主要实现逻辑是在MybatisCryptHandler处理工具类中,当前方式现只能处理请求参数和查询结果是对象类而不是字符串类型,在下篇文章中会介绍如何针对字符串进行过滤拦截。
- 所以本篇文章就是如何解决针对字符串进行过滤拦截。mybatis拦截器也可以拦截字符串参数,至于为什么不在Mybatis拦截器里面把字符串参数进行加解密的原因:
- 不是所有字符串类型数据都需要加解密,只有在用手机号字作为入参和查询结果才加解密数据。而mybatis拦截器无法直接区分本次拦截的字符串是否需要加解密,所以才需要额外配置字符串拦截过滤(这里的字符串包括:手机号,真实姓名,身份证、银行卡号、支付宝账号等数据)
Spring切面拦截
- Spring的aop在项目中使用场景还是比较广泛的,这次就使用次来进行字符串拦截过滤。
- 添加springboot切面maven依赖项
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
- 因为这里需要对代理方法的参数进行加密操作,并获取其执行结果进行解密操作,所以这里使用@Around环绕切面,并使用ProceedingJoinPoint做切面方法参数(ProceedingJoinPoint继承自JoinPoint,里面多了两个阻塞方法proceed,用于获取代理方法的执行结果)。
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Crypt {
boolean decrypt () default true;
boolean encrypt () default true;
boolean subObject () default false;
int[] encryptParamIndex () default {};
}
@Slf4j
@Aspect
@Component
public class MybatisCryptAspect {
@Around(value = "@annotation(crypt)")
public Object crypt (ProceedingJoinPoint joinPoint, Crypt crypt) throws Throwable {
Object result = null;
try {
Object[] params = joinPoint.getArgs();
if (crypt.encrypt() && crypt.encryptParamIndex().length > 0) {
for (int i : crypt.encryptParamIndex()) {
params[i] = paramEncrypt(params[i]);
}
}
result = joinPoint.proceed(params);
if (crypt.decrypt()) {
if (result instanceof String) {
result = resultDecrypt(result);
} else if (result instanceof List) {
result = ((List<?>) result).stream().map(this::resultDecrypt).collect(Collectors.toList());
}
}
} catch (Exception e) {
log.error("MybatisCryptAspect加密方法异常", e);
}
return result;
}
private Object resultDecrypt (Object result) {
if (null == result || !(result instanceof String)) {
return result;
}
return AesTools.decryptECB(String.valueOf(result));
}
private Object paramEncrypt (Object result) {
if (null == result) {
return null;
}
if (result instanceof String) {
return AesTools.encryptECB(String.valueOf(result));
}
if (result instanceof List) {
return ((List<?>) result).stream().map(o -> AesTools.encryptECB(String.valueOf(o))).collect(Collectors.toList());
}
return result;
}
}
- 在实际项目,直接使用字符串作为入参和查询结果的业务代码不多,其中又只定位需要加解密的特定字段,这样其实就更少了。这里就可以在对应的mapper方法上使用注解,利用aop切面形式进行加解密操作。
- Mapper方法因为都会最终都会走mybatis拦截器,对象类型又都会被mybatis拦截器进行拦截加解密。所以在mapper接口上的方法增加注解@Crypt时,会出现下列几种场景:
- 字符串参数查询对象类型数据,只是字符串入参加密;
- 对象类型查询字符串数据,只是字符串结果解密;
- 字符串参数查询字符串数据,字符串入参加密 + 字符串结果解密;
- 方法是多参数情况下,只有其中一个或多个入参参数需要加密查询,这个参数可能是第一个,也可能是最后一个;
@Mapper
public interface PhoneDataMapper {
@Select("select id, phone, user_phone userPhone, name, real_name realName from phone_data")
List<PhoneData> selectAll ();
@Crypt(decrypt = false, encryptParamIndex = 0)
@Select("select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = #{phone}")
PhoneData selectByPhone (@Param("phone") String phone);
@Crypt(encrypt = false)
@Select("select user_phone from phone_data where user_phone = #{userPhone}")
String selectPhoneBy (PhoneData phoneData);
@Crypt(encryptParamIndex = 1)
@Select("select user_phone from phone_data where phone = #{phone} and user_phone = #{userPhone}")
String selectPhoneByPhone (@Param("phone") String phone, @Param("userPhone") String userPhone);
@Select("select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = #{userPhone}")
List<PhoneData> selectList (PhoneData phoneData);
}
@Slf4j
@RestController
public class AopMapperController {
@Autowired
private PhoneDataMapper phoneDataMapper;
@GetMapping("/aop/select")
public String select (@RequestParam String phone) {
PhoneData build = PhoneData.build(phone);
List<PhoneData> selectList = phoneDataMapper.selectList(build);
log.info(" selectList = {}", JSON.toJSONString(selectList));
List<PhoneData> list = phoneDataMapper.selectAll();
log.info(" selectAll = " + JSON.toJSONString(list));
PhoneData phoneData = phoneDataMapper.selectByPhone(phone);
log.info(" selectByPhone = " + JSON.toJSONString(phoneData));
String phone1 = phoneDataMapper.selectPhoneBy(build);
log.info(" selectPhoneBy = " + phone1);
String selectPhoneByPhone = phoneDataMapper.selectPhoneByPhone(phone);
log.info(" selectPhoneByPhone = " + selectPhoneByPhone);
return "ok";
}
}
- 项目启动,访问查询接口,其sql日志打印出结果如下:
2022-01-07 16:15:21.896 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectList : ==> Preparing: select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = ?
2022-01-07 16:15:21.908 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectList : ==> Parameters: ZHlSotVArLBAviP2KWi3Cg==(String)
2022-01-07 16:15:21.956 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectList : <== Total: 1
2022-01-07 16:15:22.007 INFO 6300 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : selectList = [{"id":1,"name":"15222222222","phone":"15222222222","realName":"15222222222","userPhone":"15222222222"}]
2022-01-07 16:15:22.007 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectAll : ==> Preparing: select id, phone, user_phone userPhone, name, real_name realName from phone_data
2022-01-07 16:15:22.007 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectAll : ==> Parameters:
2022-01-07 16:15:22.044 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.mapper.PhoneDataMapper.selectAll : <== Total: 1
2022-01-07 16:15:22.045 INFO 6300 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : selectAll = [{"id":1,"name":"15222222222","phone":"15222222222","realName":"15222222222","userPhone":"15222222222"}]
2022-01-07 16:15:22.051 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.PhoneDataMapper.selectByPhone : ==> Preparing: select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = ?
2022-01-07 16:15:22.051 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.PhoneDataMapper.selectByPhone : ==> Parameters: ZHlSotVArLBAviP2KWi3Cg==(String)
2022-01-07 16:15:22.087 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.PhoneDataMapper.selectByPhone : <== Total: 1
2022-01-07 16:15:22.088 INFO 6300 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : selectByPhone = {"id":1,"name":"15222222222","phone":"15222222222","realName":"15222222222","userPhone":"15222222222"}
2022-01-07 16:15:22.089 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.PhoneDataMapper.selectPhoneBy : ==> Preparing: select user_phone from phone_data where user_phone = ?
2022-01-07 16:15:22.089 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.PhoneDataMapper.selectPhoneBy : ==> Parameters: ZHlSotVArLBAviP2KWi3Cg==(String)
2022-01-07 16:15:22.126 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.PhoneDataMapper.selectPhoneBy : <== Total: 1
2022-01-07 16:15:22.127 INFO 6300 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : selectPhoneBy = 15222222222
2022-01-07 16:15:22.127 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.P.selectPhoneByPhone : ==> Preparing: select user_phone from phone_data where phone = ? and user_phone = ?
2022-01-07 16:15:22.127 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.P.selectPhoneByPhone : ==> Parameters: 15222222222(String), ZHlSotVArLBAviP2KWi3Cg==(String)
2022-01-07 16:15:22.163 DEBUG 6300 --- [ XNIO-1 task-1] c.m.w.m.P.selectPhoneByPhone : <== Total: 1
2022-01-07 16:15:22.164 INFO 6300 --- [ XNIO-1 task-1] c.m.web.controller.AopMapperController : selectPhoneByPhone = 15222222222
总结
- 总结一下上述实现逻辑:
- 在AOP切面中,先获取代理方法的参数,根据方法上注解判断其是否需要进行加密操作。
- 获取代理方法的执行结果,再根据方法上注解配置是否需要解密操作。
- 这样通过方法上增加注解,就完成自动安全加解密操作。
- 如果代码中有问题或者有可以优化的思路,欢迎指出。虚心学习,共同进步 -_-