国际验证码以及手机验证码登录

This commit is contained in:
cnc
2026-09-18 09:44:26 +08:00
parent 926426fe4f
commit e0524d17eb
11 changed files with 296 additions and 22 deletions

16
pom.xml
View File

@@ -61,6 +61,8 @@
<aws.sdk.version>2.48.1</aws.sdk.version> <aws.sdk.version>2.48.1</aws.sdk.version>
<!-- SMS 配置 --> <!-- SMS 配置 -->
<sms4j.version>3.3.5</sms4j.version> <sms4j.version>3.3.5</sms4j.version>
<!-- 阿里云国际短信 SDK 版本,请以 https://mvnrepository.com/artifact/com.aliyun/dysmsapi20180501 最新版为准 -->
<aliyun-dysmsapi.version>1.0.12</aliyun-dysmsapi.version>
<!-- 工作流配置 --> <!-- 工作流配置 -->
<warm-flow.version>1.8.9</warm-flow.version> <warm-flow.version>1.8.9</warm-flow.version>
<liteflow.version>2.16.1.2</liteflow.version> <liteflow.version>2.16.1.2</liteflow.version>
@@ -259,6 +261,20 @@
<version>${sms4j.version}</version> <version>${sms4j.version}</version>
</dependency> </dependency>
<!-- 阿里云国际短信 SDK(SendMessageToGlobe) -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20180501</artifactId>
<version>${aliyun-dysmsapi.version}</version>
</dependency>
<!-- 阿里云 OpenAPI 基础包 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.3.10</version>
</dependency>
<!-- Spring Boot Admin 服务端 --> <!-- Spring Boot Admin 服务端 -->
<dependency> <dependency>
<groupId>de.codecentric</groupId> <groupId>de.codecentric</groupId>

View File

@@ -62,6 +62,18 @@
<artifactId>ruoyi-common-mail</artifactId> <artifactId>ruoyi-common-mail</artifactId>
</dependency> </dependency>
<!-- 短信模块(提供 AliyunInternationalSmsProperties / 自动配置) -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sms</artifactId>
</dependency>
<!-- 阿里云国际短信 SDK(本模块直接 import 了 Client / models / RuntimeOptions) -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20180501</artifactId>
</dependency>
<!-- mcp模块 --> <!-- mcp模块 -->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>

View File

