@@ -48,6 +48,7 @@
|
|||||||
<guava.version>33.0.0-jre</guava.version>
|
<guava.version>33.0.0-jre</guava.version>
|
||||||
<hutool.version>5.8.26</hutool.version>
|
<hutool.version>5.8.26</hutool.version>
|
||||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||||
|
<dypnsapi.version>2.0.0</dypnsapi.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<!-- 统一依赖管理 -->
|
<!-- 统一依赖管理 -->
|
||||||
@@ -168,6 +169,13 @@
|
|||||||
<artifactId>commons-lang3</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
<version>${commons-lang3.version}</version>
|
<version>${commons-lang3.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!--阿里云短信认证服务-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aliyun</groupId>
|
||||||
|
<artifactId>dypnsapi20170525</artifactId>
|
||||||
|
<version>${dypnsapi.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,12 @@
|
|||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-pool2</artifactId>
|
<artifactId>commons-pool2</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 阿里云短信认证服务 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aliyun</groupId>
|
||||||
|
<artifactId>dypnsapi20170525</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,10 @@ public class RedisKeyConstants {
|
|||||||
* 验证码 KEY 前缀
|
* 验证码 KEY 前缀
|
||||||
*/
|
*/
|
||||||
private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";
|
private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";
|
||||||
|
/**
|
||||||
|
* 验证码 KEY 过期时间 (分钟)
|
||||||
|
*/
|
||||||
|
public static final long VERIFICATION_CODE_EXPIRE_TIME = 3;
|
||||||
/**
|
/**
|
||||||
* 构建验证码 KEY
|
* 构建验证码 KEY
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package top.crushtj.xiaoyishu.auth.enums;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import java.lang.String;
|
|
||||||
|
|
||||||
import top.crushtj.framework.common.exception.BaseExceptionInterface;
|
import top.crushtj.framework.common.exception.BaseExceptionInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,9 +16,10 @@ import top.crushtj.framework.common.exception.BaseExceptionInterface;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum ResponseCodeEnum implements BaseExceptionInterface {
|
public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||||
// -------- 通用异常状态码 --------
|
// -------- 通用异常状态码 --------
|
||||||
SYSTEM_ERROR("AUTH-10000", "系统错误"),
|
SYSTEM_ERROR("AUTH-10000", "系统错误"), PARAM_NOT_VALID("AUTH-10001", "参数错误"), // -------- 验证码异常状态码 --------
|
||||||
PARAM_NOT_VALID("AUTH-10001", "参数错误"),
|
VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁,请3分钟后再试"),
|
||||||
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 = "手机号不能为空")
|
@NotBlank(message = "手机号不能为空")
|
||||||
private String phone;
|
private String phoneNumber;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-19
@@ -1,51 +1,102 @@
|
|||||||
package top.crushtj.xiaoyishu.auth.service.impl;
|
package top.crushtj.xiaoyishu.auth.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
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 jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.stereotype.Service;
|
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.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.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import static top.crushtj.xiaoyishu.auth.constant.RedisKeyConstants.VERIFICATION_CODE_EXPIRE_TIME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码服务实现类
|
||||||
|
* 修复点:异步异常捕获、Redis过期时间、验证码日志脱敏、手机号校验、线程任务跟踪
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class VerificationCodeServiceImpl implements VerificationCodeService {
|
public class VerificationCodeServiceImpl implements VerificationCodeService {
|
||||||
|
// 短信发送超时时间(秒)
|
||||||
|
private static final int SMS_SEND_TIMEOUT_SECONDS = 5;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private RedisTemplate<String, Object> redisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Resource(name = "taskExecutor")
|
||||||
|
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AliyunSmsHelper aliyunSmsHelper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
|
public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
|
||||||
// 手机号
|
// 1. 获取并校验手机号格式
|
||||||
String phone = sendVerificationCodeReqVO.getPhone();
|
String phoneNumber = sendVerificationCodeReqVO.getPhoneNumber();
|
||||||
|
|
||||||
// 构建验证码 redis key
|
// 2. 构建Redis Key并检查发送频率
|
||||||
String key = RedisKeyConstants.buildVerificationCodeKey(phone);
|
String key = RedisKeyConstants.buildVerificationCodeKey(phoneNumber);
|
||||||
|
|
||||||
// 判断是否已发送验证码
|
|
||||||
boolean isSent = redisTemplate.hasKey(key);
|
boolean isSent = redisTemplate.hasKey(key);
|
||||||
if (isSent) {
|
if (isSent) {
|
||||||
// 若之前发送的验证码未过期,则提示发送频繁
|
|
||||||
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
|
throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成 6 位随机数字验证码
|
// 3. 生成6位随机验证码
|
||||||
String verificationCode = RandomUtil.randomNumbers(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 分钟
|
// 6. 短信发送失败则直接抛异常,不存储Redis
|
||||||
redisTemplate.opsForValue().set(key, verificationCode, 3, TimeUnit.MINUTES);
|
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();
|
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:
|
logging:
|
||||||
level:
|
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