springboot+mysql数据库字段加密

数据库敏感字段加密


前言

提示:在我们进行开发的过程中,可能会遇到银行账号、用户身份证、手机号等铭感字段,用户会要求将这些数据库中的字段加密,以提高数据的安全性

本文章讲解如何实现数据库敏感字段加密,及加密后加密字段如何进行精确条件查询

一、需求

        目前有个用户表,需要对用户表的手机号、身份证字段加密,页面展示数据时解密。

        目标:

        【新增数据】接口请求参数:

        数据库结果(手机号、身份证已经加密):

         【查询返给前端】结果:

二、如何实现

1.简述

        思路:通过自定义注解,对实体类的字段进行标记,再重写mybatisplus的interceptor接口,对新增接口的入参、查询接口的结果集进行加密解密操作。

        项目的yml及pom文件自行配置,主要使用mysql8.0.27、mybatisplus3.5.2依赖包

2.自定义注解

  1)定义标记加解密的类和字段的注解

/**
 * com.oak.mybatisplus.annotation -> 敏感类
 */
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
/**
 * com.oak.mybatisplus.annotation -> 需要加密的字段
 */
@Documented
@Inherited
@Target({ElementType.FIELD  })
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
/**
 * com.oak.mybatisplus.annotation -> 解密字段
 */
@Documented
@Inherited
@Target({ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptTransaction {

}

3.加密实现类

1)遍历对象字段及注解信息并加密

package com.oak.mybatisplus.interceptor.handler;

/**
 * com.oak.mybatisplus.interceptor -> 实体类加密方法
 */
import cn.hutool.core.util.StrUtil;
import com.oak.mybatisplus.annotation.EncryptTransaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Objects;

@Component
public class EncryptImpl implements EncryptUtil{
    @Autowired
    private AESUtil aesUtil;
    @Override
    public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
        for (Field field : declaredFields) {
            //取出所有被EncryptTransaction注解的字段
            EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
            if (!Objects.isNull(encryptTransaction)) {
                field.setAccessible(true);
                Object object = field.get(paramsObject);
                //暂时只实现String类型的加密
                if (object instanceof String) {
                    String value = (String) object;
                    //加密
                    try {
                        if(StrUtil.isNotBlank(value)){
                            field.set(paramsObject, aesUtil.encrypt(value));
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return paramsObject;
    }
}

2)遍历对象字段及注解信息并解密

package com.oak.mybatisplus.interceptor.handler;

import com.oak.mybatisplus.annotation.DecryptTransaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Objects;

/**
 * com.oak.mybatisplus.interceptor -> 解密字段
 */
@Component
public class DecryptImpl implements DecryptUtil{

    @Autowired
    private AESUtil aesUtil;

    /**
     * 解密
     * @param result resultType的实例
     * @param <T>
     * @return
     * @throws IllegalAccessException
     */
    @Override
    public <T> T decrypt(T result) throws IllegalAccessException {
        //取出resultType的类
        Class<?> resultClass = result.getClass();
        Field[] declaredFields = resultClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //取出所有被DecryptTransaction注解的字段
            DecryptTransaction decryptTransaction = field.getAnnotation(DecryptTransaction.class);
            if (!Objects.isNull(decryptTransaction)) {
                field.setAccessible(true);
                Object object = field.get(result);
                //String的解密
                if (object instanceof String) {
                    String value = (String) object;
                    //对注解的字段进行逐一解密
                    try {
                        field.set(result, aesUtil.decrypt(value));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return result;
    }
}

3)加解密工具类

       AESUtil是一个加解密工具类,具体实现方法可根据项目要求编写。

4.拦截器实现

 1)新增参数加密

import com.oak.mybatisplus.annotation.SensitiveData;
import com.oak.mybatisplus.interceptor.handler.EncryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.Objects;
import java.util.Properties;


/**
 * com.oak.mybatisplus.interceptor -> sql查询参数加密
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class ParameterInterceptor  implements Interceptor {

    @Autowired
    private EncryptUtil encryptUtil;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
        //若指定ResultSetHandler ,这里则能强转为ResultSetHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        // 获取参数对像,即 mapper 中 paramsType 的实例
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        //取出实例
        Object parameterObject = parameterField.get(parameterHandler);
        if (parameterObject != null) {
            Class<?> parameterObjectClass = parameterObject.getClass();
            //校验该实例的类是否被@SensitiveData所注解
            SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class);
            if (Objects.nonNull(sensitiveData)) {
                //取出当前当前类所有字段,传入加密方法
                Field[] declaredFields = parameterObjectClass.getDeclaredFields();
                encryptUtil.encrypt(declaredFields, parameterObject);
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

2)查询结果解密

import com.oak.mybatisplus.annotation.SensitiveData;
import com.oak.mybatisplus.interceptor.handler.DecryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.CollectionUtils;

import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;

/**
 * com.oak.mybatisplus.interceptor -> sql返回结果解密
 * @Date: 2022/10/24
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetInterceptor implements Interceptor {

    @Autowired
    private DecryptUtil decryptUtil;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //取出查询的结果
        Object resultObject = invocation.proceed();
        if (Objects.isNull(resultObject)) {
            return null;
        }
        //基于selectList
        if (resultObject instanceof ArrayList) {
            ArrayList resultList = (ArrayList) resultObject;
            if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
                for (Object result : resultList) {
                    //逐一解密
                    decryptUtil.decrypt(result);
                }
            }
            //基于selectOne
        } else {
            if (needToDecrypt(resultObject)) {
                decryptUtil.decrypt(resultObject);
            }
        }
        return resultObject;
    }


    private boolean needToDecrypt(Object object) {
        Class<?> objectClass = object.getClass();
        SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
        return Objects.nonNull(sensitiveData);
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

5.使用

注解作用在实体类及字段上

6.新需求

        由于数据库字段被加密了,想模糊查询只能在内存中用lambda做,而分页查询建议只做精确查询,接下来看如何实现         

此时我们可以重写MybatisPlusInterceptor的intercept方法对参数加密,也可以自定义参数拦截器,直接对查询条件加密。

          接下来讲解一下自定义参数加密:

1)参数加密注解

/**
 * 参数加密
 */
@Documented
@Inherited
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptParam {
}

2)参数解析器

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.oak.mybatisplus.annotation.EncryptParam;
import com.oak.mybatisplus.interceptor.handler.AESUtil;
import com.oak.mybatisplus.interceptor.handler.EncryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;

/**
 * 自定义参数解析
 */
@Slf4j
@Component
public class EncryptParamArgumentResolver implements HandlerMethodArgumentResolver {


    @Autowired
    private EncryptUtil encryptUtil;

    @Autowired
    private AESUtil aesUtil;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        parameter.getParameterAnnotations();
        return parameter.hasParameterAnnotation(EncryptParam.class);
    }

    /**
     * 解析参数
     * @return
     * @throws Exception
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest
            , WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        String name = parameter.getParameterName();
        assert request != null;
        log.info("请求方式:【{}】;请求接口:【{}】", request.getMethod(),request.getRequestURI());
        String requestStr = IOUtils.toString(request.getInputStream(), "utf-8");
        // 解析post参数
        if (StrUtil.isNotBlank(requestStr)) {
            Object entity = JSONUtil.toBean(requestStr, parameter.getParameterType());
            Field[] fields = entity.getClass().getDeclaredFields();
            encryptUtil.encrypt(fields, entity);
            log.info("请求参数【{}】加密:加密前{},加密后:{}", name,requestStr, JSONUtil.toJsonStr(entity));
            return entity;
        }
        // 解析get参数
        String val = request.getParameter(name);
        if(StrUtil.isBlank(val)){
            return val;
        }
        String ent = aesUtil.encrypt(val);
        log.info("请求参数【{}】加密:加密前{},加密后:{}", name, val, ent);
        return ent;
    }

}

3)添加自定义解析器

        由于@requestbody等注解的顺序在我们自定义注解之前,注解执行器遍历时会优先执行其他的参数解析器并跳出循环,所以我们需要把自定义参数解析器放在第一位。

import com.oak.mybatisplus.interceptor.EncryptParamArgumentResolver;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import java.util.ArrayList;
import java.util.List;

/**
 * com.oak.mybatisplus.config -> 自定义参数解析器加载顺序置前
 */
@Component
public class ResolverBeanPostProcessor implements BeanPostProcessor {

    @Autowired
    private EncryptParamArgumentResolver encryptParamArgumentResolver;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("-------------------------------" + beanName);
        if("requestMappingHandlerAdapter".equals(beanName)){
            //requestMappingHandlerAdapter进行修改
            RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter)bean;
            List<HandlerMethodArgumentResolver> argumentResolvers = adapter.getArgumentResolvers();
            //添加自定义参数处理器
            argumentResolvers = addArgumentResolvers(argumentResolvers);
            adapter.setArgumentResolvers(argumentResolvers);
        }
        return bean;
    }

    private List<HandlerMethodArgumentResolver> addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
        //将自定的添加到最前面
        resolvers.add(encryptParamArgumentResolver);
        //将原本的添加后面
        resolvers.addAll(argumentResolvers);
        return  resolvers;
    }
}

2)使用

        在controller

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.oak.mybatisplus.annotation.EncryptParam;
import com.oak.mybatisplus.system.entity.Student;
import com.oak.mybatisplus.system.service.IStudentService;
import com.oak.mybatisplus.utils.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 */
@RestController
@RequestMapping("/student")
public class StudentController {


