feat: 支持手机号登录(演示环境不开放)

1.在个人中心-安全设置中绑手机号后,才支持手机号登录
2.SMS4J(短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程)
This commit is contained in:
2023-10-27 21:32:25 +08:00
parent 2f2905efdc
commit 4d70bc84db
27 changed files with 780 additions and 126 deletions

View File

@@ -34,6 +34,7 @@ import cn.hutool.core.bean.BeanUtil;
import top.charles7c.cnadmin.auth.model.request.AccountLoginRequest;
import top.charles7c.cnadmin.auth.model.request.EmailLoginRequest;
import top.charles7c.cnadmin.auth.model.request.PhoneLoginRequest;
import top.charles7c.cnadmin.auth.model.vo.LoginVO;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
import top.charles7c.cnadmin.auth.model.vo.UserInfoVO;
@@ -96,6 +97,20 @@ public class AuthController {
return LoginVO.builder().token(token).build();
}
@SaIgnore
@Operation(summary = "手机号登录", description = "根据手机号和验证码进行登录认证")
@PostMapping("/phone")
public LoginVO phoneLogin(@Validated @RequestBody PhoneLoginRequest loginRequest) {
String phone = loginRequest.getPhone();
String captchaKey = RedisUtils.formatKey(CacheConsts.CAPTCHA_KEY_PREFIX, phone);
String captcha = RedisUtils.getCacheObject(captchaKey);
ValidationUtils.throwIfBlank(captcha, "验证码已失效");
ValidationUtils.throwIfNotEqualIgnoreCase(loginRequest.getCaptcha(), captcha, "验证码错误");
RedisUtils.deleteCacheObject(captchaKey);
String token = loginService.phoneLogin(phone);
return LoginVO.builder().token(token).build();
}
@SaIgnore
@Operation(summary = "用户退出", description = "注销用户的当前登录")
@Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx",

View File

@@ -17,6 +17,8 @@
package top.charles7c.cnadmin.webapi.controller.common;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.mail.MessagingException;
import javax.validation.constraints.NotBlank;
@@ -27,6 +29,10 @@ import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -36,6 +42,7 @@ import com.wf.captcha.base.Captcha;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
@@ -90,22 +97,47 @@ public class CaptchaController {
String captchaKeyPrefix = CacheConsts.CAPTCHA_KEY_PREFIX;
String limitCaptchaKey = RedisUtils.formatKey(limitKeyPrefix, captchaKeyPrefix, email);
long limitTimeInMillisecond = RedisUtils.getTimeToLive(limitCaptchaKey);
CheckUtils.throwIf(limitTimeInMillisecond > 0, "发送邮箱验证码过于频繁,请您 {}s 后再试", limitTimeInMillisecond / 1000);
CheckUtils.throwIf(limitTimeInMillisecond > 0, "发送验证码过于频繁,请您 {}s 后再试", limitTimeInMillisecond / 1000);
// 生成验证码
CaptchaProperties.CaptchaMail captchaMail = captchaProperties.getMail();
String captcha = RandomUtil.randomNumbers(captchaMail.getLength());
// 发送验证码
Long expirationInMinutes = captchaMail.getExpirationInMinutes();
String content = TemplateUtils.render(captchaMail.getTemplatePath(),
Dict.create().set("captcha", captcha).set("expiration", expirationInMinutes));
MailUtils.sendHtml(email, String.format("【%s】邮箱验证码", projectProperties.getName()), content);
// 保存验证码
String captchaKey = RedisUtils.formatKey(captchaKeyPrefix, email);
RedisUtils.setCacheObject(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes));
RedisUtils.setCacheObject(limitCaptchaKey, captcha, Duration.ofSeconds(captchaMail.getLimitInSeconds()));
return R.ok(String.format("发送成功,验证码有效期 %s 分钟", expirationInMinutes));
}
@Operation(summary = "获取短信验证码", description = "发送验证码到指定手机号")
@GetMapping("/sms")
public R getSmsCaptcha(
@NotBlank(message = "手机号不能为空") @Pattern(regexp = RegexConsts.MOBILE, message = "手机号格式错误") String phone) {
String limitKeyPrefix = CacheConsts.LIMIT_KEY_PREFIX;
String captchaKeyPrefix = CacheConsts.CAPTCHA_KEY_PREFIX;
String limitCaptchaKey = RedisUtils.formatKey(limitKeyPrefix, captchaKeyPrefix, phone);
long limitTimeInMillisecond = RedisUtils.getTimeToLive(limitCaptchaKey);
CheckUtils.throwIf(limitTimeInMillisecond > 0, "发送验证码过于频繁,请您 {}s 后再试", limitTimeInMillisecond / 1000);
// 生成验证码
CaptchaProperties.CaptchaSms captchaSms = captchaProperties.getSms();
String captcha = RandomUtil.randomNumbers(captchaSms.getLength());
// 发送验证码
Long expirationInMinutes = captchaSms.getExpirationInMinutes();
SmsBlend smsBlend = SmsFactory.getBySupplier(SupplierConstant.CLOOPEN);
Map<String, String> messageMap = MapUtil.newHashMap(2, true);
messageMap.put("captcha", captcha);
messageMap.put("expirationInMinutes", String.valueOf(expirationInMinutes));
SmsResponse smsResponse =
smsBlend.sendMessage(phone, captchaSms.getTemplateId(), (LinkedHashMap<String, String>)messageMap);
CheckUtils.throwIf(!smsResponse.isSuccess(), "验证码发送失败");
// 保存验证码
String captchaKey = RedisUtils.formatKey(captchaKeyPrefix, phone);
RedisUtils.setCacheObject(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes));
RedisUtils.setCacheObject(limitCaptchaKey, captcha, Duration.ofSeconds(captchaSms.getLimitInSeconds()));
return R.ok(String.format("发送成功,验证码有效期 %s 分钟", expirationInMinutes));
}
}

