diff --git a/pom.xml b/pom.xml index 08dfa3a..699c78a 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,8 @@ 2.48.1 3.3.5 + + 1.0.12 1.8.9 2.16.1.2 @@ -259,6 +261,20 @@ ${sms4j.version} + + + com.aliyun + dysmsapi20180501 + ${aliyun-dysmsapi.version} + + + + + com.aliyun + tea-openapi + 0.3.10 + + de.codecentric diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 3377ef2..a608da7 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -62,6 +62,18 @@ ruoyi-common-mail + + + org.dromara + ruoyi-common-sms + + + + + com.aliyun + dysmsapi20180501 + + org.dromara diff --git a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java index 9844b3f..d9a34cb 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java @@ -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 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 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(); + } + /** * 发送邮箱验证码 diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java index d938a40..6f686b4 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java @@ -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); + } + + /** + * 根据手机号注册新用户。 + * + * @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; } } diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index bb04b95..0fcb417 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -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: diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index f0582be..cd363b5 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -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: diff --git a/ruoyi-common/ruoyi-common-sms/pom.xml b/ruoyi-common/ruoyi-common-sms/pom.xml index 1ca17a7..3c802f8 100644 --- a/ruoyi-common/ruoyi-common-sms/pom.xml +++ b/ruoyi-common/ruoyi-common-sms/pom.xml @@ -23,11 +23,27 @@ sms4j-spring-boot-starter + + + com.aliyun + dysmsapi20180501 + + + + + com.aliyun + tea-openapi + + org.dromara ruoyi-common-redis + + com.mysql + mysql-connector-j + diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/AliyunInternationalSmsAutoConfiguration.java b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/AliyunInternationalSmsAutoConfiguration.java new file mode 100644 index 0000000..65b8294 --- /dev/null +++ b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/AliyunInternationalSmsAutoConfiguration.java @@ -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); + } + +} diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/AliyunInternationalSmsProperties.java b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/AliyunInternationalSmsProperties.java new file mode 100644 index 0000000..595ee20 --- /dev/null +++ b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/config/AliyunInternationalSmsProperties.java @@ -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; + +} diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/AliyunInternationalSmsService.java b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/AliyunInternationalSmsService.java new file mode 100644 index 0000000..948c9ae --- /dev/null +++ b/ruoyi-common/ruoyi-common-sms/src/main/java/org/dromara/common/sms/core/AliyunInternationalSmsService.java @@ -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()); + } + } + +} diff --git a/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 5919ce3..6bc495b 100644 --- a/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ org.dromara.common.sms.config.SmsAutoConfiguration +org.dromara.common.sms.config.AliyunInternationalSmsAutoConfiguration