Commit 71b9104a13fddcd03d37e49c76b9e42f5c3db379

Authored by YunaiV
1 parent d784b113

增加短信验证码的功能

Showing 17 changed files with 255 additions and 16 deletions
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.http
... ... @@ -6,3 +6,12 @@ Content-Type: application/json
6 6 "mobile": "15601691300",
7 7 "password": "admin123"
8 8 }
  9 +
  10 +### 请求 /send-sms-code 接口 => 成功
  11 +POST {{userServerUrl}}/send-sms-code
  12 +Content-Type: application/json
  13 +
  14 +{
  15 + "mobile": "15601691300",
  16 + "scene": 1
  17 +}
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/SysAuthController.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.userserver.modules.system.controller.auth;
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.userserver.modules.system.controller.auth.vo.*;
5 5 import cn.iocoder.yudao.userserver.modules.system.service.auth.SysAuthService;
  6 +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
6 7 import io.swagger.annotations.Api;
7 8 import io.swagger.annotations.ApiImplicitParam;
8 9 import io.swagger.annotations.ApiImplicitParams;
... ... @@ -27,6 +28,8 @@ public class SysAuthController {
27 28  
28 29 @Resource
29 30 private SysAuthService authService;
  31 + @Resource
  32 + private SysSmsCodeService smsCodeService;
30 33  
31 34 @PostMapping("/login")
32 35 @ApiOperation("使用手机 + 密码登录")
... ... @@ -45,10 +48,8 @@ public class SysAuthController {
45 48 @PostMapping("/send-sms-code")
46 49 @ApiOperation("发送手机验证码")
47 50 public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid MbrAuthSendSmsReqVO reqVO) {
48   -// passportManager.sendSmsCode(sendSmsCodeDTO, HttpUtil.getIp(request));
49   -// // 返回成功
50   -// return success(true);
51   - return null;
  51 + smsCodeService.sendSmsCode(reqVO.getMobile(), reqVO.getScene(), getClientIP());
  52 + return success(true);
52 53 }
53 54  
54 55 @PostMapping("/reset-password")
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/controller/auth/vo/MbrAuthSendSmsReqVO.java
1 1 package cn.iocoder.yudao.userserver.modules.system.controller.auth.vo;
2 2  
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
3 4 import cn.iocoder.yudao.framework.common.validation.Mobile;
  5 +import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
4 6 import io.swagger.annotations.ApiModel;
5 7 import io.swagger.annotations.ApiModelProperty;
6 8 import lombok.Data;
... ... @@ -19,6 +21,7 @@ public class MbrAuthSendSmsReqVO {
19 21  
20 22 @ApiModelProperty(value = "发送场景", example = "1", notes = "对应 MbrSmsSceneEnum 枚举")
21 23 @NotNull(message = "发送场景不能为空")
  24 + @InEnum(SysSmsSceneEnum.class)
22 25 private Integer scene;
23 26  
24 27 }
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/dataobject/package-info.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.dal.dataobject;
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/dal/mysql/sms/MbrSmsCodeDO.java renamed to yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/dataobject/sms/SysSmsCodeDO.java
1   -package cn.iocoder.yudao.userserver.modules.member.dal.mysql.sms;
  1 +package cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import com.baomidou.mybatisplus.annotation.TableName;
5   -import lombok.Data;
6   -import lombok.EqualsAndHashCode;
  5 +import lombok.*;
7 6 import lombok.experimental.Accessors;
8 7  
9 8 import java.util.Date;
... ... @@ -15,11 +14,13 @@ import java.util.Date;
15 14 *
16 15 * @author 芋道源码
17 16 */
18   -@TableName("mbr_sms_code")
  17 +@TableName("sys_sms_code")
19 18 @Data
20 19 @EqualsAndHashCode(callSuper = true)
21   -@Accessors(chain = true)
22   -public class MbrSmsCodeDO extends BaseDO {
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class SysSmsCodeDO extends BaseDO {
23 24  
24 25 /**
25 26 * 编号
... ... @@ -36,7 +37,7 @@ public class MbrSmsCodeDO extends BaseDO {
36 37 /**
37 38 * 发送场景
38 39 *
39   - * 枚举 {@link MbrSmsCodeDO}
  40 + * 枚举 {@link SysSmsCodeDO}
40 41 */
41 42 private Integer scene;
42 43 /**
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/mysql/package-info.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.dal.mysql;
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/mysql/sms/SysSmsCodeMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.dal.mysql.sms;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
  5 +import cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms.SysSmsCodeDO;
  6 +import org.apache.ibatis.annotations.Mapper;
  7 +
  8 +@Mapper
  9 +public interface SysSmsCodeMapper extends BaseMapperX<SysSmsCodeDO> {
  10 +
  11 + /**
  12 + * 获得手机号的最后一个手机验证码
  13 + *
  14 + * @param mobile 手机号
  15 + * @param scene 发送场景,选填
  16 + * @return 手机验证码
  17 + */
  18 + default SysSmsCodeDO selectLastByMobile(String mobile, Integer scene) {
  19 + return selectOne(new QueryWrapperX<SysSmsCodeDO>()
  20 + .eq("mobile", mobile)
  21 + .eqIfPresent("scene", scene)
  22 + .orderByDesc("id")
  23 + .last("LIMIT 1"));
  24 + }
  25 +
  26 +}
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/dal/redis/package-info.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.dal.redis;
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/SysErrorCodeConstants.java
... ... @@ -14,4 +14,11 @@ public interface SysErrorCodeConstants {
14 14 ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1005000001, "登录失败,账号被禁用");
15 15 ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1005000002, "登录失败"); // 登录失败的兜底,未知原因
16 16  
  17 + // ========== SMS CODE 模块 1005001000 ==========
  18 + ErrorCode USER_SMS_CODE_NOT_FOUND = new ErrorCode(1005001000, "验证码不存在");
  19 + ErrorCode USER_SMS_CODE_EXPIRED = new ErrorCode(1005001001, "验证码已过期");
  20 + ErrorCode USER_SMS_CODE_USED = new ErrorCode(1005001002, "验证码已使用");
  21 + ErrorCode USER_SMS_CODE_NOT_CORRECT = new ErrorCode(1005001003, "验证码不正确");
  22 + ErrorCode USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1005001004, "超过每日短信发送数量");
  23 + ErrorCode USER_SMS_CODE_SEND_TOO_FAST = new ErrorCode(1005001005, "短信发送过于频率");
