Spring切面加解密MySQL数据实战

1,134 阅读6分钟

前言

  • 上篇文章中描述了使用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,用于获取代理方法的执行结果)。
/**
 * <p>作用于类:标识当前实体需要进行结果解密操作.
 * <p>作用于字段:标识当前实体的字段需要进行加解密操作.
 * <p>作用于方法:标识当前mapper方法会被切面进行拦截,并进行数据的加解密操作.
 * <p>注意:如果作用于字段,那当前类必须先标注该注解,因为会优先判断类是否需要加解密,然后在判断字段是否需要加解密,否则只作用于字段不会起效
 * <p>注意:如果作用于方法,且有入参需要加密情况下,{@link #encryptParamIndex}属性必须有值,否则加密不起效
 *
 * @author zrh
 * @date 2022/1/4
 */
@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 {};
}

/**
 * mybatis内容加密切面类
 *
 * @author zrh
 * @date 2022/1/1
 */
@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();
            // 判断当前参数是否需要加密和加密字段下标是否为null
            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;
    }

    /**
     * 查询结果解密
     * @param result
     * @return
     */
    private Object resultDecrypt (Object result) {
        if (null == result || !(result instanceof String)) {
            return result;
        }
        return AesTools.decryptECB(String.valueOf(result));
    }

    /**
     * 查询参数加密
     * @param result
     * @return
     */
    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时,会出现下列几种场景:
    • 字符串参数查询对象类型数据,只是字符串入参加密;
    • 对象类型查询字符串数据,只是字符串结果解密;
    • 字符串参数查询字符串数据,字符串入参加密 + 字符串结果解密;
    • 方法是多参数情况下,只有其中一个或多个入参参数需要加密查询,这个参数可能是第一个,也可能是最后一个;
/**
 * @Author: ZRH
 * @Date: 2021/11/25 13:48
 */
@Mapper
public interface PhoneDataMapper {

    /**
     * 无参查询对象类型数据
     * @return
     */
    @Select("select id, phone, user_phone userPhone, name, real_name realName from phone_data")
    List<PhoneData> selectAll ();

    /**
     * 字符串参数查询对象类型数据
     * @param phone
     * @return
     */
    @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);

    /**
     * 对象类型查询字符串数据
     * @param phoneData
     * @return
     */
    @Crypt(encrypt = false)
    @Select("select user_phone from phone_data where user_phone = #{userPhone}")
    String selectPhoneBy (PhoneData phoneData);

    /**
     * 字符串参数查询字符串数据
     * @param phone
     * @return
     */
    @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);

    /**
     * 无参查询对象类型数据
     * @return
     */
    @Select("select id, phone, user_phone userPhone, name, real_name realName from phone_data where user_phone = #{userPhone}")
    List<PhoneData> selectList (PhoneData phoneData);
}

/**
 * @Author: ZRH
 * @Date: 2022/1/5 11:55
 */
@Slf4j
@RestController
public class AopMapperController {

    @Autowired
    private PhoneDataMapper phoneDataMapper;

    /**
     * 查询示例接口
     * @param phone
     * @return
     */
    @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切面中,先获取代理方法的参数,根据方法上注解判断其是否需要进行加密操作。
    • 获取代理方法的执行结果,再根据方法上注解配置是否需要解密操作。
    • 这样通过方法上增加注解,就完成自动安全加解密操作。
  • 如果代码中有问题或者有可以优化的思路,欢迎指出。虚心学习,共同进步 -_-