@@ -6,6 +6,7 @@ import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import com.aliyun.dysmsapi20180501.models.SendMessageToGlobeResponseBody;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -21,6 +22,7 @@ import org.dromara.common.mail.core.MailBuilder;
import org.dromara.common.redis.annotation.RateLimiter; import org.dromara.common.redis.annotation.RateLimiter;
import org.dromara.common.redis.enums.LimitType; import org.dromara.common.redis.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.sms.core.AliyunInternationalSmsService;
import org.dromara.common.web.config.properties.CaptchaProperties; import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.common.web.core.WaveAndCircleCaptcha; import org.dromara.common.web.core.WaveAndCircleCaptcha;
import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.SmsBlend;
@@ -51,6 +53,7 @@ public class CaptchaController {
private final CaptchaProperties captchaProperties; private final CaptchaProperties captchaProperties;
private final MailProperties mailProperties; private final MailProperties mailProperties;
private final AliyunInternationalSmsService aliyunInternationalSmsService;
/** /**
* 发送短信验证码。 * 发送短信验证码。
@@ -80,6 +83,33 @@ public class CaptchaController {
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
return R.ok(); return R.ok();
} }
/**
* 发送国际短信(阿里云国际短信包,向境外号码发送)
*
* @param phoneNumber 国际号码,带国家码不含 +/00,例如 62895111666
*/
@GetMapping("/sendInternational")
public R<Object> sendInternational( String phoneNumber) {
// if (!RegexValidator.isMobile(phoneNumber)) {
// return R.fail("请输入正确的手机号!");
// }
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
String code = RandomUtil.randomNumbers(4);
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put("code", code);
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
SendMessageToGlobeResponseBody body = aliyunInternationalSmsService.sendMessageToGlobe(phoneNumber, code);
if (!"OK".equals(body.getResponseCode())) {
log.error("验证码短信发送异常 => {}", body.getResponseDescription());
return R.warn("验证码短信发送失败,请联系管理员!" );
}
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
return R.ok();
}
/** /**
* 发送邮箱验证码 * 发送邮箱验证码

View File

@@ -3,12 +3,15 @@ package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter; import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.BCrypt;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.SystemConstants; import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.enums.LoginType; import org.dromara.common.core.enums.LoginType;
import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.exception.user.CaptchaExpireException; import org.dromara.common.core.exception.user.CaptchaExpireException;
import org.dromara.common.core.exception.user.UserException; import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils; import org.dromara.common.core.utils.MessageUtils;
@@ -20,9 +23,11 @@ import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser; import org.dromara.system.api.model.LoginUser;
import org.dromara.system.api.model.SmsLoginBody; import org.dromara.system.api.model.SmsLoginBody;
import org.dromara.system.domain.SysUser; import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.SysUserMapper; import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.ISysUserService;
import org.dromara.web.domain.vo.LoginVo; import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy; import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService; import org.dromara.web.service.SysLoginService;
@@ -38,11 +43,18 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor @RequiredArgsConstructor
public class SmsAuthStrategy implements IAuthStrategy { public class SmsAuthStrategy implements IAuthStrategy {
/**
* 短信自动注册新用户的初始登录密码
*/
private static final String INIT_PASSWORD = "@feiqin123456$";
private final SysLoginService loginService; private final SysLoginService loginService;
private final SysUserMapper userMapper; private final SysUserMapper userMapper;
private final ISysUserService userService;
/** /**
* 执行短信验证码登录,并按客户端配置生成访问令牌。 * 执行短信验证码登录,并按客户端配置生成访问令牌。
* 若手机号不存在,则先校验短信验证码,再根据手机号自动注册新用户并完成登录。
* *
* @param body 登录请求体 * @param body 登录请求体
* @param client 当前客户端配置 * @param client 当前客户端配置
@@ -54,8 +66,16 @@ public class SmsAuthStrategy implements IAuthStrategy {
ValidatorUtils.validate(loginBody); ValidatorUtils.validate(loginBody);
String phoneNumber = loginBody.getPhoneNumber(); String phoneNumber = loginBody.getPhoneNumber();
String smsCode = loginBody.getSmsCode(); String smsCode = loginBody.getSmsCode();
// 先校验短信验证码, 避免验证码错误时误注册新用户
loginService.checkLogin(LoginType.SMS, phoneNumber, () -> !validateSmsCode(phoneNumber, smsCode));
// 手机号对应用户不存在时, 自动注册新用户
SysUserVo user = loadUserByPhoneNumber(phoneNumber); SysUserVo user = loadUserByPhoneNumber(phoneNumber);
loginService.checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phoneNumber, smsCode)); if (ObjectUtil.isNull(user)) {
user = registerUserByPhoneNumber(phoneNumber);
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phoneNumber);
throw new UserException("user.blocked", phoneNumber);
}
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了 // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = loginService.buildLoginUser(user); LoginUser loginUser = loginService.buildLoginUser(user);
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
@@ -88,23 +108,53 @@ public class SmsAuthStrategy implements IAuthStrategy {
} }
/** /**
* 按手机号加载可登录用户,并校验是否存在或被停用。 * 按手机号加载可登录用户。
* *
* @param phoneNumber 手机号 * @param phoneNumber 手机号
* @return 用户信息 * @return 用户信息, 不存在时返回 {@code null}
*/ */
private SysUserVo loadUserByPhoneNumber(String phoneNumber) { private SysUserVo loadUserByPhoneNumber(String phoneNumber) {
SysUserVo user = userMapper.lambda() return userMapper.lambda()
.eq(SysUser::getPhoneNumber, phoneNumber) .eq(SysUser::getPhoneNumber, phoneNumber)
.voOne(); .voOne();
if (ObjectUtil.isNull(user)) { }
log.info("登录用户:{} 不存在.", phoneNumber);
throw new UserException("user.not.exists", phoneNumber); /**
} else if (SystemConstants.DISABLE.equals(user.getStatus())) { * 根据手机号注册新用户。
log.info("登录用户:{} 已被停用.", phoneNumber); *
throw new UserException("user.blocked", phoneNumber); * @param phoneNumber 手机号
* @return 注册后的用户信息
*/
private SysUserVo registerUserByPhoneNumber(String phoneNumber) {
SysUserBo userBo = new SysUserBo();
userBo.setUserName(generateUserName(phoneNumber));
userBo.setNickName(phoneNumber);
userBo.setPhoneNumber(phoneNumber);
userBo.setUserType(UserType.SYS_USER.getUserType());
userBo.setStatus(SystemConstants.NORMAL);
// 短信注册使用统一的初始登录密码
userBo.setPassword(BCrypt.hashpw(INIT_PASSWORD));
boolean regFlag = userService.registerUser(userBo);
if (!regFlag) {
log.info("手机号:{} 自动注册失败.", phoneNumber);
throw new UserException("user.register.error");
} }
return user; log.info("手机号:{} 不存在, 已自动注册新用户.", phoneNumber);
return loadUserByPhoneNumber(phoneNumber);
}
/**
* 生成唯一的登录账号, 默认使用手机号, 若已存在则追加随机后缀。
*
* @param phoneNumber 手机号
* @return 唯一的登录账号
*/
private String generateUserName(String phoneNumber) {
String userName = phoneNumber;
while (userMapper.lambda().eq(SysUser::getUserName, userName).exists()) {
userName = phoneNumber + RandomUtil.randomNumbers(4);
}
return userName;
} }
} }

