
MyBatis 配置 typeHandler 敏感字段加解密操作
在sqlmap中加解密的逻辑:根据字段值的前缀来区分是做加密还是解密操作: 1. 加密时字段只过滤 `null` 值,明文不做任何处理直接加密 2. 解密时会判断字段是否是加密数据,如果是才会解密否则直接返回原始数据 3. fail fast 模式,当加/解密失败时,立即抛出异常
1.MyBatis JavaType 别名
package com.test.insurdock.encrypt;
import org.apache.ibatis.type.Alias;
@Alias("encrypt")
public class MyEncrypt {
}
package com.test.insurdock.encrypt;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 拦截 JavaType 为 #{@link Encrypt} 的 SQL
* 注意:[关键]
* 1. 加密时字段只过滤 `null` 值,明文不做任何处理直接加密
* 2. 解密时会判断字段是否是加密数据,如果是才会解密否则直接返回原始数据
* 3. fail fast 模式,当加/解密失败时,立即抛出异常
*
*/
@MappedTypes(MyEncrypt.class)
public class MyEncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
// 只要 parameter 非空都进行加密
ps.setString(i, MyEncryptUtil.encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String r = rs.getString(columnName);
// 兼容待修复的数据
return r == null ? null : (MyEncryptUtil.isEncrypted(r) ? MyEncryptUtil.decrypt(r) : r);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String r = rs.getString(columnIndex);
// 兼容待修复的数据
return r == null ? null : (MyEncryptUtil.isEncrypted(r) ? MyEncryptUtil.decrypt(r) : r);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String r = cs.getString(columnIndex);
// 兼容待修复的数据
return r == null ? null : (MyEncryptUtil.isEncrypted(r) ? MyEncryptUtil.decrypt(r) : r);
}
}2.加密工具类
package com.test.insurdock.encrypt;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyEncryptUtil {
private static Logger log = LoggerFactory.getLogger(MyEncryptUtil.class);
private static final String ENC_PREFIX = "ENC$";
private MyEncryptUtil() {
throw new UnsupportedOperationException();
}
public static boolean isEncrypted(String content) {
return StringUtils.isNotEmpty(content) && content.startsWith(ENC_PREFIX);
}
/**
* 加密。
* @param content 明文
* @return 密文
*/
public static String encrypt(String content) {
if (StringUtils.isEmpty(content) || isEncrypted(content)) {
return content;
}
return ENC_PREFIX + MyAES.encrypt(content);
}
/**
* 解密。
* @param content 密文
* @return 明文
*/
public static String decrypt(String content) {
try {
if (isEncrypted(content)) {
return MyAES.decrypt(StringUtils.removeStart(content, ENC_PREFIX));
}
} catch (Exception e) {
log.warn("can't decrypt, turn to raw content : {}", content);
}
return content;
}
}3.加解密类
package com.test.insurdock.encrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class MyAES {
private static Logger logger = LoggerFactory.getLogger(MyAES.class);
private static final String AES_ALG = "AES";
//java.security.InvalidKeyException: Invalid AES key length: 10 bytes
//16位
private static final String AES_KEY = "test567890abcdef";
private static final String AES_CBC_PCK_ALG = "AES/ECB/PKCS5Padding";
public static final String CHARSET_UTF8 = "UTF-8";
public static String encrypt(String srcContent) {
try {
String aesStr = aesEncrypt(srcContent, AES_KEY, CHARSET_UTF8);
return aesStr;
} catch (Exception e) {
throw new SecurityException(e);
}
}
public static String decrypt(String aesContent) {
try {
return aesDecrypt(aesContent, AES_KEY, CHARSET_UTF8);
} catch (Exception e) {
throw new SecurityException(e);
}
}
private static String aesEncrypt(String content, String aesKey, String charset) throws Exception {
Cipher cipher = Cipher.getInstance(AES_CBC_PCK_ALG);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey.getBytes(), AES_ALG));
byte[] encryptBytes = cipher.doFinal(content.getBytes(charset));
return new String(Base64.getEncoder().encode(encryptBytes));
}
private static String aesDecrypt(String content, String key, String charset) throws Exception {
Cipher cipher = Cipher.getInstance(AES_CBC_PCK_ALG);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(), AES_ALG));
byte[] cleanBytes = cipher.doFinal(Base64.getDecoder().decode(content));
return new String(cleanBytes, charset);
}
}4.sqlmap使用
#查询返回方法,将密文字段解密出来,查询返回给明文的字段。
<resultMap type="com.test.insurdock.model.scooterorder.SysCouponEntity" id="BaseResultMap">
<result property="renterPlaceMobile" column="enc_renter_place_mobile" typeHandler="com.test.insurdock.encrypt.MyEncryptTypeHandler"/>
<result property="renterPlaceName" column="enc_renter_place_name" typeHandler="com.test.insurdock.encrypt.MyEncryptTypeHandler"/>
<result property="renterPlaceCardNo" column="enc_renter_place_card_no" typeHandler="com.test.insurdock.encrypt.MyEncryptTypeHandler"/>
</resultMap>
#添加 insert方法中,将明文中的字段加密存储到密文数据库字段中
<insert id="insert" parameterType="com.test.insurdock.model.scooterorder.SysCouponEntity" useGeneratedKeys="true" keyProperty="id">
insert into sys_coupon
(
`renter_place_mobile`,
`renter_place_name`,
`renter_place_card_no`,
`enc_renter_place_mobile`,
`enc_renter_place_name`,
`enc_renter_place_card_no`
)
values
(
#{renterPlaceMobile},
#{renterPlaceName},
#{renterPlaceCardNo},
#{renterPlaceMobile,typeHandler=com.test.insurdock.encrypt.MyEncryptTypeHandler},
#{renterPlaceName,typeHandler=com.test.insurdock.encrypt.MyEncryptTypeHandler},
#{renterPlaceCardNo,typeHandler=com.test.insurdock.encrypt.MyEncryptTypeHandler}
)
</insert>