代码地址:https://github.com/Gronckler/WorkProject.git
这里引用一个项目中的实际案例:用户敏感数据加密后入库,查询出来后自动解密,即数据在库中是以密文方式存在。项目工程仍然沿用前期工程(手动管理数据源方式)。
思路:
加密入库:自定义注解标注实体类有敏感信息的字段,通过拦截器反射获取到敏感字段后进行加密处理并添加一个特殊前缀,再入库。
查询解密:遍历查询结果字段,对内容中有上述前缀的内容做解密处理后再返回。
本方法的局限性:由于这里使用了注解,因为本方法仅适用于使用实体类封装参数的数据库操作,对于使用Map类型的封装参数的数据操作无效。
一、自定义标注敏感字段注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.FIELD})
@Documented
public @interface EncryptField {
String name() default "";
}
实体类敏感字段标注自定义注解
import com.example.architecture.annotation.EncryptField;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Data
@RequiredArgsConstructor
//@NoArgsConstructor
public class User {
private String id;
private String name;
@EncryptField
private String password;
private String remark;
}
二、编写解密拦截器、解密拦截器
加密拦截器
import com.example.architecture.annotation.EncryptField;
import com.example.architecture.tool.AESEncryptUtil;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Field;
import java.util.Properties;
/**
* 1、此方法用于拦截MappedStatement对象,对敏感数据做加密处理后重新拼接sql入库。
* 2、敏感信息如身份证号码、手机号、姓名、银行卡号等,需要在对应的pojo字段上加@Encrypt标注。
* 3、这里采用的AES对称加密方式。
*/
//注意这里导入的包选择appache.ibatis的包
@Intercepts({
@Signature(type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MybatisEncryptInterceptor implements Interceptor {
private String key = "12345678901234567";
private String prefix = "@cipher@";
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
//注解中method的值
String methodName = invocation.getMethod().getName();
//sql类型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if ("update".equals(methodName)) {
Object object = invocation.getArgs()[1];
// Date currentDate = new Date(System.currentTimeMillis());
//对有要求的字段填值
if (SqlCommandType.INSERT.equals(sqlCommandType)) {
Field[] fields = object.getClass().getDeclaredFields();
for (Field f : fields) {
// if(f.isAnnotationPresent(EncryptField.class) && f.getGenericType().toString().equals("String")){
if (f.isAnnotationPresent(EncryptField.class) && f.getGenericType().equals(String.class)) {
f.setAccessible(true);
String oldValue = (String) f.get(object);
//encrypt
// byte[] encrypt = AESUtil.encrypt(oldValue, key);
// String base64 = Base64.getEncoder().encodeToString(encrypt);
String base64 = AESEncryptUtil.aesEncrypt(oldValue, key);
String newValue = prefix + base64;
f.set(object, newValue);
}
}
} else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
// Field fieldModifyTime = object.getClass().getDeclaredField("modifyTime");
// fieldModifyTime.setAccessible(true);
// fieldModifyTime.set(object, currentDate);
// log.info("更新操作时设置modify_time:{}", currentDate);
}
} else if ("query".equals(methodName)) {
Object object = invocation.getArgs()[1];
//对有要求的字段填值
Field[] fields = object.getClass().getDeclaredFields();
for (Field f : fields) {
// if(f.isAnnotationPresent(EncryptField.class) && f.getGenericType().toString().equals("String")){
if (f.isAnnotationPresent(EncryptField.class) && f.getGenericType().equals(String.class)) {
f.setAccessible(true);
String oldValue = (String) f.get(object);
String base64 = AESEncryptUtil.aesEncrypt(oldValue, key);
String newValue = prefix + base64;
f.set(object, newValue);
}
}
}
return invocation.proceed();
}
解密拦截器
import com.example.architecture.tool.AESEncryptUtil;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.*;
/**
* 1、此方法用于拦截MappedStatement对象,对敏感数据做加密处理后重新拼接sql入库。
* 2、敏感信息如身份证号码、手机号、姓名、银行卡号等,需要在对应的pojo字段上加@Encrypt标注。
* 3、这里采用的AES对称加密方式。
*/
//注意这里导入的包选择appache.ibatis的包
@Intercepts({
// @Signature(type = StatementHandler.class, method = "prepare", args = {Collection.class, Object.class}),
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class MybatisDecryptInterceptor implements Interceptor {
private String key = "12345678901234567";
private String prefix = "@cipher@";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object proceed = invocation.proceed();
if (proceed instanceof ArrayList<?>) {
List<?> list = (ArrayList<?>) proceed;
for (Object value : list) {
if(value instanceof Map){//用Map接收sql返回值的情况
DecryptMapField(value);
}else{//用pojo接收sql返回值的情况
DecryptPojoField(value);
}
}
} else {
DecryptPojoField(proceed);
}
return proceed;
}
private void DecryptMapField(Object value) throws Exception {
Map map = (Map)value;
Set<String> set = map.keySet();
for(String k : set){
Object o = map.get(k);
if(o instanceof String &&((String)o).startsWith(prefix)){
String tmpValue = o.toString();
if(!StringUtils.isEmpty(tmpValue)){
String replace = tmpValue.replace(prefix, "");
String v = AESEncryptUtil.aesDecrypt(replace, key);
map.put(k,v);//对加密过的value解密后重新放回map
}
}
}
}
private void DecryptPojoField(Object value) throws Exception {
Field[] fields = value.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Object fieldValue = field.get(value);
if (fieldValue == null) {
continue;
}
String oldValue = fieldValue.toString();
if (oldValue.startsWith(prefix)) {
String newValue = oldValue.replace(prefix, "");
String s = AESEncryptUtil.aesDecrypt(newValue, key);
field.set(value,s);
}
}
}
三、mapper
注意这里的resultMap,因为我们是手动管理数据源的,因为需要我们自己手动映射pojo和table字段关系,否则会查询查询出来的实体属性都是null。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.architecture.dao.ITestDao">
<resultMap id="userMap" type="com.example.architecture.pojo.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="password" column="password"/>
<result property="remark" column="remark"/>
</resultMap>
<select id="count" resultType="int">
select count(1) from user
</select>
<insert id="insertUser" parameterType="com.example.architecture.pojo.User">
insert into user(name,password,remark)
values (#{name},
#{password},
#{remark,typeHandler=com.example.architecture.handler.ConverHandler}
)
</insert>
<select id="queryUser" parameterType="com.example.architecture.pojo.User" resultMap="userMap">
select * from user where 1=1
<if test="id !=null and id !=''">and id = #{id}</if>
<if test="name !=null and name !=''">and name = #{name}</if>
<if test="password !=null and password != ''">and password = #{password}</if>
</select>
</mapper>
四、注册加密、解密拦截器(由于我们这里使用手动管理数据源,因为需要手动注册拦截器,如果是spring自动管理就不需要了。)
@Bean("sqlSessionFactory")
SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
ResourcePatternResolver reslover = new PathMatchingResourcePatternResolver();
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeHandlers(new TypeHandler[]{new ConverHandler()});//注册自定义的handler
//注册mybatis的自定义interceptors
factoryBean.setPlugins(new Interceptor[]{new MybatisEncryptInterceptor(),new MybatisDecryptInterceptor()});
//手动管理数据源时,需要指定mapper路径
factoryBean.setMapperLocations(reslover.getResources("classpath:com/example/architecture/mapper/*.xml"));
SqlSessionFactory factory = factoryBean.getObject();
// factory.getConfiguration().setLazyLoadingEnabled(true);
// factory.getConfiguration().setAggressiveLazyLoading(false);
// factory.getConfiguration().setProxyFactory(new CglibProxyFactory());
return factory;
}
四、测试
插入数据
查看数据库,发现密码已经是密文方式存储了。
执行查询方法,查看查询数据是否自动解密
小结:
这里一定要注意的是,我们是手动管理数据源的,所以要自己映射实体类和表字段,即上面的resultMap,否则会出现实体类属性为null的情况。
代码地址:https://github.com/Gronckler/WorkProject.git