描述
脱敏模块用于在数据序列化输出到前端时,对敏感信息(如手机号、身份证号、银行卡号、邮箱等)进行自动脱敏处理,防止敏感数据泄露。
核心特性:
* 基于 Jackson 序列化实现,同时兼容 Spring MVC(Servlet)和 Spring WebFlux(Reactive)两种 Web 技术栈。
* 提供 @RegexSensitive、@SlideSensitive、@CustomSensitive 三种脱敏注解,覆盖正则替换、滑动窗口、自定义处理器等场景。
* 内置常用的脱敏策略(手机号、身份证号、银行卡号、邮箱等),开箱即用。
* 支持自定义脱敏处理器,可注入 Spring Bean,实现数据库查询、远程调用等复杂脱敏逻辑。
* 支持多注解叠加,按 order 顺序链式执行。
快速开始
引入依赖后,只需在需要脱敏的字段上添加对应注解即可自动生效,无需额外配置:
@Data
public class UserVo {
@SlideSensitive(type = SlideSensitiveType.PHONE_NUMBER)
private String phone;
@SlideSensitive(type = SlideSensitiveType.ID_CARD_NO)
private String idCardNo;
@RegexSensitive(type = RegexSensitiveType.EMAIL)
private String email;
}
控制器返回该对象时,Jackson 序列化会自动拦截并执行脱敏逻辑,对业务代码完全透明。
脱敏注解
框架提供了三种脱敏注解,分别对应不同的脱敏策略:
@RegexSensitive
基于正则表达式的脱敏,通过匹配和替换规则实现。
属性 |
默认值 |
说明 |
type |
无(必填) |
脱敏类型,见 RegexSensitiveType 枚举 |
regex |
"^[\s\S]*$" |
匹配的正则表达式,仅 type=CUSTOM 时生效 |
replacement |
"" |
替换规则,仅 type=CUSTOM 时生效 |
order |
0 |
多注解叠加时的执行顺序,值越小越先执行 |
RegexSensitiveType 枚举:
枚举值 |
正则 |
替换规则 |
说明 |
CUSTOM |
^[\s\S]*$ |
自定义类型 |
|
(^.)(@.$) |
$1**$2 |
邮箱脱敏,如 1**@qq.com |
|
ENCRYPTED_PASSWORD |
(.{3}).*(.{2}$) |
$1**$2 |
对称密文密码脱敏 |
// 邮箱脱敏
@RegexSensitive(type = RegexSensitiveType.EMAIL)
private String email;
// 自定义正则脱敏
@RegexSensitive(type = RegexSensitiveType.CUSTOM, regex = "(.{2}).(.{2}$)", replacement = "$1***$2")
private String customField;
@SlideSensitive
基于滑动窗口的脱敏,保留左右明文,中间用掩码字符替换。
属性 |
默认值 |
说明 |
type |
无(必填) |
脱敏类型,见 SlideSensitiveType 枚举 |
leftPlainTextLen |
0 |
左边保留的明文数,仅 type=CUSTOM 时生效 |
rightPlainTextLen |
0 |
右边保留的明文数,仅 type=CUSTOM 时生效 |
maskString |
"*" |
掩码字符,仅 type=CUSTOM 时生效 |
order |
0 |
多注解叠加时的执行顺序,值越小越先执行 |
SlideSensitiveType 枚举:
枚举值 |
左明文 |
右明文 |
掩码 |
说明 |
CUSTOM |
0 |
0 |
* |
自定义类型 |
ALL_ASTERISK |
0 |
0 |
* |
全部替换为 * |
BANK_CARD_NO |
6 |
4 |
* |
银行卡号,如 330150**1234 |
ID_CARD_NO |
6 |
4 |
* |
身份证号,如 655356*1234 |
PHONE_NUMBER |
3 |
2 |
* |
手机号,如 138**56 |
// 手机号脱敏
@SlideSensitive(type = SlideSensitiveType.PHONE_NUMBER)
private String phone;
// 银行卡号脱敏
@SlideSensitive(type = SlideSensitiveType.BANK_CARD_NO)
private String bankCardNo;
// 自定义滑动脱敏
@SlideSensitive(type = SlideSensitiveType.CUSTOM, leftPlainTextLen = 2, rightPlainTextLen = 2, maskString = "#")
private String customField;
@CustomSensitive
使用自定义脱敏处理器,实现 CustomDesensitizationHandler 接口。
属性 |
默认值 |
说明 |
handler |
无(必填) |
自定义脱敏处理器类 |
args |
{} |
传递给处理器的构造参数 |
ignoreEmpty |
true |
空字符串是否跳过脱敏,为 true 时空字符串原样输出不进入处理器 |
condition |
"" |
条件脱敏 SpEL 表达式(预留属性,暂未实现) |
order |
0 |
多注解叠加时的执行顺序,值越小越先执行 |
@CustomSensitive(handler = TestDesensitizationHandler.class)
private String field;
高级特性
多注解叠加
一个字段可以同时标注多个脱敏注解,框架会按 order 属性升序依次执行,前一个注解的脱敏结果作为后一个注解的输入:
// 先滑动脱敏保留前3后4,再正则脱敏
@SlideSensitive(type = SlideSensitiveType.CUSTOM, leftPlainTextLen = 3, rightPlainTextLen = 4, order = 1)
@RegexSensitive(type = RegexSensitiveType.CUSTOM, regex = "(\\d{4})\\d{4}(\\d{4})", replacement = "$1**$2", order = 2)
private String phone;
order 默认值为 0,未指定 order 属性的注解按声明顺序执行。建议在叠加场景下显式指定 order 以保证执行顺序明确。
|
空值跳过
@CustomSensitive 默认开启 ignoreEmpty = true,空字符串不会进入脱敏处理器,避免产生 这类无意义结果。如需对空字符串也进行脱敏,可关闭此选项:
@CustomSensitive(handler = TestDesensitizationHandler.class, ignoreEmpty = false)
private String field;
Spring Bean 注入
自定义脱敏处理器支持声明为 Spring Bean,可注入任意依赖(如数据库访问层、配置、远程服务)。框架会优先从 Spring 容器获取处理器实例,获取不到时才通过反射创建。
@Component
public class DbMaskHandler implements CustomDesensitizationHandler {
@Autowired
private MaskRuleMapper maskRuleMapper;
@Override
public String handle(String origin, String[] args) {
// 从数据库查询脱敏规则
MaskRule rule = maskRuleMapper.findByField(args[0]);
return origin.replaceAll(rule.getRegex(), rule.getReplacement());
}
}
然后直接在注解中引用该处理器类即可,无需手动注册:
@CustomSensitive(handler = DbMaskHandler.class, args = {"user_phone"})
private String phone;
处理器类必须实现 CustomDesensitizationHandler 接口。如果声明为 Spring Bean,框架会自动从容器获取;如果未声明为 Bean,框架会通过反射调用无参构造方法创建实例。
|
自定义复合注解
当项目中多个字段需要使用相同的脱敏策略组合时,可以基于 @CustomSensitive 创建自定义复合注解。核心思路是将 @CustomSensitive 作为元注解标注在自定义注解上,并实现一个 CustomDesensitizationHandler 封装复合脱敏逻辑。
典型的应用场景包括:
* 将 @SlideSensitive(type = SlideSensitiveType.PHONE_NUMBER) 封装为 @PhoneSensitive
* 将 @SlideSensitive(type = SlideSensitiveType.ID_CARD_NO) 封装为 @IdCardSensitive
* 将多个脱敏规则组合为一个复合注解,如同时进行滑动脱敏和正则脱敏
下面以封装手机号脱敏为例,展示完整的实现步骤:
1. 实现 CustomDesensitizationHandler
处理器内部复用内置的 SlideDesensitizationHandler,封装手机号脱敏逻辑:
@Component
public class PhoneSensitiveHandler implements CustomDesensitizationHandler {
@Override
public String handle(String origin, String[] args) {
SlideDesensitizationHandler handler = DesensitizationHandlerHolder
.getSlideDesensitizationHandler();
return handler.handle(origin, SlideSensitiveType.PHONE_NUMBER);
}
}
2. 定义复合注解
以 @CustomSensitive 作为元注解,指定处理器类:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CustomSensitive(handler = PhoneSensitiveHandler.class)
public @interface PhoneSensitive {
}
3. 使用复合注解
在实体类字段上直接使用自定义注解,与使用内置注解体验一致:
@Data
public class UserVo {
@PhoneSensitive
private String phone;
}
复合脱敏示例
以下示例展示如何创建一个组合了滑动脱敏和正则脱敏的复合注解,实现对手机号的"保留前3后4 + 中间四位掩码"双重脱敏:
@Component
public class CompositePhoneHandler implements CustomDesensitizationHandler {
@Override
public String handle(String origin, String[] args) {
SlideDesensitizationHandler slideHandler = DesensitizationHandlerHolder
.getSlideDesensitizationHandler();
RegexDesensitizationHandler regexHandler = DesensitizationHandlerHolder
.getRegexDesensitizationHandler();
// 第一步:滑动脱敏,保留前3后4
String step1 = slideHandler.handle(origin, 3, 4, "");
// 第二步:正则脱敏,将中间四位数字替换为 *
return regexHandler.handle(step1, "(\\d{4})\\d{4}(\\d{4})", "$1**$2");
}
}
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CustomSensitive(handler = CompositePhoneHandler.class)
public @interface CompositePhoneSensitive {
}
自定义复合注解的核心优势在于将脱敏规则集中管理。当规则需要调整时,只需修改 Handler 实现,无需逐个修改实体类中的注解配置。同时,语义化的注解名也让代码更具可读性。Handler 声明为 @Component 后,框架会自动从 Spring 容器获取实例,可注入任意依赖。
|
示例
下面展示一个完整的脱敏使用示例,包含脱敏对象定义、自定义处理器实现和控制器接口。
脱敏对象
package com.luamas.thinkbridge.integration.demo.sensitive.vo;
import com.luamas.thinkbridge.common.sensitive.annotation.CustomSensitive;
import com.luamas.thinkbridge.common.sensitive.annotation.RegexSensitive;
import com.luamas.thinkbridge.common.sensitive.annotation.SlideSensitive;
import com.luamas.thinkbridge.common.sensitive.enums.RegexSensitiveType;
import com.luamas.thinkbridge.common.sensitive.enums.SlideSensitiveType;
import com.luamas.thinkbridge.integration.demo.sensitive.handler.TestDesensitizationHandler;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
@Schema
public class UserVo {
/
* 用户名,不脱敏
/
@Schema(title = "用户名", example = "xiaoming")
private String username;
/
* 密码脱敏
*/
@Schema(title = "密码", example = "admina123456")
@RegexSensitive(type = RegexSensitiveType.ENCRYPTED_PASSWORD)
private String password;
/
* 邮件
*/
@Schema(title = "邮箱", example = "luamas@gmail.com")
@RegexSensitive(type = RegexSensitiveType.EMAIL)
private String email;
/
* 手机号
*/
@Schema(title = "手机号", example = "13800138000")
@SlideSensitive(type = SlideSensitiveType.PHONE_NUMBER)
private String phoneNumber;
/*
* 测试自定义脱敏
*/
@Schema(title = "自定义脱敏测试字段", example = "这是测试属性")
@CustomSensitive(handler = TestDesensitizationHandler.class)
private String testField;
}
自定义脱敏处理器
package com.luamas.thinkbridge.integration.demo.sensitive.handler;
import com.luamas.thinkbridge.common.sensitive.handler.CustomDesensitizationHandler;
public class TestDesensitizationHandler implements CustomDesensitizationHandler {
@Override
public String handle(String s, String... args) {
return "TEST-" + s;
}
}
MVC 控制器
package com.luamas.thinkbridge.integration.demo.sensitive.controller;
import com.luamas.thinkbridge.integration.demo.sensitive.vo.UserVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Profile("sensitive")
@Tag(name = "脱敏")
@RestController
@RequestMapping("sensitive")
public class SensitiveController {
@Operation(summary = "查看脱敏效果",
description = "返回包含多种脱敏类型的用户信息,演示正则脱敏、滑动脱敏和自定义脱敏")
@GetMapping
public UserVo sensitive(
@Parameter(description = "用户名", example = "xiaoming") String username,
@Parameter(description = "密码", example = "admina123456") String password,
@Parameter(description = "邮箱", example = "luamas@gmail.com") String email,
@Parameter(description = "手机号", example = "13800138000") String phoneNumber,
@Parameter(description = "自定义脱敏字段", example = "这是测试属性") String testField) {
return new UserVo()
.setUsername(username)
.setPassword(password)
.setEmail(email)
.setPhoneNumber(phoneNumber)
.setTestField(testField);
}
}