日常开发过程中需要对数据库中敏感字段加解密,如手机号、密码等数据,直接在业务代码中进行加解密有点冗余,本文采用自定义注解+mybatis拦截器实现;
主要步骤:
1:自定义注解;
2:实现mybatis的Interceptor接口;
3:加解密算法;
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:(官网链接https://mybatis.org/mybatis-3/zh/configuration.html#plugins)
// 执行
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
// 请求参数处理
ParameterHandler (getParameterObject, setParameters)
// 返回结果集处理
ResultSetHandler (handleResultSets, handleOutputParameters)
// SQL语句构建
StatementHandler (prepare, parameterize, batch, update, query)
话不多说直接上代码,整体项目结构如图所示:
自定义注解,该注解作用范围在类上面
package com.mybatis.interceptor.transaction;
import java.lang.annotation.*;
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
自定义加密注解,该注解作用范围在字段上,如密码等字段
package com.mybatis.interceptor.transaction;
import java.lang.annotation.*;
/**
* 加密注解
*/
@Documented
@Inherited
@Target({ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
自定义解密注解,该注解作用范围也在字段上
package com.mybatis.interceptor.transaction;
import java.lang.annotation.*;
/**
* 解密注解
*/
@Documented
@Inherited
@Target({ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptTransaction {
}
加密接口及其实现类
package com.mybatis.interceptor.handler;
import java.lang.reflect.Field;
public interface EncryptUtil {
/**
* 加密
* @param declaredFields
* @param paramsObject
* @param <T>
* @return
* @throws IllegalAccessException
*/
<T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException;
}
加密实现类
package com.mybatis.interceptor.handler;
import com.mybatis.interceptor.transaction.EncryptTransaction;
import com.mybatis.interceptor.util.AESUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Objects;
@Component
public class EncryptUtilImpl 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 {
field.set(paramsObject, aesUtil.encrypt(value));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return paramsObject;
}
}
解密接口及其实现类
package com.mybatis.interceptor.handler;
public interface DecryptUtil {
/**
* 解密
*
* @param result resultType的实例
* @return T
* @throws IllegalAccessException 字段不可访问异常
*/
<T> T decrypt(T result) throws IllegalAccessException;
}
解密实现类
package com.mybatis.interceptor.handler;
import com.mybatis.interceptor.transaction.DecryptTransaction;
import com.mybatis.interceptor.util.AESUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Objects;
@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;
}
}
具体加解密算法
package com.mybatis.interceptor.util;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Component
public class AESUtil {
private static final String defaultV = "6859505890402435";
private static final String key = "1061697007556132";
private static SecretKeySpec getKey(String strKey) throws Exception {
byte[] arrBTmp = strKey.getBytes();
byte[] arrB = new byte[16]; // 创建一个空的16位字节数组(默认值为0)
for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {
arrB[i] = arrBTmp[i];
}
SecretKeySpec skeySpec = new SecretKeySpec(arrB, "AES");
return skeySpec;
}
/**
* 加密
* @param content
* @return
* @throws Exception
*/
public static String encrypt(String content) throws Exception {
final Base64.Encoder encoder = Base64.getEncoder();
SecretKeySpec skeySpec = getKey(key);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(defaultV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(content.getBytes());
String encodedText = encoder.encodeToString(encrypted);
return encodedText;
}
/**
* 解密
* @param content
* @return
* @throws Exception
*/
public static String decrypt(String content) throws Exception {
final Base64.Decoder decoder = Base64.getDecoder();
SecretKeySpec skeySpec = getKey(key);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(defaultV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
byte[] base64 = decoder.decode(content);
byte[] original = cipher.doFinal(base64);
String originalString = new String(original);
return originalString;
}
}
入参拦截器
参考官网,通过 @Intercepts 和 @Signature 的联合使用,@Intercepts 注解开启拦截器,@Signature 注解定义拦截器的实际类型。指定 ParameterHandler.class 类型。
package com.mybatis.interceptor.interceptor;
import com.mybatis.interceptor.handler.EncryptUtil;
import com.mybatis.interceptor.transaction.SensitiveData;
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;
@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) {
}
}
结果拦截器
package com.mybatis.interceptor.interceptor;
import com.mybatis.interceptor.handler.DecryptUtil;
import com.mybatis.interceptor.transaction.DecryptTransaction;
import com.mybatis.interceptor.transaction.SensitiveData;
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.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;
@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) {
}
}
实体类编写,带上自定义注解
package com.mybatis.interceptor.entity;
import com.mybatis.interceptor.transaction.DecryptTransaction;
import com.mybatis.interceptor.transaction.EncryptTransaction;
import com.mybatis.interceptor.transaction.SensitiveData;
import java.io.Serializable;
@SensitiveData
public class User implements Serializable {
private static final long serialVersionUID = -1205226416664488559L;
private Long id;
private String city;
@DecryptTransaction
@EncryptTransaction
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", city='" + city + '\'' +
", name='" + name + '\'' +
'}';
}
}
mapper接口
package com.mybatis.interceptor.mapper;
import com.mybatis.interceptor.entity.User;
import java.util.List;
public interface UserMapper {
Long addUser(User user);
User findById(Long id);
List<User> list();
User findByName(String name);
}
service及其实现类
package com.mybatis.interceptor.service;
import com.mybatis.interceptor.entity.User;
import java.util.List;
public interface UserService {
List<User> list();
Long add(User user);
User findById(Long id);
User findByName(String name);
}
service实现类
package com.mybatis.interceptor.service;
import com.mybatis.interceptor.mapper.UserMapper;
import com.mybatis.interceptor.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public List<User> list() {
return userMapper.list();
}
@Override
public Long add(User user) {
return userMapper.addUser(user);
}
@Override
public User findById(Long id) {
return userMapper.findById(id);
}
@Override
public User findByName(String name) {
return userMapper.findByName(name);
}
}
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.interceptor.mapper.UserMapper">
<resultMap id="baseResultMap" type="com.mybatis.interceptor.entity.User">
<result column="id" property="id" jdbcType="INTEGER" />
<result column="city" property="city" jdbcType="VARCHAR" />
<result column="name" property="name" jdbcType="VARCHAR" />
</resultMap>
<insert id="addUser">
INSERT INTO user (
id,city, name
)
VALUES (
#{id,jdbcType=INTEGER},
#{city,jdbcType=VARCHAR},
#{name,jdbcType=VARCHAR}
)
</insert>
<select id="list" resultMap="baseResultMap">
SELECT u.* FROM user u
</select>
<select id="findById" resultMap="baseResultMap">
SELECT u.* FROM user u WHERE u.id=#{id,jdbcType=INTEGER}
</select>
<select id="findByName" resultMap="baseResultMap">
SELECT u.* FROM user u WHERE u.name=#{name,jdbcType=VARCHAR}
</select>
</mapper>
Spring 启动类
package com.mybatis.interceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication
@MapperScan("com.mybatis.interceptor.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
properties配置
server.port=9999
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
#MySql配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
测试类
package com.mybatis.interceptor;
import com.mybatis.interceptor.entity.User;
import com.mybatis.interceptor.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Test
public void contextLoads() {
}
@Autowired
private UserService userService;
@Test
public void addTest(){
User user = new User();
user.setCity("shanghai");
user.setName("nima");
user.setId(Long.valueOf(10));
userService.add(user);
}
@Test
public void findTest(){
System.out.println(userService.findById(Long.valueOf(10)));
}
}
加解密测试通过,如下图所示: