mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 10:57:13 +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