17 24 }
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/enums/sms/MbrSmsSceneEnum.java renamed to yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/enums/sms/SysSmsSceneEnum.java
1   -package cn.iocoder.yudao.userserver.modules.member.enums.sms;
  1 +package cn.iocoder.yudao.userserver.modules.system.enums.sms;
2 2  
3 3 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4 4 import lombok.AllArgsConstructor;
... ... @@ -13,13 +13,13 @@ import java.util.Arrays;
13 13 */
14 14 @Getter
15 15 @AllArgsConstructor
16   -public enum MbrSmsSceneEnum implements IntArrayValuable {
  16 +public enum SysSmsSceneEnum implements IntArrayValuable {
17 17  
18 18 LOGIN_BY_SMS(1, "手机号登陆"),
19 19 CHANGE_MOBILE_BY_SMS(2, "更换手机号"),
20 20 ;
21 21  
22   - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MbrSmsSceneEnum::getScene).toArray();
  22 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SysSmsSceneEnum::getScene).toArray();
23 23  
24 24 private final Integer scene;
25 25 private final String name;
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/framework/package-info.java 0 → 100644
  1 +/**
  2 + * 属于 system 模块的 framework 封装
  3 + *
  4 + * @author 芋道源码
  5 + */
  6 +package cn.iocoder.yudao.userserver.modules.system.framework;
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/framework/sms/SmsCodeConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.framework.sms;
  2 +
  3 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  4 +import org.springframework.context.annotation.Configuration;
  5 +
  6 +@Configuration
  7 +@EnableConfigurationProperties(SmsCodeProperties.class)
  8 +public class SmsCodeConfiguration {
  9 +}
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/framework/sms/SmsCodeProperties.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.framework.sms;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.validation.annotation.Validated;
  6 +
  7 +import javax.validation.constraints.NotEmpty;
  8 +import javax.validation.constraints.NotNull;
  9 +import java.time.Duration;
  10 +import java.util.Collection;
  11 +
  12 +@ConfigurationProperties(prefix = "yudao.sms-code")
  13 +@Validated
  14 +@Data
  15 +public class SmsCodeProperties {
  16 +
  17 + /**
  18 + * 过期时间
  19 + */
  20 + @NotNull(message = "过期时间不能为空")
  21 + private Duration expireTimes;
  22 + /**
  23 + * 短信发送频率
  24 + */
  25 + @NotNull(message = "短信发送频率不能为空")
  26 + private Duration sendFrequency;
  27 + /**
  28 + * 每日发送最大数量
  29 + */
  30 + @NotNull(message = "每日发送最大数量不能为空")
  31 + private Integer sendMaximumQuantityPerDay;
  32 + /**
  33 + * 验证码最小值
  34 + */
  35 + @NotNull(message = "验证码最小值不能为空")
  36 + private Integer beginCode;
  37 + /**
  38 + * 验证码最大值
  39 + */
  40 + @NotNull(message = "验证码最大值不能为空")
  41 + private Integer endCode;
  42 +
  43 +}
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/SysSmsCodeService.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.service.sms;
  2 +
  3 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  4 +import cn.iocoder.yudao.framework.common.validation.Mobile;
  5 +import cn.iocoder.yudao.userserver.modules.system.enums.sms.SysSmsSceneEnum;
  6 +
  7 +/**
  8 + * 短信验证码 Service 接口
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +public interface SysSmsCodeService {
  13 +
  14 + /**
  15 + * 创建短信验证码,并进行发送
  16 + *
  17 + * @param mobile 手机号
  18 + * @param scene 发送场景 {@link SysSmsSceneEnum}
  19 + * @param createIp 发送 IP
  20 + */
  21 + void sendSmsCode(@Mobile String mobile, Integer scene, String createIp);
  22 +
  23 + /**
  24 + * 验证短信验证码,并进行使用
  25 + * 如果正确,则将验证码标记成已使用
  26 + * 如果错误,则抛出 {@link ServiceException} 异常
  27 + *
  28 + * @param mobile 手机号
  29 + * @param scene 发送场景
  30 + * @param code 验证码
  31 + * @param usedIp 使用 IP
  32 + */
  33 + void useSmsCode(@Mobile String mobile, Integer scene, String code, String usedIp);
  34 +
  35 +}
