mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-13 14:57:16 +08:00
feat: 支持手机号登录(演示环境不开放)
1.在个人中心-安全设置中绑手机号后,才支持手机号登录 2.SMS4J(短信聚合框架,轻松集成多家短信服务,解决接入多个短信 SDK 的繁琐流程)
This commit is contained in:
@@ -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",
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user