概要
使用mybatis的BaseTypeHandler来给敏感字段进行AES加密
整体架构流程
开发中,公司会要求对敏感字段进行加密处理后入库,查询的时候需要解密后展示,这就要求原字符串和加密字符串之间进行转换
如果一两个字符串还好,可以直接在入库的时候和查询的时候处理一下,但是如果有很多字段都需要这样处理怎么办?
Mybatis里面有一个TypeHandler可以解决这个问题,只需要在需要加密/解密的字段在xml上映射自定义的TypeHandler,查询返回必须是resultMap,入库就是在xml入库的字段上加上TypeHandler,
自己自定义加密工具,可以根据你的需求来定义,因为我项目中涉及的字段比较多,其中涉及一些复杂的sql,TypeHandler不能保证都能使用,所以我加解密的是一种十六进制的字符,跟MySQL原生的加解密函数HEX(AES_ENCRYPT(str,key_str))函数生成加密串就行,和工具AESUtil生成的加密串是一致的。mysql中解密AES字符串AES_DECRYPT(UNHEX(str),key_str)
技术细节
为了方便,这里我们使用的是Hutool的AES加密工具
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.8</version>
</dependency>
然后创建AesUtil加密工具, private static String keyStr = “自定义的key”; 这个key加密和解密要用同一个,不然会解密失败
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import org.apache.commons.lang3.StringUtils;
public class AESUtil {
private static final String AES_KEY = "AES_KEY";
private static final byte[] KEY_BYTES;
private static String keyStr = "自定义的key";
static {
KEY_BYTES = new byte[16];
int i = 0;
for (byte b : keyStr.getBytes()) {
KEY_BYTES[i++ % 16] ^= b;
}
}
public static String encrypt(String content) {
if (StringUtils.isBlank(content)) {
return content;
}
return HexUtil.encodeHexStr(SecureUtil.aes(KEY_BYTES).encrypt(content), false);
}
public static String decrypt(String content) {
if (StringUtils.isBlank(content)) {
return content;
}
return SecureUtil.aes(KEY_BYTES).decryptStr(content);
}
public static void main(String[] args) {
String encrypted = encrypt("测试");
System.out.println(encrypted);
System.out.println(decrypt(encrypted));
}
}
定义AesTypeHandler
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 继承Mybatis的 BaseTypeHandler类,重写方法
*/
public class TypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, AESUtil.encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return AESUtil.decrypt(rs.getString(columnName));
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return AESUtil.decrypt(rs.getString(columnIndex));
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return AESUtil.decrypt(cs.getString(columnIndex));
}
}
准备一个实体Test,我要加密phone和name字段
import lombok.Data;
@Data
public class Test {
private Integer id;
private String phone;
private String name;
}
对应的xml里的操作
<resultMap id="BaseResultMap" type="cn.com.fl.service.common.entity.Test">
<!--@mbg.generated-->
<!--@Table test-->
<id column="id" jdbcType="INTEGER" property="id" />
<result column="phone" jdbcType="VARCHAR" property="phone" typeHandler="cn.com.fl.service.common.AesTypeHandler.TypeHandler"/>
<result column="name" jdbcType="VARCHAR" property="name" typeHandler="cn.com.fl.service.common.AesTypeHandler.TypeHandler"/>
</resultMap>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
<!--@mbg.generated-->
<!--select
id,CAST(AES_DECRYPT(UNHEX(phone),'scyd')as char) as phone,name
from test
where id = #{id,jdbcType=INTEGER}-->
select
id,phone,`name`
from test
where id = #{id,jdbcType=INTEGER}
</select>
查询不用做任何操作,resultMap在加有TypeHandler的字段是自动映射进行解密操作
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="cn.com.fl.service.common.entity.Test" useGeneratedKeys="true">
INSERT into test (phone, `name`)
values (#{phone,jdbcType=VARCHAR,typeHandler=cn.com.fl.service.common.AesTypeHandler.TypeHandler}, #{name,jdbcType=VARCHAR,typeHandler=cn.com.fl.service.common.AesTypeHandler.TypeHandler})
</insert>
service层Dao层省略…
测试
测试代码
@ResponseBody
@PostMapping("/test")
public Result add(){
Test test = new Test();
test.setPhone("15999999999");
test.setName("测试");
int insert = mapper.insert(test);
if (insert>0){
return new Result().success();
}
return new Result().error("插入失败");
}
可以看到控制台是打印出来了加密的效果了
数据库效果
然后我们来查询一下这条数据看看
@ResponseBody
@RequestMapping("/select")
public String select(){
Test test = mapper.selectByPrimaryKey(24);
return test.toString() ;
}
我们可以试试用mysql原生的函数AES_DECRYPT(UNHEX(str),key_str)能否解密
答案是可以解密的,测试成功!!!
小结
因为我项目中使用的是mybatis没有用mybatis plus 在xml动态映射的时候显的有点僵硬,如果是用mybatis plus的可以用注解的方式,只需要在需要加密/解密的字段上使用@TableField(typeHandler = AesTypeHandler.class),
包含该字段的实体上使用@TableName(autoResultMap = true)即可(PS:AesTypeHandler是自定义的TypeHandler),大家有兴趣的可以去试试
参考链接
https://www.cnblogs.com/java-spring/p/14676670.html