View File

@@ -155,7 +155,7 @@ spring.data:
# 数据库索引 # 数据库索引
database: 0 database: 0
# redis 密码必须配置 # redis 密码必须配置
# password: ruoyi123 password: 123456
# 连接超时时间 # 连接超时时间
timeout: 10s timeout: 10s
# 是否开启ssl # 是否开启ssl
@@ -227,9 +227,9 @@ sms:
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。 # 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
access-key-id: 您的accessKey access-key-id: 您的accessKey
# 称为accessSecret有些称之为apiSecret # 称为accessSecret有些称之为apiSecret
access-key-secret: 您的accessKeySecret access-key-secret: 9jea9cuubXCEWlMXEd48ye2FCWQQPf
signature: 您的短信签名 signature: 深圳市聚心时代科技
sdk-app-id: 您的sdkAppId sdk-app-id: LTAI5t8c9LHzr2oUVoKw5yEX
config2: config2:
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分 # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
supplier: tencent supplier: tencent
@@ -238,6 +238,18 @@ sms:
signature: 您的短信签名 signature: 您的短信签名
sdk-app-id: 您的sdkAppId sdk-app-id: 您的sdkAppId
--- # 阿里云国际短信(SendMessageToGlobe,向境外号码发送)
aliyun:
international-sms:
# 是否启用
enabled: true
# 阿里云国际站 AccessKey(建议用环境变量注入,勿硬编码)
access-key-id: LTAI5t8c9LHzr2oUVoKw5yEX
access-key-secret: 9jea9cuubXCEWlMXEd48ye2FCWQQPf
# 国际短信接入点,固定为新加坡节点
endpoint: dysmsapi.ap-southeast-1.aliyuncs.com
# 发件人标识 SenderID(部分国家/地区需报备,可留空)
sender-id: ''
--- # 三方授权 --- # 三方授权
justauth: justauth:

View File

@@ -264,15 +264,15 @@ mqtt.client:
# 是否开启客户端,默认:true # 是否开启客户端,默认:true
enabled: true enabled: true
# 连接的服务端 ip ,默认:127.0.0.1 # 连接的服务端 ip ,默认:127.0.0.1
ip: ssl://www.mmipco.cn ip: 192.168.1.249
# 端口:默认:1883 # 端口:默认:1883
port: 8883 port: 1883
# 客户端名称 # 客户端名称
name: Mqtt-Client name: Mqtt_bird_monitoring
# 客户端Id(非常重要,一般为设备 sn,不可重复) # 客户端Id(非常重要,一般为设备 sn,不可重复)
client-id: 000001 client-id: Mqttbirdmonitoring001
username: admin # username: admin
password: 61e6062129e9 # password: 61e6062129e9
# 超时时间,单位:秒,默认:5秒 # 超时时间,单位:秒,默认:5秒
timeout: 5 timeout: 5
# 重连时间,默认 5000 毫秒 # 重连时间,默认 5000 毫秒
@@ -291,7 +291,7 @@ mqtt.client:
mqtt-executor-size: 2 mqtt-executor-size: 2
# 是否开启 ssl # 是否开启 ssl
ssl: ssl:
enabled: true enabled: false
keystore-path: keystore-path:
keystore-pass: keystore-pass:
truststore-path: truststore-path:

View File

@@ -23,11 +23,27 @@
<artifactId>sms4j-spring-boot-starter</artifactId> <artifactId>sms4j-spring-boot-starter</artifactId>
</dependency> </dependency>
<!-- 阿里云国际短信 SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20180501</artifactId>
</dependency>
<!-- 阿里云 OpenAPI 基础包(提供 com.aliyun.teaopenapi.models.Config,显式声明避免仅靠传递依赖) -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
</dependency>
<!-- RuoYi Common Redis--> <!-- RuoYi Common Redis-->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId> <artifactId>ruoyi-common-redis</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@@ -0,0 +1,37 @@
package org.dromara.common.sms.config;
import com.aliyun.dysmsapi20180501.Client;
import com.aliyun.teaopenapi.models.Config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* 阿里云国际短信自动配置
*
* @author ruoyi
*/
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(AliyunInternationalSmsProperties.class)
@ConditionalOnProperty(prefix = "aliyun.international-sms", name = "enabled", havingValue = "true")
public class AliyunInternationalSmsAutoConfiguration {
/**
* 构建阿里云国际短信客户端
*
* @param properties 国际短信配置
* @return 短信客户端
*/
@Bean
public Client dysmsapi20180501Client(AliyunInternationalSmsProperties properties) throws Exception {
Config config = new Config()
.setAccessKeyId(properties.getAccessKeyId())
.setAccessKeySecret(properties.getAccessKeySecret());
config.endpoint = properties.getEndpoint();
return new Client(config);
}
}

View File

@@ -0,0 +1,40 @@
package org.dromara.common.sms.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 阿里云国际短信配置属性
*
* @author ruoyi
*/
@Data
@ConfigurationProperties(prefix = "aliyun.international-sms")
public class AliyunInternationalSmsProperties {
/**
* 是否启用
*/
private Boolean enabled = false;
/**
* 阿里云国际站 AccessKeyId
*/
private String accessKeyId;
/**
* 阿里云国际站 AccessKeySecret
*/
private String accessKeySecret;
/**
* 接入点,国际短信固定为 dysmsapi.ap-southeast-1.aliyuncs.com
*/
private String endpoint = "dysmsapi.ap-southeast-1.aliyuncs.com";
/**
* 发件人标识 SenderId(部分国家/地区需报备)
*/
private String senderId;
}

View File

@@ -0,0 +1,60 @@
package org.dromara.common.sms.core;
import com.aliyun.dysmsapi20180501.Client;
import com.aliyun.dysmsapi20180501.models.SendMessageToGlobeRequest;
import com.aliyun.dysmsapi20180501.models.SendMessageToGlobeResponse;
import com.aliyun.dysmsapi20180501.models.SendMessageToGlobeResponseBody;
import com.aliyun.teautil.models.RuntimeOptions;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.sms.config.AliyunInternationalSmsProperties;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 阿里云国际短信发送服务
*
* @author ruoyi
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AliyunInternationalSmsService {
private final Client dysmsapi20180501Client;
private final AliyunInternationalSmsProperties properties;
/**
* 向境外手机号发送短信
*
* @param to 国际号码,带国家码且不含 + / 00 前缀,例如 62895111666
* @param message 短信内容(国际短信可自由编辑)
* @return 发送结果
*/
public SendMessageToGlobeResponseBody sendMessageToGlobe(String to, String message) {
log.info("国际短信发送开始, 手机号={}, message={}", to, message);
SendMessageToGlobeRequest request = new SendMessageToGlobeRequest()
.setTo(to)
.setMessage("您的验证码为:" + message + ",请勿泄露于他人!");
if (StringUtils.hasText(properties.getSenderId())) {
request.setChannelId(properties.getSenderId());
}
try {
SendMessageToGlobeResponse response = dysmsapi20180501Client
.sendMessageToGlobeWithOptions(request, new RuntimeOptions());
SendMessageToGlobeResponseBody body = response.getBody();
if (body == null || !"OK".equalsIgnoreCase(body.getResponseCode())) {
log.error("国际短信发送失败, to={}, body={}", to, body);
throw new ServiceException("国际短信发送失败:" + (body == null ? "无响应" : body.getResponseCode()));
}
return body;
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("国际短信发送异常, to={}", to, e);
throw new ServiceException("国际短信发送异常:" + e.getMessage());
}
}
}

View File

@@ -1 +1,2 @@
org.dromara.common.sms.config.SmsAutoConfiguration org.dromara.common.sms.config.SmsAutoConfiguration
org.dromara.common.sms.config.AliyunInternationalSmsAutoConfiguration