weblog/doc/5、整合 SaToken 实现 JWT 登录功能/5.4 获取手机短信验证码接口开发.md
2025-02-17 10:05:44 +08:00

11 KiB
Raw Blame History

本小节中,正式进入到第一个业务接口的开发工作 —— 获取短信验证码接口。如下图所示,小红书是支持通过验证码来直接登录的:

业务逻辑设计

我们来分析一下该接口的业务逻辑要如何写?流程图如下:

解释一波逻辑:

  • 前端将手机号作为入参,请求获取短信验证码接口;
  • 后端拿到手机号,根据手机号构建 Redis Key , 如 verification_code:18012349108;
  • 查询 Redis , 判断该 Key 值是否存在;
  • 若已经存在说明验证码还在有效期内设置了3分钟有效期并提示用户请求验证码太过频繁
  • 若不存在,则生成 6 位随机数字作为验证码;
  • 调用第三方短信发送服务,比如阿里云的,将验证码发送到用户手机上;
  • 同时,将该验证码存储到 Redis 中,过期时间为 3 分钟,用于后续用户点击登录时,判断填写的验证码和缓存中的是否一致,以及判断用户获取验证码是否太过频繁。

接口定义

业务逻辑设计完毕后,接下来,定义一下此接口的请求地址、入参,以及出参。

接口地址

POST /verification/code/send

入参

{
    "phone": "18019939108" // 手机号
}

出参

{
	"success": false,
	"message": "请求太频繁请3分钟后再试",
	"errorCode": "AUTH-20000",
	"data": null
}

新建入参 VO

开始动手编码。编辑 xiaohashu-auth 认证服务,添加包 /model/vo/verificationcode , 并创建 SendVerificationCodeReqVO 入参实体类,代码如下:

package com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SendVerificationCodeReqVO {

    @NotBlank(message = "手机号不能为空")
    private String phone;

}

添加 guava、hutool、commons-lang3 工具

接着,编辑项目最外层的 pom.xml , 将一些比较流行的工具包依赖整合到项目中,如 guavahutoolcommongs-lang3 , 比如等会就将使用 hutool 快捷的生成 6 位数字验证码,代码如下:

    <properties>
		// 省略...
		
        <guava.version>33.0.0-jre</guava.version>
        <hutool.version>5.8.26</hutool.version>
        <commons-lang3.version>3.12.0</commons-lang3.version>
    </properties>

    <!-- 统一依赖管理 -->
    <dependencyManagement>
        <dependencies>
			// 省略...

            <!-- 相关工具类 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${guava.version}</version>
            </dependency>

            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.version}</version>
            </dependency>

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    // 省略...

版本号声明完毕后,编辑 xiaoha-common 模块的 pom.xml , 引入这些工具类:

    <dependencies>
		// 省略...

        <!-- 相关工具类 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>

添加完毕后,记得刷新一下 Maven 将相关包下载到本地仓库中。

定义 Redis Key 常量类

回到认证服务中,添加 /constant 常量类包,并创建 RedisKeyConstants 常量类,用于统一管理 Redis Key代码如下:

package com.quanxiaoha.xiaohashu.auth.constant;

/**
 * @author: 犬小哈
 * @date: 2024/5/21 15:04
 * @version: v1.0.0
 * @description: TODO
 **/
public class RedisKeyConstants {

    /**
     * 验证码 KEY 前缀
     */
    private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";

    /**
     * 构建验证码 KEY
     * @param phone
     * @return
     */
    public static String buildVerificationCodeKey(String phone) {
        return VERIFICATION_CODE_KEY_PREFIX + phone;
    }
}
  • 定义一个短信验证码 key 的前缀:verification_code: ;
  • 并封装一个拼接验证码完整 key 的静态构建方法,入参为手机号,方便等会在业务层中便捷的生成 key

定义获取短信验证码频繁错误码

编辑 ResponseCodeEnum 全局错误枚举类,添加 请求太频繁请3分钟后再试 对应的错误码枚举值,代码如下:

