Java注解+com.fasterxml.jackson信息脱敏
前言
什么是数据脱敏
数据脱敏,指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。这样就可以在开发、测试和其它非生产环境以及外包环境中安全地使用脱敏后的真实数据集。
应用场景
我们在开发程序过程中,需要对一些敏感信息比如证件号码、手机号码、地址、银行卡号等进行脱敏处理后进行显示,来保证数据的安全性,同时保证了数据库安全。
脱敏实现
数据脱敏类型枚举
package com.kcwx.common.utils.desensitize;/*** 数据脱敏类型* */
public enum DesensitizeType {/**手机号码*/PHONE,/**身份证号码*/ID_CARD,/**地址*/ADDRESS,/**邮箱*/EMAIL,/**银行卡*/BANK_CARD,/**社会信用代码*/CREDITCODE,/**组织机构代码*/ORGCODE;
}
数据脱敏规则
package com.kcwx.common.utils;import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;/*** 脱敏验证* */
public class DesensitizeValidator {/***省份地址验证* */private static final List<String> VALID_REGIONS = Arrays.asList("11", "12", "13", "14", "15", "21", "22", "23","31", "32", "33", "34", "35", "36", "37", "41", "42", "43", "44", "45", "46", "50", "51", "52","53", "54", "61", "62", "63", "64", "65", "71","81", "82", "91");/*** 三大运营商最新号段正则(2025年更新)* */private static final String PHONE_REGEX = "^1(3[0-9]|4[5-9]|5[0-9]|6[2567]|7[0-8]|8[0-9]|9[0-9])\\d{8}$";/*** 座机号正则(支持带区号和不带区号格式)* */private static final String LANDLINE_REGEX = "^(0\\d{2,3}-?)?[1-9]\\d{6,7}$";/*** 18位统一社会信用代码正则(字母/数字组合)* */private static final String CREDIT_CODE_REGEX = "^([0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}|[1-9]\\d{14})$";/*** 验证18位身份证号* */public static boolean validate18(String idCard){// 1. 基础格式校验(长度+数字校验)if (idCard == null || idCard.length() != 18) {return false;}// 处理X校验位(不区分大小写)String normalized = idCard.replace('x', 'X');// 校验前17位是否为数字if (!Pattern.matches("^\\d{17}", normalized.substring(0, 17))) { return false;}// 2. 行政区划代码校验(2025年最新版)String regionCode = normalized.substring(0, 2);if (!VALID_REGIONS.contains(regionCode)) {return false;}// 3. 出生日期校验(支持到2099年)String birthStr = normalized.substring(6, 14);try {int year = Integer.parseInt(birthStr.substring(0, 4));int month = Integer.parseInt(birthStr.substring(4, 6));int day = Integer.parseInt(birthStr.substring(6, 8));if (year < 1900 || year > 2099){// 未来身份证需考虑扩展return false;}//简单日期校验(实际项目建议使用java.time.LocalDate)if (month < 1 || month > 12 || day < 1 || day > 31) { return false;}} catch (NumberFormatException e) {return false;}// 4. 校验码计算int[] weights = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};char[] checkCode = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};int sum = 0;for (int i = 0; i < 17; i++) {sum += weights[i] * Character.getNumericValue(normalized.charAt(i));}int mod = sum % 11;char expected = checkCode[mod];char actual = normalized.charAt(17);return expected == actual;}/*** 验证15位身份证号* */public static boolean isValid15(String idCard){// 1. 基础格式验证if (idCard == null || idCard.length() != 15) {return false;}try{long n = Long.parseLong(idCard);if (n < Math.pow(10, 14)) {return false;}} catch (NumberFormatException e) {return false;}// 2. 行政区划验证String regionCode = idCard.substring(0, 2);if (!VALID_REGIONS.contains(regionCode)) {return false;}// 3. 日期验证(兼容历史数据)String birthStr = "19" +idCard.substring(6, 12);try{SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");sdf.setLenient(false); // 严格模式sdf.parse(birthStr);} catch (Exception e) {return false;}return true;}/*** 获取脱敏后的身份证号* */public static String getDsIDCard(String idCard){if(validate18(idCard)||isValid15(idCard)) {if (idCard.length() == 18) {// 18位身份证脱敏正则(保留前6位和后4位)return idCard.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2");} else {// 15位身份证脱敏正则(保留前6位和后3位)return idCard.replaceAll("(\\d{6})\\d{6}(\\w{3})", "$1********$2");}}return idCard;}/*** 电话号码验证* */public static boolean validPhone(String phone){return (phone.length()==11&&phone.matches(PHONE_REGEX));}/*** 座机电话号码验证* */public static boolean validFixPhone(String phone){return phone.matches(LANDLINE_REGEX);}/*** 获取脱敏后的电话号码* */public static String getDsPhone(String phone){if(validFixPhone( phone)) {//座机号脱敏(保留前3位和后2位)if (phone.contains("-")) {// 处理带区号的情况String[] parts = phone.split("-");return parts[0] + "-" + parts[1].replaceAll("(\\d{3})\\d{2}(\\d{2})", "$1**$2");} else {return phone.replaceAll("(\\d{3})\\d{2}(\\d{2})", "$1****$2");}}else if(validPhone(phone)){//手机号码脱敏,保留前三位和后四位return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}return phone;}/*** 统一社会信用代码验证* */public static boolean validCreditCode(String creditCode){return creditCode.matches(CREDIT_CODE_REGEX);}/*** 获取脱敏后的统一社会信用代码* */public static String getDsCreditCode(String creditCode){if(validCreditCode(creditCode)){//统一社会信用代码脱敏(保留前6位和后4位)return creditCode.replaceAll("(?<=^.{6}).*(?=.{4}$)", "********");}return creditCode;}/*** 获取脱敏后的组织机构代码* */public static String getDsOrgCode(String orgCode){if(StringUtils.isNotEmpty(orgCode)&&orgCode.length()>=10) {//组织机构代码脱敏(保留前4位和后6位)return orgCode.replaceAll("(?<=^.{4}).*(?=.{6}$)", "******");}return orgCode;}
}
脱敏注解
package com.kcwx.common.utils.desensitize;import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 字段脱敏注解* */
@Target(ElementType.FIELD) // 仅适用于字段
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizeSerializer.class)
public @interface Desensitize {/**脱敏类型,默认为手机号*/DesensitizeType desensitizeType() default DesensitizeType.PHONE;
}
json序列化字段脱敏
package com.kcwx.common.utils.desensitize;import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.BeanProperty;
import com.kcwx.common.utils.IDCardValidator;
import com.kcwx.common.utils.StringUtils;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;import java.io.IOException;
import java.util.Objects;/*** json序列化字段脱敏* */
@NoArgsConstructor
@AllArgsConstructor
public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {private DesensitizeType desensitizeType;@Overridepublic void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {if (StringUtils.isNotEmpty(value)) {switch (desensitizeType){case PHONE:gen.writeString(DesensitizeValidator.getDsPhone(value));break;case ID_CARD:gen.writeString(DesensitizeValidator.getDsIDCard(value));break;case CREDITCODE:gen.writeString(DesensitizeValidator.getDsCreditCode(value));break;case ORGCODE:gen.writeString(DesensitizeValidator.getDsOrgCode(value));break;default:gen.writeString(value);}}else{gen.writeString("");}}@Overridepublic JsonSerializer<?> createContextual( SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {if (beanProperty != null) {if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {//获取Desensitize注解Desensitize sensitive = beanProperty.getAnnotation(Desensitize.class);if (sensitive != null) {this.desensitizeType = sensitive.desensitizeType();return this;}}//返回默认的序列化器return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);}//返回默认的序列化器return serializerProvider.findNullValueSerializer(null);}
}
实例
在需要脱敏的字段使用@Desensitize
import com.kcwx.common.utils.desensitize.Desensitize;
import com.kcwx.common.utils.desensitize.DesensitizeType;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;@Data
public class DsOpdInfoAll {/**机构名称*/@ApiModelProperty("机构名称")private String deptName;/**社会信用代码*/@ApiModelProperty("社会信用代码")@Desensitize(desensitizeType = DesensitizeType.CREDITCODE)private String creditCode;/**机构编码*/@ApiModelProperty("机构编码")@Desensitize(desensitizeType = DesensitizeType.ORGCODE)private String orgCode;
}
接口返回效果