背景: 海外项目,考虑到数据合规的问题,项目中入库的数据进行加密,但正常的业务交集还是要用明文数据,所以查询出来的数据要求明文返给前端。
传统的做法: 在数据入库前进行数据加密,把数据查询出来前,再进行数据对应的加密,这是最简单的实现,但会有比较多的侵入式编程。使用 AOP 的话代码会比较复杂,也不算很通用。
最佳解决方案: 是用借助 mybatis 的 「TypeHandler」,仅需引入对应的依赖,少量的配置,少量的改动既能实现数据加密的方案。
实现步骤:
1、自定义 typeHandler 继承 org.apache.ibatis.type.BaseTypeHandler ,重写相关方法
/**
* @author lwc
*/
@Slf4j
@MappedTypes(Encrypt.class)
public class CryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
try {
String encryptStr = Base64Utils.encodeToString(s.getBytes(Charset.defaultCharset()));
preparedStatement.setString(i, encryptStr);
} catch (Exception e) {
log.error("数据加密失败", e);
// 如果加密失败使用,则保证入库原始数据
preparedStatement.setString(i, s);
}
}
@Override
public String getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
return decrypt(resultSet.getString(columnName));
}
@Override
public String getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
return decrypt(resultSet.getString(columnIndex));
}
@Override
public String getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
return decrypt(callableStatement.getString(columnIndex));
}
/**
* 解密相关的参数,如果解密失败
* @param param
* @return
*/
private String decrypt(String param) {
// 判断返回结果是否是密文,不是密文那不进行解密
if (!isEncrypt(param)) {
return param;
}
try {
byte[] bytes = Base64Utils.decodeFromString(param);
String result = new String(bytes, Charset.defaultCharset());
return result;
} catch (Exception e) {
log.error("数据加密失败", e);
return param;
}
}
/**
* 判断字符串是否是密文
* @param param
* @return
*/
private boolean isEncrypt(String param) {
// 使用正则判断,是否是Base64密文
String base64Pattern = "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$";
return Pattern.matches(base64Pattern, param);
}
}
说明:
加密转换在 setNonNullParameter 中执行,解密在 getNullableResult中执行。
2、 CryptTypeHandler 使用一个 MappedTypes 注解,包含一个 CryptType 类,这个类使用 mybatis
别名功能,可以极大简化 mapper sql 的编写。
@Alias("encrypt") public class Encrypt {}
3、注册 typeHandler , SpringBoot 项目配置方式
# 类型转换器包路径
mybatis.type-handlers-package=com.xx.xx.x
mybatis.type-aliases-package=com.xx.xx
4、mapper.xml 文件改造
//新增数据 ,只需在 #{} 指定typeHandler , 传入参数最后被加密, 配置typeHandler需要全路径,或者使用较简单的javaType属性,使用上面定义的 encrypt
<insert id="insertBankCard" keyProperty="id" useGeneratedKeys="true" parameterType="com.gwm.prism.bank">
INSERT INTO bank (card_number, phone, user_name, id_number)
VALUES
(#{cardNumber,javaType=encrypt},
#{phone,typeHandler=com.gwm.prism.handler.CryptTypeHandler},
#{userName,typeHandler=com.gwm.prism.handler.CryptTypeHandler},
#{idNumber,javaType=encrypt})
</insert>
//查询在resultMap属性,指定 typeHandler
<resultMap id="bankResultMap" type="org.demo.pojo.BankCardDO">
<result property="cardNumber" column="card_number" typeHandler="com.gwm.prism.handler.CryptTypeHandler"/>
<result property="userName" column="user_name" typeHandler="com.gwm.prism.handler.CryptTypeHandler"/>
<result property="idNumber" column="id_number" typeHandler="com.gwm.prism.handler.CryptTypeHandler"/>
<result property="phone" column="phone" typeHandler="com.gwm.prism.handler.CryptTypeHandler"/>
</resultMap>
<select id="queryBank" resultMap="bankResultMap">
select * from bank;
</select>
常用加密算法的类型:
1、 非对称加密
- 密钥:加解密钥相同
- 缺点:无法确保密钥被安全传递
- 常用算法:DES、3DES(TripleDES)、AES等
2、对称加密
- 密钥:公私钥加密对,公钥加密,私钥解密
- 公钥由私钥生成,私钥可以推导出公钥,但是从公钥无法推出私钥
- 优点:解决了密钥传输中的安全问题
- 常用算法:RSA、ECC(椭圆曲线加密算法)
- 使用场景: SSH安全验证等
- 缺点:解决了信息传送的问题,但是又引入了新问题,即无法验证发送方是正确的,就是说,可能被伪造成发送方
3、哈希加密
- 将一段数据(任意长度)经过计算装换成一段定长的数据
不可逆性:几乎无法通过哈希的结果推导出原文 - 无碰撞性:两个不同原文哈希后的结果一定不同
- 常用算法:MD5,SHA256
- 使用场景:
数据库中的用户密码存储(MD5)
挖矿计算(SHA256)
4、数字签名加密
- 密钥:私钥签名,公钥验证签名
- 使用场景: 比特币交易验证等
Java 中常用的加密类型参考:https://www.jianshu.com/p/c83810a3479f