数据脱敏与加解密
概述
敏感数据(手机号、身份证、银行卡号等)在存储和传输中必须保护。常见方案:
| 方案 | 说明 | 适用场景 |
|---|---|---|
| 脱敏 | 展示时隐藏部分字符 | 日志、前端展示 |
| 加密存储 | 数据库存储加密值,查询时解密 | 手机号、身份证 |
| 哈希存储 | 不可逆哈希 | 密码 |
| 字段级加密 | 对特定字段加密 | 银行卡号、社保号 |
一、Spring 加密工具
1.1 TextEncryptor — 文本加密
java
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.security.crypto.encrypt.Encryptors;
// 创建加密器(密码 + salt)
TextEncryptor encryptor = Encryptors.text("my-password", "hex-salt");
// 加密
String encrypted = encryptor.encrypt("13800138000");
// 输出: 2a3b4c5d6e7f...
// 解密
String decrypted = encryptor.decrypt(encrypted);
// 输出: 138001380001.2 BytesEncryptor — 字节加密
java
BytesEncryptor encryptor = Encryptors.standard("password", "salt");
byte[] encrypted = encryptor.encrypt("sensitive-data".getBytes(StandardCharsets.UTF_8));
byte[] decrypted = encryptor.decrypt(encrypted);1.3 QueryableTextEncryptor — 保持可搜索性
java
// 保持确定性加密(相同明文 → 相同密文),可用于精确查询
TextEncryptor deterministic = Encryptors.queryableText("password", "salt");注意: 确定性加密会泄露"相同值"的信息。对手机号等需要精确查询的字段使用,对更敏感的数据建议用非确定性加密并配合其他查询方案。
二、数据脱敏工具类
java
public class DataMaskingUtils {
// 手机号:138****8000
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
// 身份证:110101****1234
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 10) return idCard;
return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
}
// 邮箱:abc@xxx.com → a***@xxx.com
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) return email;
int atIndex = email.indexOf("@");
String prefix = email.substring(0, atIndex);
String domain = email.substring(atIndex);
if (prefix.length() <= 1) return prefix + "***" + domain;
return prefix.charAt(0) + "***" + domain;
}
// 银行卡号:6222****1234
public static String maskBankCard(String cardNo) {
if (cardNo == null || cardNo.length() < 8) return cardNo;
return cardNo.substring(0, 4) + "****" + cardNo.substring(cardNo.length() - 4);
}
// 姓名:张**(保留姓)
public static String maskName(String name) {
if (name == null || name.length() <= 1) return name;
return name.charAt(0) + "*".repeat(name.length() - 1);
}
}三、MyBatis TypeHandler 自动加解密
3.1 加密 TypeHandler
java
@MappedTypes(String.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class EncryptTypeHandler extends BaseTypeHandler<String> {
private static final TextEncryptor encryptor =
Encryptors.text("my-app-key", "a1b2c3d4e5f6");
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
String parameter, JdbcType jdbcType) throws SQLException {
// 写入时加密
ps.setString(i, encryptor.encrypt(parameter));
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
// 读取时解密
return value == null ? null : encryptor.decrypt(value);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return value == null ? null : encryptor.decrypt(value);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return value == null ? null : encryptor.decrypt(value);
}
}3.2 使用
java
// MyBatis XML 配置
// <result column="phone" property="phone" typeHandler="com.example.handler.EncryptTypeHandler"/>
// MyBatis-Plus 注解方式
@Data
@TableName("user")
public class User {
@TableId
private Long id;
@TableField(typeHandler = EncryptTypeHandler.class)
private String phone; // 数据库存储为加密密文
@TableField(typeHandler = EncryptTypeHandler.class)
private String idCard; // 数据库存储为加密密文
private String username; // 普通字段
}3.3 查询时加密匹配
java
// 需要精确查询时,手动加密查询条件
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User findByPhone(String phone) {
// 将明文手机号加密后查询
String encryptedPhone = encryptor.encrypt(phone);
return userMapper.findByEncryptedPhone(encryptedPhone);
}
}xml
<!-- Mapper XML -->
<select id="findByEncryptedPhone" resultType="User">
SELECT * FROM user WHERE phone = #{encryptedPhone}
</select>四、Jackson 序列化脱敏
4.1 自定义注解
java
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DataMaskingSerializer.class)
public @interface DataMask {
MaskType value() default MaskType.PHONE;
enum MaskType {
PHONE, ID_CARD, EMAIL, BANK_CARD, NAME
}
}4.2 自定义序列化器
java
public class DataMaskingSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
DataMask maskAnnotation = gen.getCurrentAnnotation(DataMask.class);
if (maskAnnotation == null || value == null) {
gen.writeString(value);
return;
}
switch (maskAnnotation.value()) {
case PHONE:
gen.writeString(DataMaskingUtils.maskPhone(value));
break;
case ID_CARD:
gen.writeString(DataMaskingUtils.maskIdCard(value));
break;
case EMAIL:
gen.writeString(DataMaskingUtils.maskEmail(value));
break;
case BANK_CARD:
gen.writeString(DataMaskingUtils.maskBankCard(value));
break;
case NAME:
gen.writeString(DataMaskingUtils.maskName(value));
break;
default:
gen.writeString(value);
}
}
}4.3 使用
java
@Data
public class UserVO {
private Long id;
private String username;
@DataMask(MaskType.PHONE)
private String phone; // 返回前端自动脱敏
@DataMask(MaskType.ID_CARD)
private String idCard; // 返回前端自动脱敏
@DataMask(MaskType.EMAIL)
private String email; // 返回前端自动脱敏
@DataMask(MaskType.NAME)
private String realName; // 返回前端自动脱敏
}五、日志脱敏
5.1 Logback 脱敏 Converter
java
public class MaskingConverter extends MessageConverter {
private static final Pattern PHONE_PATTERN =
Pattern.compile("(1[3-9]\\d)\\d{4}(\\d{4})");
private static final Pattern ID_CARD_PATTERN =
Pattern.compile("(\\d{6})\\d{8}(\\d{4})");
@Override
public String convert(ILoggingEvent event) {
String message = event.getFormattedMessage();
message = PHONE_PATTERN.matcher(message).replaceAll("$1****$2");
message = ID_CARD_PATTERN.matcher(message).replaceAll("$1********$2");
return message;
}
}5.2 logback.xml 配置
xml
<configuration>
<conversionRule conversionWord="maskedMsg"
converterClass="com.example.logging.MaskingConverter"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %maskedMsg%n</pattern>
</encoder>
</appender>
</configuration>5.3 AOP 日志脱敏
java
@Aspect
@Component
public class LogMaskingAspect {
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)" +
" || @annotation(org.springframework.web.bind.annotation.PostMapping)" +
" || @annotation(org.springframework.web.bind.annotation.GetMapping)")
public Object maskLogArgs(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
// 对参数进行脱敏
Object[] maskedArgs = Arrays.stream(args)
.map(this::maskObject)
.toArray();
log.info("请求参数: {}", maskedArgs);
return pjp.proceed();
}
private Object maskObject(Object obj) {
if (obj instanceof String) {
String str = (String) obj;
// 替换日志中的敏感信息
str = str.replaceAll("phone=[^&,}]+", "phone=***");
str = str.replaceAll("password=[^&,}]+", "password=***");
str = str.replaceAll("idCard=[^&,}]+", "idCard=***");
return str;
}
return obj;
}
}六、敏感信息加密存储实战
6.1 实体设计
java
@Entity
@Table(name = "user_sensitive")
public class UserSensitive {
@Id
private Long id;
// 无需加密:用户名
private String username;
// 自动加解密:手机号
@Column(name = "phone")
private String phone;
// 手动管理加密:身份证
@Column(name = "id_card_encrypted")
private String idCardEncrypted;
// 哈希存储:查询用(手机号后 6 位哈希)
@Column(name = "phone_suffix_hash")
private String phoneSuffixHash;
// 脱敏展示用
@Transient
private String phoneMasked;
}6.2 手动管理加密
java
@Service
public class SensitiveDataService {
@Autowired
private UserSensitiveRepository repository;
private final TextEncryptor encryptor =
Encryptors.text("app-key", "fixed-salt");
// 保存时加密
public void saveUser(UserSensitive user) {
// 加密身份证
user.setIdCardEncrypted(encryptor.encrypt(user.getIdCardPlain()));
// 生成查询哈希
String phone = user.getPhone();
user.setPhoneSuffixHash(hashPhoneSuffix(phone));
// 脱敏展示值
user.setPhoneMasked(DataMaskingUtils.maskPhone(phone));
repository.save(user);
}
// 查询时解密
public String getIdCard(Long userId) {
UserSensitive user = repository.findById(userId).orElseThrow();
return encryptor.decrypt(user.getIdCardEncrypted());
}
// 通过手机号后 6 位查询
public List<UserSensitive> findByPhoneSuffix(String suffix) {
String hash = hashPhoneSuffix(suffix);
return repository.findByPhoneSuffixHash(hash);
}
private String hashPhoneSuffix(String phone) {
String suffix = phone.substring(phone.length() - 6);
return DigestUtils.sha256Hex(suffix);
}
}七、总结
| 知识点 | 说明 |
|---|---|
TextEncryptor | 文本加密,适合字段级加密 |
BytesEncryptor | 字节加密,适合二进制数据 |
queryableTextEncryptor | 确定性加密,相同明文→相同密文 |
| MyBatis TypeHandler | 自动加解密,对业务代码透明 |
| Jackson 序列化脱敏 | 返回前端时自动脱敏 |
| 日志脱敏 | 替换日志中的敏感信息 |
参考链接: