fix(sms): 修复短信验证码发送功能的安全性和稳定性问题

- 添加了参数校验,防止空参数导致的异常
- 替换了硬编码的阿里云密钥配置,支持租户自定义配置
- 修复了随机数生成器的安全问题,使用ThreadLocalRandom替代Random
- 添加了日志记录功能,便于问题排查和监控
- 优化了Redis缓存键的存储逻辑,兼容历史数据格式
- 增强了异常处理机制,提供更详细的错误信息反馈
- 修复了短信模板参数格式问题,确保验证码正确传递
- 添加了手机号脱敏处理,保护用户隐私安全
This commit is contained in:
2026-03-24 22:05:42 +08:00
parent f38859f211
commit aafb2ef113

View File

@@ -47,6 +47,7 @@ import com.wf.captcha.SpecCaptcha;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -61,8 +62,8 @@ import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadLocalRandom;
import static com.gxwebsoft.common.core.constants.WebsiteConstants.CACHE_KEY_UNIVERSAL_PASSWORD; import static com.gxwebsoft.common.core.constants.WebsiteConstants.CACHE_KEY_UNIVERSAL_PASSWORD;
import static com.gxwebsoft.common.core.constants.WebsiteConstants.CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS; import static com.gxwebsoft.common.core.constants.WebsiteConstants.CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS;
@@ -73,6 +74,7 @@ import static com.gxwebsoft.common.core.constants.WebsiteConstants.CACHE_KEY_VER
* @author WebSoft * @author WebSoft
* @since 2018-12-24 16:10:11 * @since 2018-12-24 16:10:11
*/ */
@Slf4j
@Tag(name = "登录认证") @Tag(name = "登录认证")
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@@ -441,11 +443,15 @@ public class MainController extends BaseController {
@Operation(summary = "发送短信验证码") @Operation(summary = "发送短信验证码")
@PostMapping("/sendSmsCaptcha") @PostMapping("/sendSmsCaptcha")
public ApiResult<?> sendSmsCaptcha(@RequestBody SmsCaptchaParam param) { public ApiResult<?> sendSmsCaptcha(@RequestBody SmsCaptchaParam param) {
// 默认配置 if (param == null) {
String accessKeyId = "LTAI5tEsyhW4GCKbds1qsopg"; return fail("参数不能为空");
String accessKeySecret = "zltFlQrYVAoq2KMFDWgLa3GhkMNeyO"; }
String userTemplateId = "SMS_481670203"; // 默认配置(当租户未配置短信服务时使用)
String sign = "网宿信息"; String accessKeyId = "LTAI5t7jGTFTbpSLzzXY8HzP";
String accessKeySecret = "Z22EPJyUhQaIZfEEmZ4Hdbw6xZibCb";
String templateCode = "SMS_481670203";
String signName = "网宿信息";
String regionId = "cn-hangzhou";
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) { if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
return fail("请输入有效的手机号码"); return fail("请输入有效的手机号码");
@@ -459,54 +465,91 @@ public class MainController extends BaseController {
} }
// 读取租户的短信配置 Integer tenantId = getTenantId();
if (getTenantId() != null) { if (tenantId == null && StrUtil.isNotBlank(param.getTenantId())) {
String string = redisUtil.get("setting:sms:" + getTenantId()); try {
if (string != null) { tenantId = Integer.valueOf(param.getTenantId());
JSONObject jsonObject = JSONObject.parseObject(string); } catch (NumberFormatException e) {
accessKeyId = jsonObject.getString("accessKeyId"); return fail("租户ID格式不正确");
accessKeySecret = jsonObject.getString("accessKeySecret");
userTemplateId = jsonObject.getString("userTemplateId");
sign = jsonObject.getString("sign");
} }
} }
DefaultProfile profile = DefaultProfile.getProfile("regionld", accessKeyId, accessKeySecret); // 读取租户的短信配置SettingController写入的key格式为sms:{tenantId}
if (tenantId != null) {
String settingJson = redisUtil.get("sms:" + tenantId);
// 兼容历史key
if (StrUtil.isBlank(settingJson)) {
settingJson = redisUtil.get("setting:sms:" + tenantId);
}
if (StrUtil.isNotBlank(settingJson)) {
JSONObject jsonObject = JSONObject.parseObject(settingJson);
accessKeyId = StrUtil.blankToDefault(jsonObject.getString("accessKeyId"), accessKeyId);
accessKeySecret = StrUtil.blankToDefault(jsonObject.getString("accessKeySecret"), accessKeySecret);
signName = StrUtil.blankToDefault(
StrUtil.blankToDefault(jsonObject.getString("signName"), jsonObject.getString("sign")),
signName
);
templateCode = StrUtil.blankToDefault(
StrUtil.blankToDefault(jsonObject.getString("templateCode"), jsonObject.getString("userTemplateId")),
templateCode
);
regionId = StrUtil.blankToDefault(jsonObject.getString("regionId"), regionId);
}
}
if (StrUtil.isBlank(accessKeyId) || StrUtil.isBlank(accessKeySecret) || StrUtil.isBlank(signName) || StrUtil.isBlank(templateCode)) {
return fail("短信服务未配置,请在系统设置中完善短信配置");
}
DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
IAcsClient client = new DefaultAcsClient(profile); IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest(); CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST); request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com"); request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25"); request.setSysVersion("2017-05-25");
request.setSysAction("SendSms"); request.setSysAction("SendSms");
request.putQueryParameter("RegionId", "cn-hangzhou"); request.putQueryParameter("RegionId", regionId);
request.putQueryParameter("PhoneNumbers", param.getPhone()); request.putQueryParameter("PhoneNumbers", param.getPhone());
request.putQueryParameter("SignName", sign); request.putQueryParameter("SignName", signName);
request.putQueryParameter("TemplateCode", userTemplateId); request.putQueryParameter("TemplateCode", templateCode);
// 生成短信验证码 // 生成短信验证码
Random randObj = new Random(); String code = Integer.toString(ThreadLocalRandom.current().nextInt(100000, 1000000));
String code = Integer.toString(100000 + randObj.nextInt(900000)); JSONObject templateParam = new JSONObject();
request.putQueryParameter("TemplateParam", "{\"code\":" + code + "}"); templateParam.put("code", code);
request.putQueryParameter("TemplateParam", templateParam.toJSONString());
try { try {
CommonResponse response = client.getCommonResponse(request); CommonResponse response = client.getCommonResponse(request);
String json = response.getData(); String json = response.getData();
Gson g = new Gson(); Gson g = new Gson();
HashMap result = g.fromJson(json, HashMap.class); HashMap result = g.fromJson(json, HashMap.class);
if ("OK".equals(result.get("Message"))) { if ("OK".equals(result.get("Message")) || "OK".equals(result.get("Code"))) {
System.out.println("短信发送成功========================" + result); log.info("短信发送成功 phone={}, result={}", DesensitizedUtil.mobilePhone(param.getPhone()), result);
cacheClient.set(param.getPhone(), code, 5L, TimeUnit.MINUTES); cacheClient.set(param.getPhone(), code, 5L, TimeUnit.MINUTES);
String key = "code:" + param.getPhone(); String key = "code:" + param.getPhone();
redisUtil.set(key, code, 5L, TimeUnit.MINUTES); redisUtil.set(key, code, 5L, TimeUnit.MINUTES);
return success("发送成功", result.get("Message")); return success("发送成功", result.get("Message"));
} else { } else {
log.warn("短信发送失败 phone={}, result={}", DesensitizedUtil.mobilePhone(param.getPhone()), result);
return fail("发送失败"); return fail("发送失败");
} }
} catch (ServerException e) { } catch (ServerException e) {
e.printStackTrace(); log.error("短信发送失败(ServerException) phone={}", DesensitizedUtil.mobilePhone(param.getPhone()), e);
return fail("发送失败");
} catch (ClientException e) { } catch (ClientException e) {
e.printStackTrace(); log.error(
"短信发送失败(ClientException) phone={}, errCode={}, errMsg={}, requestId={}",
DesensitizedUtil.mobilePhone(param.getPhone()),
e.getErrCode(),
e.getErrMsg(),
e.getRequestId(),
e
);
if ("InvalidAccessKeyId.NotFound".equals(e.getErrCode()) || "SignatureDoesNotMatch".equals(e.getErrCode())) {
return fail("短信配置错误请检查AccessKeyId/AccessKeySecret是否正确");
} }
return fail("发送失败"); return fail("发送失败");
} }
}
@OperationLog @OperationLog
@Operation(summary = "重置密码") @Operation(summary = "重置密码")