package com.quanxiaoha.xiaohashu.auth.enums;

import com.quanxiaoha.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @date: 2023-08-15 10:33
 * @description: 响应异常码
 **/
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {

	// 省略...

    // ----------- 业务异常状态码 -----------
    VERIFICATION_CODE_SEND_FREQUENTLY("AUTH-20000", "请求太频繁请3分钟后再试"),
    ;

    // 异常码
    private final String errorCode;
    // 错误信息
    private final String errorMessage;

}

编写 service 业务代码

以上准备工作完成后,开始编写业务层代码。首先,添加 /service/impl 包,并创建 VerificationCodeService 业务接口,以及其实现类:

package com.quanxiaoha.xiaohashu.auth.service;

import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;

public interface VerificationCodeService {

    /**
     * 发送短信验证码
     *
     * @param sendVerificationCodeReqVO
     * @return
     */
    Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO);
}

业务接口中定义一个发送短信验证码的方法。

package com.quanxiaoha.xiaohashu.auth.service.impl;

import cn.hutool.core.util.RandomUtil;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.auth.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.auth.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import com.quanxiaoha.xiaohashu.auth.service.VerificationCodeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 发送短信验证码
     *
     * @param sendVerificationCodeReqVO
     * @return
     */
    @Override
    public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
        // 手机号
        String phone = sendVerificationCodeReqVO.getPhone();

        // 构建验证码 redis key
        String key = RedisKeyConstants.buildVerificationCodeKey(phone);

        // 判断是否已发送验证码
        boolean isSent = redisTemplate.hasKey(key);
        if (isSent) {
            // 若之前发送的验证码未过期,则提示发送频繁
            throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
        }

        // 生成 6 位随机数字验证码
        String verificationCode = RandomUtil.randomNumbers(6);

        // todo: 调用第三方短信发送服务

        log.info("==> 手机号: {}, 已发送验证码:【{}】", phone, verificationCode);

        // 存储验证码到 redis, 并设置过期时间为 3 分钟
        redisTemplate.opsForValue().set(key, verificationCode, 3, TimeUnit.MINUTES);

        return Response.success();
    }
}

业务逻辑已经在文章开头说明过了,不再赘述。需要注意的是,调用第三方短信发送服务的逻辑,这里先写个 todo , 等后续小节中再单独讲解如何接入。

编写 controller 控制层

最后,创建 /controller 包,并添加 VerificationCodeController 控制器以及定义 /verification/code/send 接口,代码如下:

package com.quanxiaoha.xiaohashu.auth.controller;

import com.quanxiaoha.framework.biz.operationlog.aspect.ApiOperationLog;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import com.quanxiaoha.xiaohashu.auth.service.VerificationCodeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Slf4j
public class VerificationCodeController {

    @Resource
    private VerificationCodeService verificationCodeService;

    @PostMapping("/verification/code/send")
    @ApiOperationLog(description = "发送短信验证码")
    public Response<?> send(@Validated @RequestBody SendVerificationCodeReqVO sendVerificationCodeReqVO) {
        return verificationCodeService.send(sendVerificationCodeReqVO);
    }

}

自测一波

重启项目,通过 Apipost 工具来测试一下接口功能是否正常。如下图所示:

填写对应的手机号,点击发送按钮,可以看到响参提示 success 成功。再查看后台控制台日志,成功打印了生成了的验证码:

连接到 Redis 中,验证一下对应的 Key 是否存在,以及值是否和控制台日志中的验证码一样,如下图所示,是没有问题的,同时右上角的 ttl 值就是该 Key 值还剩多长时间过期,过期后该 Key 会被自动删除:

如果 Redis 中,该 Key 还未过期,再次请求获取验证码接口,也会正常提示【请求太频繁请3分钟后再试】信息:

OK, 至此 ,获取手机短信验证码接口的大体逻辑,就开发完成啦~

本小节源码下载

https://t.zsxq.com/vt5iP