数据库敏感字段加密
前言
提示:在我们进行开发的过程中,可能会遇到银行账号、用户身份证、手机号等铭感字段,用户会要求将这些数据库中的字段加密,以提高数据的安全性
本文章讲解如何实现数据库敏感字段加密,及加密后加密字段如何进行精确条件查询
一、需求
目前有个用户表,需要对用户表的手机号、身份证字段加密,页面展示数据时解密。
目标:
【新增数据】接口请求参数:
数据库结果(手机号、身份证已经加密):
【查询返给前端】结果:
二、如何实现
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分支即可看到