针对特定属性下的数据加密传输,例如手机号,身份证,地址信息等。
一. 为什么要数据加密
如果我们将用户的手机号码,身份号码以及地址等信息直接进行传递,很容易在传递数据包的时候被截获明文。
较好的结果就是将用户个人信息进行贩卖,较差的结果就是利用这些信息来进行社会工程学的诈骗,例如伪装成银行客服询问相关业务情况,由于能够准确的说出你的身份证以及地址信息,你就会放松警惕。
所以我们最好能够将这些敏感信息进行加密传输或者敏感信息脱敏。
二. 加密措施
2.1 敏感信息脱敏
可以参考正则表达式或者其他方式进行脱敏,目的就是为了隐藏关键的字母或者数字,不能够被其他人得知完整数据。
-
身份证脱敏
/** * 身份证脱敏 * @param idNo 原始身份证号信息 * @return 脱敏后信息 */ public String maskIdNo(String idNo){ // 结果 202210******141024 return idNo.replaceAll("(\\w{6})\\w*(\\w{6})", "$1******$2"); }
-
手机号脱敏
/** * 手机号脱敏 * @param phone 原始手机号信息 * @return 脱敏后信息 */ public String maskIdNo(String phone){ // 结果 136****1024 return phone.replaceAll("(\\w{3})\\w*(\\w{4})", "$1****$2"); }
脱敏信息可以有效帮助较少安全隐患,但是也有明显的缺点,即数据接收端也无法准确得知具体信息,比如有一个前端界面确实需要能够回显用户身份证信息,这个时候该种方法就没办法达到要求了。所以我们在这里需要使用对称加密,前后端约定同一种加密手段,并通过约定的手段进行加解密。
2.2 可逆对称加密
这里,我们选择使用AES进行举例,首先我们需要与前端约定好一个解密钥匙,例如使用以下的约定密钥前缀+日月年格式:
/**
* 加密前缀 (与前端约定)
*/
private static String secretKeyPrefix = "abcdefg";
/**
* AES加密
* @param content 明文
* @return 密文
* @throws Exception
*/
public static String aesEncrypt(String content) throws Exception {
// 构建密钥解密串
String encryptKey = getDayDecryptKey();
if (StringUtils.isEmpty(content) || StringUtils.isEmpty(encryptKey)) {
return null;
}
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
// 初始化密钥
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
// 使用构造的密钥进行最终加密
byte[] encryptStr = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptStr);
}
private static String getDayDecryptKey() {
// 使用约定的密钥前缀以及日月年格式,例如:abcdefg25102022
String dayDecryptKey = secretKeyPrefix + DateUtil.formatDate(LocalDateTime.now(), DateUtil.Format.DDMMYYYY.value());
return dayDecryptKey;
}
/**
* 加密前缀 (与前端约定)
*/
private static String secretKeyPrefix = "abcdefg";
/**
* AES解密
* @param encryptStr 密文
* @return 明文
* @throws Exception
*/
public static String aesDecrypt(String encryptStr) {
try {
String dayDecryptKey = getDayDecryptKey();
byte[] encryptByte = Base64.getDecoder().decode(encryptStr);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(dayDecryptKey.getBytes(), "AES"));
byte[] decryptBytes = cipher.doFinal(encryptByte);
return new String(decryptBytes);
} catch (Exception e) {
log.error("EncryptUtil aesDecrypt[dayDecryptKey] error.", e.getMessage());
}
}
private static String getDayDecryptKey() {
// 使用约定的密钥前缀以及日月年格式,例如:abcdefg25102022
String dayDecryptKey = secretKeyPrefix + DateUtil.formatDate(LocalDateTime.now(), DateUtil.Format.DDMMYYYY.value());
return dayDecryptKey;
}
这里我们模拟一个真实的数据加密场景,前端使用加密内容进行传输,后端进行解密后解析处理,定义一个待解密对象包含一个待解密字符串属性:
@Data
public class DecryptContent implements Serializable {
/**
* 待解密内容
*/
private String decryptContent;
}
/**
* 加密信息通信
* @param decryptContent 加密字符串
* @return
*/
@PostMapping("/test")
public void test(@RequestBody DecryptContent decryptContent) {
if (decryptContent == null) {
return;
}else{
String decryptMsg = validateDecryptContent(decryptContent.getDecryptContent());
log.info("解密成功,信息={}", decryptMsg);
}
}
/**
* 加密信息校验
* @param decryptContent 加密字符串
* @return
*/
public static String validateDecryptContent(String decryptContent) {
if (StringUtils.isBlank(decryptContent)) {
log.error("DecryptContentValidate validateDecryptContent param[decryptContent] is blank.");
return null;
}
String decryptStr = EncryptUtil.aesDecrypt(decryptContent);
return decryptStr;
}
2.3 mybatis-plus注解加密方法
如果我们使用了mybatis-plus,那么数据脱敏加解密就更方便了。
- 第一步,我们在DO类上的@TableName注解上除了标价value属性的表名之外,还需要加上autoResultMap的属性,并设置为true
- 第二步,我们在需要标记加密的字段上加上该注解@TableField(typeHandler = EncryptHandler.class),注解后面的Handler即表示需要加密数据的处理方式,观察源码可以发现这个类必须是实现了TypeHandler接口的实现类。如下示例,这里我们已经实现了一个EncryptHandler,继承了BaseTypeHandler,而BaseTypeHandler即是一个继承了TypeReference并实现了TypeHandler的抽象类:
public class EncryptHandler extends BaseTypeHandler {
public EncryptHandler() {
}
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, EncryptProxy.encrypt(o.toString()));
}
public Object getNullableResult(ResultSet resultSet, String s) throws SQLException {
String string = resultSet.getString(s);
return EncryptProxy.decrypt(string);
}
public Object getNullableResult(ResultSet resultSet, int i) throws SQLException {
String string = resultSet.getString(i);
return EncryptProxy.decrypt(string);
}
public Object getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String string = callableStatement.getString(i);
return EncryptProxy.decrypt(string);
}
}
实体类示例:
@TableName(value = "table",autoResultMap = true)
public class Table {
/** 系统密码 */
@TableField(typeHandler = EncryptHandler.class)
private String password;
}