    @Autowired
    private IStudentService studentService;

    /**
     * 列表
     * phone = 15200000001
     * @return
     */
    @GetMapping("/list")
    public ResponseResult list(@EncryptParam String phone,String id){
        LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Student::getStudentId,id);
        // 分页查询  应该把phone参数也加密
        queryWrapper.eq(Student::getPhone,phone);
        IPage<Student> page = new Page<>(1,5);
        IPage<Student> list = studentService.page(page,queryWrapper);
        return ResponseResult.success(list);
    }

    /**
     * service.queryPage
     * @param student
     * @return
     */
    @PostMapping("/pageList")
    public ResponseResult page(@EncryptParam @RequestBody Student student){
        IPage list = studentService.queryPage(student);
        return ResponseResult.success(list);
    }

    /**
     * this.baseMapper.selectPage
     * @param student
     * @return
     */
    @PostMapping("/daoQueryPage")
    public ResponseResult daoQueryPage(@EncryptParam Student student){
        IPage list = studentService.daoQueryPage(student);
        return ResponseResult.success(list);
    }


    /**
     * 新增
     * 测试数据:{         "studentId": "999999",         "classNo": "20210101",         "cardNo": "340421000000000001",         "deleted": 0,         "birthday": "2021-10-01T20:05:56.000+00:00",         "sex": true,         "phone": "15200000001",         "email": "2021000001@qq.com",         "sname": "顾冷",         "sno": "000001"       }
     * @param student
     * @return
     */
    @PostMapping("/insert")
    public ResponseResult insert(@RequestBody Student student){
        boolean bool = studentService.saveOrUpdate(student);
        return ResponseResult.success(bool);
    }

}

三、总结

        到此数据库字段加密的用法就结束了,感兴趣的小伙伴可以研究一下mybatisplus自带的分页拦截器如何实现参数加密吧。

        本章节代码地址:https://gitee.com/thin-rain/my-springboot.git

        切换到origin/mybatis-plus分支即可看到

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值