国际验证码以及手机验证码登录
This commit is contained in:
16
pom.xml
16
pom.xml
@@ -61,6 +61,8 @@
|
||||
<aws.sdk.version>2.48.1</aws.sdk.version>
|
||||
<!-- SMS 配置 -->
|
||||
<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>
|
||||
<liteflow.version>2.16.1.2</liteflow.version>
|
||||
@@ -259,6 +261,20 @@
|
||||
<version>${sms4j.version}</version>
|
||||
</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 服务端 -->
|
||||
<dependency>
|
||||
<groupId>de.codecentric</groupId>
|
||||
|
||||
@@ -62,6 +62,18 @@
|
||||
<artifactId>ruoyi-common-mail</artifactId>
|
||||
</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模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
|
||||
@@ -6,6 +6,7 @@ import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.aliyun.dysmsapi20180501.models.SendMessageToGlobeResponseBody;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.enums.LimitType;
|
||||
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.core.WaveAndCircleCaptcha;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
@@ -51,6 +53,7 @@ public class CaptchaController {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final MailProperties mailProperties;
|
||||
private final AliyunInternationalSmsService aliyunInternationalSmsService;
|
||||
|
||||
/**
|
||||
* 发送短信验证码。
|
||||
@@ -80,6 +83,33 @@ public class CaptchaController {
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
|
||||
@@ -3,12 +3,15 @@ package org.dromara.web.service.impl;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
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.UserException;
|
||||
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.SmsLoginBody;
|
||||
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.SysUserVo;
|
||||
import org.dromara.system.mapper.SysUserMapper;
|
||||
import org.dromara.system.service.ISysUserService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
@@ -38,11 +43,18 @@ import org.springframework.stereotype.Service;
|
||||
@RequiredArgsConstructor
|
||||
public class SmsAuthStrategy implements IAuthStrategy {
|
||||
|
||||
/**
|
||||
* 短信自动注册新用户的初始登录密码
|
||||
*/
|
||||
private static final String INIT_PASSWORD = "@feiqin123456$";
|
||||
|
||||
private final SysLoginService loginService;
|
||||
private final SysUserMapper userMapper;
|
||||
private final ISysUserService userService;
|
||||
|
||||
/**
|
||||
* 执行短信验证码登录,并按客户端配置生成访问令牌。
|
||||
* 若手机号不存在,则先校验短信验证码,再根据手机号自动注册新用户并完成登录。
|
||||
*
|
||||
* @param body 登录请求体
|
||||
* @param client 当前客户端配置
|
||||
@@ -54,8 +66,16 @@ public class SmsAuthStrategy implements IAuthStrategy {
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String phoneNumber = loginBody.getPhoneNumber();
|
||||
String smsCode = loginBody.getSmsCode();
|
||||
// 先校验短信验证码, 避免验证码错误时误注册新用户
|
||||
loginService.checkLogin(LoginType.SMS, phoneNumber, () -> !validateSmsCode(phoneNumber, smsCode));
|
||||
// 手机号对应用户不存在时, 自动注册新用户
|
||||
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 = loginService.buildLoginUser(user);
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
@@ -88,23 +108,53 @@ public class SmsAuthStrategy implements IAuthStrategy {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按手机号加载可登录用户,并校验是否存在或被停用。
|
||||
* 按手机号加载可登录用户。
|
||||
*
|
||||
* @param phoneNumber 手机号
|
||||
* @return 用户信息
|
||||
* @return 用户信息, 不存在时返回 {@code null}
|
||||
*/
|
||||
private SysUserVo loadUserByPhoneNumber(String phoneNumber) {
|
||||
SysUserVo user = userMapper.lambda()
|
||||
return userMapper.lambda()
|
||||
.eq(SysUser::getPhoneNumber, phoneNumber)
|
||||
.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);
|
||||
}
|
||||
return user;
|
||||
|
||||
/**
|
||||
* 根据手机号注册新用户。
|
||||
*
|
||||
* @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");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ spring.data:
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# redis 密码必须配置
|
||||
# password: ruoyi123
|
||||
password: 123456
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
# 是否开启ssl
|
||||
@@ -227,9 +227,9 @@ sms:
|
||||
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
|
||||
access-key-id: 您的accessKey
|
||||
# 称为accessSecret有些称之为apiSecret
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
access-key-secret: 9jea9cuubXCEWlMXEd48ye2FCWQQPf
|
||||
signature: 深圳市聚心时代科技
|
||||
sdk-app-id: LTAI5t8c9LHzr2oUVoKw5yEX
|
||||
config2:
|
||||
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: tencent
|
||||
@@ -238,6 +238,18 @@ sms:
|
||||
signature: 您的短信签名
|
||||
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:
|
||||
|
||||
@@ -264,15 +264,15 @@ mqtt.client:
|
||||
# 是否开启客户端,默认:true
|
||||
enabled: true
|
||||
# 连接的服务端 ip ,默认:127.0.0.1
|
||||
ip: ssl://www.mmipco.cn
|
||||
ip: 192.168.1.249
|
||||
# 端口:默认:1883
|
||||
port: 8883
|
||||
port: 1883
|
||||
# 客户端名称
|
||||
name: Mqtt-Client
|
||||
name: Mqtt_bird_monitoring
|
||||
# 客户端Id(非常重要,一般为设备 sn,不可重复)
|
||||
client-id: 000001
|
||||
username: admin
|
||||
password: 61e6062129e9
|
||||
client-id: Mqttbirdmonitoring001
|
||||
# username: admin
|
||||
# password: 61e6062129e9
|
||||
# 超时时间,单位:秒,默认:5秒
|
||||
timeout: 5
|
||||
# 重连时间,默认 5000 毫秒
|
||||
@@ -291,7 +291,7 @@ mqtt.client:
|
||||
mqtt-executor-size: 2
|
||||
# 是否开启 ssl
|
||||
ssl:
|
||||
enabled: true
|
||||
enabled: false
|
||||
keystore-path:
|
||||
keystore-pass:
|
||||
truststore-path:
|
||||
|
||||
@@ -23,11 +23,27 @@
|
||||
<artifactId>sms4j-spring-boot-starter</artifactId>
|
||||
</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-->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
org.dromara.common.sms.config.SmsAutoConfiguration
|
||||
org.dromara.common.sms.config.AliyunInternationalSmsAutoConfiguration
|
||||
|
||||
Reference in New Issue
Block a user