... ...
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/system/service/sms/impl/SysSmsCodeServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.userserver.modules.system.service.sms.impl;
  2 +
  3 +import cn.iocoder.yudao.userserver.modules.system.dal.dataobject.sms.SysSmsCodeDO;
  4 +import cn.iocoder.yudao.userserver.modules.system.dal.mysql.sms.SysSmsCodeMapper;
  5 +import cn.iocoder.yudao.userserver.modules.system.framework.sms.SmsCodeProperties;
  6 +import cn.iocoder.yudao.userserver.modules.system.service.sms.SysSmsCodeService;
  7 +import org.springframework.stereotype.Service;
  8 +import org.springframework.validation.annotation.Validated;
  9 +
  10 +import javax.annotation.Resource;
  11 +import java.util.Date;
  12 +
  13 +import static cn.hutool.core.util.RandomUtil.randomInt;
  14 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  15 +import static cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants.*;
  16 +
  17 +/**
  18 + * 短信验证码 Service 实现类
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +@Service
  23 +@Validated
  24 +public class SysSmsCodeServiceImpl implements SysSmsCodeService {
  25 +
  26 + @Resource
  27 + private SmsCodeProperties smsCodeProperties;
  28 +
  29 + @Resource
  30 + private SysSmsCodeMapper smsCodeMapper;
  31 +
  32 + @Override
  33 + public void sendSmsCode(String mobile, Integer scene, String createIp) {
  34 + // 创建验证码
  35 + String code = this.createSmsCode(mobile, scene, createIp);
  36 + // 发送验证码
  37 + // TODO 芋艿:重要,发送短信验证码
  38 + }
  39 +
  40 + private String createSmsCode(String mobile, Integer scene, String ip) {
  41 + // 校验是否可以发送验证码,不用筛选场景
  42 + SysSmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null);
  43 + if (lastSmsCode != null) {
  44 + if (lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。
  45 + throw exception(USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
  46 + }
  47 + if (System.currentTimeMillis() - lastSmsCode.getCreateTime().getTime()
  48 + < smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁
  49 + throw exception(USER_SMS_CODE_SEND_TOO_FAST);
  50 + }
  51 + // TODO 芋艿:提升,每个 IP 每天可发送数量
  52 + // TODO 芋艿:提升,每个 IP 每小时可发送数量
  53 + }
  54 +
  55 + // 创建验证码记录
  56 + String code = String.valueOf(randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1));
  57 + SysSmsCodeDO newSmsCode = SysSmsCodeDO.builder().mobile(mobile).code(code)
  58 + .scene(scene).todayIndex(lastSmsCode != null ? lastSmsCode.getTodayIndex() + 1 : 1)
  59 + .createIp(ip).used(false).build();
  60 + smsCodeMapper.insert(newSmsCode);
  61 + return code;
  62 + }
  63 +
  64 + @Override
  65 + public void useSmsCode(String mobile, Integer scene, String code, String usedIp) {
  66 + // 校验验证码
  67 + SysSmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, scene);
  68 + if (lastSmsCode == null) { // 若验证码不存在,抛出异常
  69 + throw exception(USER_SMS_CODE_NOT_FOUND);
  70 + }
  71 + if (System.currentTimeMillis() - lastSmsCode.getCreateTime().getTime()
  72 + >= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期
  73 + throw exception(USER_SMS_CODE_EXPIRED);
  74 + }
  75 + if (lastSmsCode.getUsed()) { // 验证码已使用
  76 + throw exception(USER_SMS_CODE_USED);
  77 + }
  78 + if (!lastSmsCode.getCode().equals(code)) {
  79 + throw exception(USER_SMS_CODE_NOT_CORRECT);
  80 + }
  81 +
  82 + // 使用验证码
  83 + smsCodeMapper.updateById(SysSmsCodeDO.builder().id(lastSmsCode.getId())
  84 + .used(true).usedTime(new Date()).usedIp(usedIp).build());
  85 + }
  86 +
  87 +}
... ...
yudao-user-server/src/main/resources/application.yaml
... ... @@ -58,5 +58,13 @@ yudao:
58 58 db-schemas: ${spring.datasource.dynamic.datasource.master.name}
59 59 error-code: # 错误码相关配置项
60 60 constants-class-list:
  61 + - cn.iocoder.yudao.userserver.modules.member.enums.MbrErrorCodeConstants
  62 + - cn.iocoder.yudao.userserver.modules.system.enums.SysErrorCodeConstants
  63 + sms-code: # 短信验证码相关的配置项
  64 + expire-times: 10m
  65 + send-frequency: 1m
  66 + send-maximum-quantity-per-day: 10
  67 + begin-code: 9999 # 这里配置 9999 的原因是,测试方便。
  68 + end-code: 9999 # 这里配置 9999 的原因是,测试方便。
61 69  
62 70 debug: false
... ...
更新日志.md
... ... @@ -10,13 +10,14 @@
10 10 ## [v1.1.1] 待定
11 11  
12 12 * 支付
  13 +* 用户前台的社交登陆
13 14  
14   -## [v1.1.0] 待定
  15 +## [v1.1.0] 进行中
15 16  
16 17 * 新增管理后台的企业微信、钉钉等社交登录
17 18 * 新增用户前台(例如说,用户使用的小程序)的后端项目 `yudao-user-server`
18 19 * 新增公共服务 `yudao-core-service` 项目,通过 Jar 包的方式,提供 `yudao-user-server` 和 `yudao-admin-server` 的共享逻辑的复用。
19   -* 新增用户前台的手机登录、验证码登录、TODO
  20 +* 新增用户前台的手机登录、验证码登录
20 21 * 修复管理后台的用户头像上传 404 的问题
21 22  
22 23 ## [v1.0.0] 2021.05.03
... ...