@@ -48,6 +48,7 @@
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
<hutool.version>5.8.26</hutool.version>
|
||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||
<dypnsapi.version>2.0.0</dypnsapi.version>
|
||||
</properties>
|
||||
|
||||
<!-- 统一依赖管理 -->
|
||||
@@ -168,6 +169,13 @@
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!--阿里云短信认证服务-->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dypnsapi20170525</artifactId>
|
||||
<version>${dypnsapi.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云短信认证服务 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dypnsapi20170525</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
+4
-1
@@ -15,7 +15,10 @@ public class RedisKeyConstants {
|
||||
* 验证码 KEY 前缀
|
||||
*/
|
||||
private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";
|
||||
|
||||
/**
|
||||
* 验证码 KEY 过期时间 (分钟)
|
||||
*/
|
||||
public static final long VERIFICATION_CODE_EXPIRE_TIME = 3;
|
||||
/**
|
||||
* 构建验证码 KEY
|
||||
*
|
||||
|
||||
@@ -2,8 +2,6 @@ package top.crushtj.xiaoyishu.auth.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import java.lang.String;
|
||||
|
||||
import top.crushtj.framework.common.exception.BaseExceptionInterface;
|
||||
|
||||
/**
|
||||
@@ -18,9 +16,10 @@ import top.crushtj.framework.common.exception.BaseExceptionInterface;
|
||||
@AllArgsConstructor
|
||||
public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
// -------- 通用异常状态码 --------
|
||||
SYSTEM_ERROR("AUTH-10000", "系统错误"),
|
||||
PARAM_NOT_VALID("AUTH-10001", "参数错误"),
|
||||
VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试");
|
||||
SYSTEM_ERROR("AUTH-10000", "系统错误"), PARAM_NOT_VALID("AUTH-10001", "参数错误"), // -------- 验证码异常状态码 --------
|
||||
VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试"),
|
||||
SMS_SEND_FAILED("AUTH-20001", "短信发送失败,请稍后再试"),
|
||||
SMS_SEND_TIMEOUT("AUTH-20002", "短信发送超时,请稍后再试");
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
|
||||
+1
-1
@@ -25,6 +25,6 @@ public class SendVerificationCodeReqVO {
|
||||
* 手机号
|
||||
*/
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
private String phone;
|
||||
private String phoneNumber;
|
||||
|
||||
}
|
||||
|
||||
+70
-19
@@ -1,51 +1,102 @@
|
||||
package top.crushtj.xiaoyishu.auth.service.impl;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import top.crushtj.framework.common.exception.BizException;
|
||||
import top.crushtj.framework.common.response.Response;
|
||||
import top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants;
|
||||
import top.crushtj.xiaoyishu.auth.enums.ResponseCodeEnum;
|
||||
import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import top.crushtj.framework.common.exception.BizException;
|
||||
import top.crushtj.framework.common.response.Response;
|
||||
import top.crushtj.framework.common.utils.MaskUtils;
|
||||
import top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants;
|
||||
import top.crushtj.xiaoyishu.auth.enums.ResponseCodeEnum;
|
||||
import top.crushtj.xiaoyishu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
|
||||
import top.crushtj.xiaoyishu.auth.service.VerificationCodeService;
|
||||
import top.crushtj.xiaoyishu.auth.sms.AliyunSmsHelper;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME;
|
||||
|
||||
/**
|
||||
* 验证码服务实现类
|
||||
* 修复点:异步异常捕获、Redis过期时间、验证码日志脱敏、手机号校验、线程任务跟踪
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class VerificationCodeServiceImpl implements VerificationCodeService {
|
||||
// 短信发送超时时间(秒)
|
||||
private static final int SMS_SEND_TIMEOUT_SECONDS = 5;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Resource(name = "taskExecutor")
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
@Resource
|
||||
private AliyunSmsHelper aliyunSmsHelper;
|
||||
|
||||
@Override
|
||||
public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
|
||||
// 手机号
|
||||
String phone = sendVerificationCodeReqVO.getPhone();
|
||||
// 1. 获取并校验手机号格式
|
||||
String phoneNumber = sendVerificationCodeReqVO.getPhoneNumber();
|
||||
|
||||
// 构建验证码 redis key
|
||||
String key = RedisKeyConstants.buildVerificationCodeKey(phone);
|
||||
|
||||
// 判断是否已发送验证码
|
||||
// 2. 构建Redis Key并检查发送频率
|
||||
String key = RedisKeyConstants.buildVerificationCodeKey(phoneNumber);
|
||||
boolean isSent = redisTemplate.hasKey(key);
|
||||
if (isSent) {
|
||||
// 若之前发送的验证码未过期,则提示发送频繁
|
||||
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
|
||||
}
|
||||
|
||||
// 生成 6 位随机数字验证码
|
||||
// 3. 生成6位随机验证码
|
||||
String verificationCode = RandomUtil.randomNumbers(6);
|
||||
|
||||
// todo: 调用第三方短信发送服务
|
||||
// 4. 异步发送短信(用CompletableFuture跟踪任务状态,捕获异常)
|
||||
CompletableFuture<Boolean> smsSendFuture = CompletableFuture.supplyAsync(() -> {
|
||||
// 设置线程名称,便于日志排查
|
||||
Thread.currentThread().setName("sms-send-" + MaskUtils.maskMobile(phoneNumber));
|
||||
String signName = "速通互联验证码";
|
||||
String templateCode = "100001";
|
||||
String templateParam = String.format("{\"code\":\"%s\",\"min\":\"%d\"}", verificationCode, VERIFICATION_CODE_EXPIRE_TIME);
|
||||
try {
|
||||
return aliyunSmsHelper.sendMessage(signName, templateCode, phoneNumber, templateParam);
|
||||
} catch (Exception e) {
|
||||
log.error("==> 手机号: {}, 短信发送接口调用异常", MaskUtils.maskMobile(phoneNumber), e);
|
||||
return false;
|
||||
}
|
||||
}, threadPoolTaskExecutor);
|
||||
|
||||
log.info("==> 手机号: {}, 已发送验证码:【{}】", phone, verificationCode);
|
||||
// 5. 同步等待短信发送结果(超时控制,避免主线程阻塞过久)
|
||||
boolean smsSendSuccess;
|
||||
try {
|
||||
smsSendSuccess = smsSendFuture.get(SMS_SEND_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("==> 手机号: {}, 短信发送任务被中断", MaskUtils.maskMobile(phoneNumber), e);
|
||||
Thread.currentThread().interrupt(); // 恢复中断状态
|
||||
throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
|
||||
} catch (ExecutionException e) {
|
||||
log.error("==> 手机号: {}, 短信发送任务执行异常", MaskUtils.maskMobile(phoneNumber), e);
|
||||
throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
|
||||
} catch (TimeoutException e) {
|
||||
log.error("==> 手机号: {}, 短信发送任务超时({}秒)", MaskUtils.maskMobile(phoneNumber), SMS_SEND_TIMEOUT_SECONDS, e);
|
||||
throw new BizException(ResponseCodeEnum.SMS_SEND_TIMEOUT);
|
||||
}
|
||||
|
||||
// 存储验证码到 redis, 并设置过期时间为 3 分钟
|
||||
redisTemplate.opsForValue().set(key, verificationCode, 3, TimeUnit.MINUTES);
|
||||
// 6. 短信发送失败则直接抛异常,不存储Redis
|
||||
if (!smsSendSuccess) {
|
||||
log.error("==> 手机号: {}, 发送验证码失败(第三方接口返回失败)", MaskUtils.maskMobile(phoneNumber));
|
||||
throw new BizException(ResponseCodeEnum.SMS_SEND_FAILED);
|
||||
}
|
||||
|
||||
// 7. 短信发送成功后,记录日志(验证码脱敏,仅保留后2位)+ 存储Redis
|
||||
log.info("==> 手机号: {}, 已发送验证码:【****{}】", MaskUtils.maskMobile(phoneNumber), verificationCode.substring(4));
|
||||
redisTemplate.opsForValue().set(key, verificationCode, VERIFICATION_CODE_EXPIRE_TIME, TimeUnit.MINUTES);
|
||||
|
||||
return Response.success();
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package top.crushtj.xiaoyishu.auth.sms;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author ayi
|
||||
* @version V1.0
|
||||
* @title AliyunAccessKeyProperties
|
||||
* @date 2026/01/16 23:31
|
||||
* @description 阿里云访问密钥配置
|
||||
*/
|
||||
|
||||
@ConfigurationProperties(prefix = "aliyun")
|
||||
@Component
|
||||
@Data
|
||||
public class AliyunAccessKeyProperties {
|
||||
private String accessKeyId;
|
||||
private String accessKeySecret;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package top.crushtj.xiaoyishu.auth.sms;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author ayi
|
||||
* @version V1.0
|
||||
* @title AliyunSmsClientConfig
|
||||
* @date 2026/01/16 23:48
|
||||
* @description 阿里云短信配置
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class AliyunSmsClientConfig {
|
||||
@Resource
|
||||
private AliyunAccessKeyProperties aliyunAccessKeyProperties;
|
||||
|
||||
@Bean
|
||||
public com.aliyun.dypnsapi20170525.Client smsClient() {
|
||||
try {
|
||||
com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();
|
||||
|
||||
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
|
||||
.setCredential(credential);
|
||||
|
||||
// Endpoint 请参考 https://api.aliyun.com/product/Dypnsapi
|
||||
config.endpoint = "dypnsapi.aliyuncs.com";
|
||||
|
||||
config.accessKeyId = aliyunAccessKeyProperties.getAccessKeyId();
|
||||
config.accessKeySecret = aliyunAccessKeyProperties.getAccessKeySecret();
|
||||
|
||||
return new com.aliyun.dypnsapi20170525.Client(config);
|
||||
} catch (Exception e) {
|
||||
log.error("初始化阿里云短信发送客户端错误: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package top.crushtj.xiaoyishu.auth.sms;
|
||||
|
||||
import com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.crushtj.framework.common.utils.JsonUtils;
|
||||
import top.crushtj.framework.common.utils.MaskUtils;
|
||||
|
||||
/**
|
||||
* @author ayi
|
||||
* @version V1.0
|
||||
* @title AliyunSmsHelper
|
||||
* @date 2026/01/16 23:36
|
||||
* @description 阿里云短信工具
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AliyunSmsHelper {
|
||||
|
||||
@Resource
|
||||
private com.aliyun.dypnsapi20170525.Client client;
|
||||
|
||||
public boolean sendMessage(String signName, String templateCode, String phoneNumber, String templateParam) {
|
||||
com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest sendSmsVerifyCodeRequest = new com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest()
|
||||
.setSignName(signName)
|
||||
.setTemplateCode(templateCode)
|
||||
.setPhoneNumber(phoneNumber)
|
||||
.setTemplateParam(templateParam);
|
||||
|
||||
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
|
||||
|
||||
try {
|
||||
log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", MaskUtils.maskMobile(phoneNumber), signName, templateCode, templateParam);
|
||||
|
||||
// 发送短信
|
||||
SendSmsVerifyCodeResponse response = client.sendSmsVerifyCodeWithOptions(sendSmsVerifyCodeRequest, runtime);
|
||||
|
||||
boolean success = response.getBody().success;
|
||||
log.info("==> 短信发送状态: {}, response: {}", success, JsonUtils.toJsonString(response));
|
||||
return success;
|
||||
} catch (Exception error) {
|
||||
log.error("==> 短信发送错误: ", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -71,4 +71,8 @@ mybatis-plus:
|
||||
|
||||
logging:
|
||||
level:
|
||||
top.crushtj.xiaoyishu.auth.domain.mappers: debug
|
||||
top.crushtj.xiaoyishu.auth.domain.mappers: debug
|
||||
|
||||
aliyun: # 接入阿里云(发送短信使用)
|
||||
accessKeyId:
|
||||
accessKeySecret:
|
||||
@@ -1,40 +0,0 @@
|
||||
package top.crushtj.xiaoyishu.auth;
|
||||
|
||||
import com.alibaba.druid.filter.config.ConfigTools;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* @author ayi
|
||||
* @version V1.0
|
||||
* @title DruidTest
|
||||
* @description
|
||||
* @date 2025/12/01
|
||||
*/
|
||||
|
||||
@SpringBootTest
|
||||
@Slf4j
|
||||
public class DruidTest {
|
||||
|
||||
/**
|
||||
* Druid 密码加密
|
||||
*/
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void testEncodePassword() {
|
||||
// 你的密码
|
||||
String password = "HhpxE2HWE4bGTyB5";
|
||||
String[] arr = ConfigTools.genKeyPair(512);
|
||||
|
||||
// 私钥
|
||||
log.info("privateKey: {}", arr[0]);
|
||||
// 公钥
|
||||
log.info("publicKey: {}", arr[1]);
|
||||
|
||||
// 通过私钥加密密码
|
||||
String encodePassword = ConfigTools.encrypt(arr[0], password);
|
||||
log.info("password: {}", encodePassword);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package top.crushtj.xiaoyishu.auth;
|
||||
|
||||
import com.alibaba.druid.filter.config.ConfigTools;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
|
||||
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* @author ayi
|
||||
* @version V1.0
|
||||
* @title DruidTest
|
||||
* @description
|
||||
* @date 2025/12/01
|
||||
*/
|
||||
|
||||
@SpringBootTest
|
||||
@Slf4j
|
||||
public class EncryptTest {
|
||||
//@Autowired
|
||||
//private StringEncryptor defaultLazyEncryptor;
|
||||
//private StringEncryptor pooledPbeStringEncryptor;
|
||||
|
||||
/**
|
||||
* Druid 密码加密
|
||||
*/
|
||||
@Test
|
||||
@SneakyThrows
|
||||
void testEncodePassword() {
|
||||
// 你的密码
|
||||
String password = "HhpxE2HWE4bGTyB5";
|
||||
String[] arr = ConfigTools.genKeyPair(512);
|
||||
|
||||
// 私钥
|
||||
log.info("privateKey: {}", arr[0]);
|
||||
// 公钥
|
||||
log.info("publicKey: {}", arr[1]);
|
||||
|
||||
// 通过私钥加密密码
|
||||
String encodePassword = ConfigTools.encrypt(arr[0], password);
|
||||
log.info("password: {}", encodePassword);
|
||||
}
|
||||
|
||||
@Test
|
||||
void smsEncode() {
|
||||
String accessKeyId = manualEncrypt("", "Yu020320.");
|
||||
System.out.println("accessKeyId:" + accessKeyId);
|
||||
String accessKeySecret = manualEncrypt("", "Yu020320.");
|
||||
System.out.println("accessKeySecret:" + accessKeySecret);
|
||||
}
|
||||
|
||||
private String manualEncrypt(String plainText, String secretKey) {
|
||||
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
|
||||
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
|
||||
config.setPassword(secretKey); // 密钥
|
||||
config.setAlgorithm("PBEWithMD5AndDES"); // 算法
|
||||
config.setPoolSize(1); // 池大小(默认)
|
||||
encryptor.setConfig(config);
|
||||
return encryptor.encrypt(plainText);
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package top.crushtj.framework.common.utils;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author ayi
|
||||
* @version V1.0
|
||||
* @title MaskUtils
|
||||
* @date 2026/01/17 13:22
|
||||
* @description 数据脱敏工具类
|
||||
*/
|
||||
|
||||
public class MaskUtils {
|
||||
|
||||
// ===================== 通用脱敏方法(基础) =====================
|
||||
/**
|
||||
* 通用脱敏方法:保留前prefixLen位,后suffixLen位,中间用*填充
|
||||
* @param content 原始字符串
|
||||
* @param prefixLen 保留前缀长度
|
||||
* @param suffixLen 保留后缀长度
|
||||
* @return 脱敏后的字符串
|
||||
*/
|
||||
public static String maskGeneral(String content, int prefixLen, int suffixLen) {
|
||||
// 空值处理:null/空串直接返回,避免NPE
|
||||
if (StringUtils.isBlank(content)) {
|
||||
return content;
|
||||
}
|
||||
int contentLen = content.length();
|
||||
// 若长度小于等于前缀+后缀,直接返回原字符串(避免过度脱敏)
|
||||
if (contentLen <= prefixLen + suffixLen) {
|
||||
return content;
|
||||
}
|
||||
// 拼接前缀 + 中间* + 后缀
|
||||
StringBuilder sb = new StringBuilder();
|
||||
// 添加前缀
|
||||
sb.append(content.substring(0, prefixLen));
|
||||
// 添加中间掩码(长度=总长度-前缀-后缀)
|
||||
sb.append("*".repeat(Math.max(0, contentLen - prefixLen - suffixLen)));
|
||||
// 添加后缀
|
||||
sb.append(content.substring(contentLen - suffixLen));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ===================== 常用敏感字段脱敏方法(业务封装) =====================
|
||||
|
||||
/**
|
||||
* 手机号脱敏:保留前3位,后4位,中间4位掩码(如:138****1234)
|
||||
* @param mobile 手机号(支持11位常规手机号)
|
||||
* @return 脱敏后的手机号
|
||||
*/
|
||||
public static String maskMobile(String mobile) {
|
||||
// 先校验手机号格式(简单校验,可根据业务调整)
|
||||
if (StringUtils.isBlank(mobile) || !mobile.matches("^1[3-9]\\d{9}$")) {
|
||||
return mobile;
|
||||
}
|
||||
return maskGeneral(mobile, 3, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱脱敏:保留前缀前3位,@及域名完整,中间掩码(如:123****@qq.com)
|
||||
* @param email 邮箱地址
|
||||
* @return 脱敏后的邮箱
|
||||
*/
|
||||
public static String maskEmail(String email) {
|
||||
if (StringUtils.isBlank(email) || !email.contains("@")) {
|
||||
return email;
|
||||
}
|
||||
String[] parts = email.split("@");
|
||||
String prefix = parts[0];
|
||||
String domain = parts[1];
|
||||
// 前缀长度<=3时不脱敏,否则保留前3位
|
||||
String maskedPrefix = prefix.length() <= 3 ? prefix : maskGeneral(prefix, 3, 0);
|
||||
return maskedPrefix + "@" + domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 姓名脱敏:
|
||||
* - 单字名:直接返回(如:李 → 李)
|
||||
* - 两字名:保留姓,名掩码(如:李白 → 李*)
|
||||
* - 三字及以上:保留姓和最后一个字,中间掩码(如:李世民 → 李*民)
|
||||
* @param name 姓名
|
||||
* @return 脱敏后的姓名
|
||||
*/
|
||||
public static String maskName(String name) {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return name;
|
||||
}
|
||||
int nameLen = name.length();
|
||||
if (nameLen == 1) {
|
||||
return name;
|
||||
} else if (nameLen == 2) {
|
||||
return name.substring(0, 1) + "*";
|
||||
} else {
|
||||
// 姓 + 中间* + 最后一个字
|
||||
return name.substring(0, 1) +
|
||||
"*".repeat(nameLen - 2) +
|
||||
name.substring(nameLen - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份证号脱敏:保留前3位,后4位,中间掩码(如:110***********1234)
|
||||
* @param idCard 身份证号(支持15位/18位)
|
||||
* @return 脱敏后的身份证号
|
||||
*/
|
||||
public static String maskIdCard(String idCard) {
|
||||
if (StringUtils.isBlank(idCard) || (idCard.length() != 15 && idCard.length() != 18)) {
|
||||
return idCard;
|
||||
}
|
||||
return maskGeneral(idCard, 3, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 银行卡号脱敏:保留前6位,后4位,中间掩码(如:622260**********1234)
|
||||
* @param bankCard 银行卡号(常规16/19位)
|
||||
* @return 脱敏后的银行卡号
|
||||
*/
|
||||
public static String maskBankCard(String bankCard) {
|
||||
if (StringUtils.isBlank(bankCard) || bankCard.length() < 10) {
|
||||
return bankCard;
|
||||
}
|
||||
return maskGeneral(bankCard, 6, 4);
|
||||
}
|
||||
|
||||
// ===================== 测试用例(可直接运行验证) =====================
|
||||
public static void main(String[] args) {
|
||||
// 测试手机号脱敏
|
||||
System.out.println("手机号脱敏:" + maskMobile("13812345678")); // 输出:138****5678
|
||||
// 测试邮箱脱敏
|
||||
System.out.println("邮箱脱敏:" + maskEmail("1234567890@qq.com")); // 输出:123****@qq.com
|
||||
// 测试姓名脱敏
|
||||
System.out.println("单字名:" + maskName("李")); // 输出:李
|
||||
System.out.println("两字名:" + maskName("李白")); // 输出:李*
|
||||
System.out.println("三字名:" + maskName("李世民")); // 输出:李*民
|
||||
// 测试身份证脱敏
|
||||
System.out.println("身份证脱敏:" + maskIdCard("110101199001011234")); // 输出:110***********1234
|
||||
// 测试银行卡脱敏
|
||||
System.out.println("银行卡脱敏:" + maskBankCard("6222600000000001234")); // 输出:622260**********1234
|
||||
// 测试空值/异常值
|
||||
System.out.println("空手机号:" + maskMobile(null)); // 输出:null
|
||||
System.out.println("无效邮箱:" + maskEmail("123456")); // 输出:123456
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user