View File

@@ -49,6 +49,7 @@ import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
import top.charles7c.cnadmin.system.model.request.UserBasicInfoUpdateRequest;
import top.charles7c.cnadmin.system.model.request.UserEmailUpdateRequest;
import top.charles7c.cnadmin.system.model.request.UserPasswordUpdateRequest;
import top.charles7c.cnadmin.system.model.request.UserPhoneUpdateRequest;
import top.charles7c.cnadmin.system.model.vo.AvatarVO;
import top.charles7c.cnadmin.system.model.vo.UserSocialBindVO;
import top.charles7c.cnadmin.system.service.UserService;
@@ -106,6 +107,21 @@ public class UserCenterController {
return R.ok("修改成功");
}
@Operation(summary = "修改手机号", description = "修改手机号")
@PatchMapping("/phone")
public R updatePhone(@Validated @RequestBody UserPhoneUpdateRequest updateRequest) {
String rawCurrentPassword =
ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateRequest.getCurrentPassword()));
ValidationUtils.throwIfBlank(rawCurrentPassword, "当前密码解密失败");
String captchaKey = RedisUtils.formatKey(CacheConsts.CAPTCHA_KEY_PREFIX, updateRequest.getNewPhone());
String captcha = RedisUtils.getCacheObject(captchaKey);
ValidationUtils.throwIfBlank(captcha, "验证码已失效");
ValidationUtils.throwIfNotEqualIgnoreCase(updateRequest.getCaptcha(), captcha, "验证码错误");
RedisUtils.deleteCacheObject(captchaKey);
userService.updatePhone(updateRequest.getNewPhone(), rawCurrentPassword, LoginHelper.getUserId());
return R.ok("修改成功");
}
@Operation(summary = "修改邮箱", description = "修改用户邮箱")
@PatchMapping("/email")
public R updateEmail(@Validated @RequestBody UserEmailUpdateRequest updateRequest) {

View File

@@ -93,6 +93,19 @@ justauth:
cache:
type: custom
--- ### 短信配置
sms:
# 从 YAML 读取配置
config-type: yaml
blends:
cloopen:
# 短信厂商
supplier: cloopen
base-url: https://app.cloopen.com:8883/2013-12-26
access-key-id: 你的Access Key
access-key-secret: 你的Access Key Secret
sdk-app-id: 你的应用ID
--- ### 邮件配置
spring.mail:
# 根据需要更换
@@ -133,6 +146,16 @@ captcha:
limitInSeconds: 60
# 模板路径
templatePath: mail/captcha.ftl
## 短信验证码配置
sms:
# 内容长度
length: 4
# 过期时间
expirationInMinutes: 5
# 限制时间
limitInSeconds: 60
# 模板 ID
templateId: 1
--- ### 安全配置-排除路径配置
security.excludes:

View File

@@ -95,6 +95,19 @@ justauth:
cache:
type: custom
--- ### 短信配置
sms:
# 从 YAML 读取配置
config-type: yaml
blends:
cloopen:
# 短信厂商
supplier: cloopen
base-url: https://app.cloopen.com:8883/2013-12-26
access-key-id: 你的Access Key
access-key-secret: 你的Access Key Secret
sdk-app-id: 你的应用ID
--- ### 邮件配置
spring.mail:
# 根据需要更换
@@ -135,6 +148,16 @@ captcha:
limitInSeconds: 60
# 模板路径
templatePath: mail/captcha.ftl
## 短信验证码配置
sms:
# 内容长度
length: 4
# 过期时间
expirationInMinutes: 5
# 限制时间
limitInSeconds: 60
# 模板 ID
templateId: 1
--- ### 安全配置-排除路径配置
security.excludes: