MyBatis 配置 typeHandler 敏感字段加解密操作
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>
浙公网安备 33010602011771号