Skip to content

腾讯云短信登录注册 ​

概述 ​

本指南旨在详细介绍如何实现基于短信验证码的用户登录和注册功能。通过使用腾讯云短信服务,系统能够在用户成功登录后自动创建新用户。本文涵盖了从短信模板配置、数据库设计、短信发送工具类开发,到接口实现和验证逻辑的全流程,适用于 Java 开发环境。

配置短信模板 ​

在开始实现短信登录功能之前,首先需要在腾讯云完成短信模板的配置。具体的配置过程请参考腾讯云官方文档,这里假设您已经完成了模板的配置。

数据库设计 ​

为了存储和管理短信验证码信息,需要创建一个专门的数据库表 sys_sms_code。以下是表结构的详细设计:

sql
-- ----------------------------
-- Table structure for sys_sms_code
-- ----------------------------
CREATE TABLE `sys_sms_code` (
  `id` bigint(20) PRIMARY KEY COMMENT '主键',
  `area_code` varchar(8) DEFAULT NULL COMMENT '国家代码',
  `platform` varchar(32) NOT NULL COMMENT '实际短信发送平台:tencent、aliyun等',
  `phone` varchar(16) NOT NULL COMMENT '手机号码',
  `code` varchar(8) NOT NULL COMMENT '验证码',
  `valid_seconds` int(10) unsigned NOT NULL COMMENT '有效时间(秒)',
  `code_type` int(11) NOT NULL COMMENT '验证类型',
  `ip` varchar(32) DEFAULT NULL COMMENT '用户IP地址',
  `remark` VARCHAR(256),
  `creator` VARCHAR(64) DEFAULT '',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updater` VARCHAR(64) DEFAULT '',
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `deleted` SMALLINT DEFAULT 0,
  `tenant_id` BIGINT NOT NULL DEFAULT 0
) COMMENT='短信验证码表';

字段说明 ​

  • id: 唯一标识每条验证码记录。
  • area_code: 国家或地区代码,例如中国为 "86"。
  • platform: 后端实际使用的短信发送平台,例如 tencent、aliyun;不接受客户端任意指定。
  • phone: 用户的手机号码。
  • code: 发送的验证码。
  • valid_seconds: 验证码的有效时间,以秒为单位。
  • code_type: 验证码的类型,例如注册、登录等。
  • ip: 请求发送验证码的用户 IP 地址。
  • remark: 备注信息。
  • creator/updater: 创建者和更新者信息。
  • create_time/update_time: 记录的创建和更新时间。
  • deleted: 逻辑删除标识。
  • tenant_id: 租户 ID,用于多租户系统。

平台字段与历史表迁移 ​

sys_sms_code 是通用短信验证码表,增加 platform 可以记录发送来源并隔离不同平台的验证码。它不是用户注册来源,也不是验证码用途:code_type 为 1 注册、2 登录、3 绑定或更换手机号、4 重置密码;verification_type=3 表示采用短信验证。

用户可能先后使用不同平台,不应把短信发送平台覆盖到用户的身份来源字段。如果以后需要记录第三方账号身份,使用独立的账号关联表记录身份平台及该平台用户 ID。

本文沿用 MySQL 表结构与 SQL。已有表由用户手动执行以下增量 SQL,程序启动不自动修改表:

sql
ALTER TABLE sys_sms_code
  ADD COLUMN platform VARCHAR(32) NOT NULL DEFAULT 'unknown'
  COMMENT '实际短信发送平台:tencent、aliyun等';

旧记录无法确认来源时保留 unknown,不要批量假定为腾讯云。只有确认历史数据全部由腾讯云发送时,才手动将相应记录更新为 tencent。新发送记录必须显式写入真实平台;同步更新生成的 Model 或使用 set("platform", value)。新建表不设置平台默认值,以便尽早发现漏写。

校验应同时匹配记录 ID、租户、平台、手机号、用途和有效期;platform 不是唯一键。重发限流还应按手机号跨平台统计,避免切换平台绕过限流。验证码消费与账号修改应放在同一事务中,保证一次性使用。

短信发送工具类 ​

为了实现短信的发送功能,我们需要开发一个工具类 TencentSmsUtils,该类封装了与腾讯云短信服务的交互逻辑。

引入依赖 ​

首先,在项目的 pom.xml 中引入腾讯云短信服务的依赖:

xml
<properties>
  <qcloudsms.version>3.1.270</qcloudsms.version>
</properties>
<dependencies>
  <dependency>
    <groupId>com.tencentcloudapi</groupId>
    <artifactId>tencentcloud-sdk-java</artifactId>
    <version>${qcloudsms.version}</version>
  </dependency>
</dependencies>

环境配置 ​

腾讯云将访问凭据命名为 SecretId 和 SecretKey。密钥、应用 ID、签名、各用途模板 ID、区域和有效期均从 EnvUtils 读取,不再从业务配置表读取密钥,也不保留不使用的 appKey 字段。

在本地未提交的 .env 或部署环境中配置以下变量。空值需要替换为自己的配置,模板签名与参数顺序应与实际审核通过的模板一致。

properties
SMS_PLATFORM=tencent
TENCENT_SMS_SECRET_ID=
TENCENT_SMS_SECRET_KEY=
TENCENT_SMS_APP_ID=
TENCENT_SMS_SIGN_NAME=
TENCENT_SMS_REGISTER_TEMPLATE_ID=
TENCENT_SMS_LOGIN_TEMPLATE_ID=
TENCENT_SMS_BIND_PHONE_TEMPLATE_ID=
TENCENT_SMS_RESET_PASSWORD_TEMPLATE_ID=
TENCENT_SMS_REGION=ap-guangzhou
TENCENT_SMS_ENDPOINT=sms.tencentcloudapi.com
SMS_CODE_VALID_SECONDS=300

框架启动时完成环境加载;独立测试在读取前调用 EnvUtils.load()。不要在日志中输出配置对象或密钥。以下腾讯云实现只接受 SMS_PLATFORM=tencent;切换为阿里云还需要对应发送适配器,不能只改平台名称。

配置对象 TxSmsConfig ​

java
package com.example.auth.app.model;

import lombok.Data;

@Data
public class TxSmsConfig {
    private String platform;
    private String secretId;
    private String secretKey;
    private String appId;
    private String templateId;
    private String smsSign;
    private Integer validSeconds;
}

Service 层:SystemConfigService ​

java
package com.example.auth.app.services.system;

import com.example.auth.app.model.TxSmsConfig;
import nexus.io.tio.utils.environment.EnvUtils;

public class SystemConfigService {
    public TxSmsConfig getLoginSmsConfig() {
        return getSmsConfig(2);
    }

    public TxSmsConfig getSmsConfig(int codeType) {
        String platform = EnvUtils.get("SMS_PLATFORM", "tencent");
        if (!"tencent".equals(platform)) {
            throw new IllegalArgumentException("Unsupported SMS platform for TencentSmsService");
        }
        String templateKey;
        switch (codeType) {
        case 1:
            templateKey = "TENCENT_SMS_REGISTER_TEMPLATE_ID";
            break;
        case 2:
            templateKey = "TENCENT_SMS_LOGIN_TEMPLATE_ID";
            break;
        case 3:
            templateKey = "TENCENT_SMS_BIND_PHONE_TEMPLATE_ID";
            break;
        case 4:
            templateKey = "TENCENT_SMS_RESET_PASSWORD_TEMPLATE_ID";
            break;
        default:
            throw new IllegalArgumentException("Unsupported SMS code type");
        }
        int validSeconds = EnvUtils.getInt("SMS_CODE_VALID_SECONDS", 300);
        if (validSeconds <= 0) {
            throw new IllegalArgumentException("SMS_CODE_VALID_SECONDS must be positive");
        }
        TxSmsConfig config = new TxSmsConfig();
        config.setPlatform(platform);
        config.setSecretId(required("TENCENT_SMS_SECRET_ID"));
        config.setSecretKey(required("TENCENT_SMS_SECRET_KEY"));
        config.setAppId(required("TENCENT_SMS_APP_ID"));
        config.setSmsSign(required("TENCENT_SMS_SIGN_NAME"));
        config.setTemplateId(required(templateKey));
        config.setValidSeconds(validSeconds);
        return config;
    }

    private String required(String key) {
        String value = EnvUtils.get(key);
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalStateException("Missing configuration: " + key);
        }
        return value;
    }
}

短信工具类:TencentSmsUtils ​

负责与腾讯云短信服务的交互,包括发送短信的具体实现。

java
import com.example.auth.app.model.TxSmsConfig;
import nexus.io.tio.utils.json.JsonUtils;
import nexus.io.tio.utils.environment.EnvUtils;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;

import lombok.extern.slf4j.Slf4j;

/**
 * 腾讯云短信服务工具类
 */
@Slf4j
public class TencentSmsUtils {
    /**
     * 发送短信
     *
     * @param sdkAppId      短信应用ID
     * @param secretId      腾讯云账户SecretId
     * @param secretKey     腾讯云账户SecretKey
     * @param signName      短信签名
     * @param templateId    短信模板ID
     * @param params        模板参数
     * @param phoneNumberSet 接收短信的手机号集合
     * @return SendSmsResponse 发送短信的响应
     */
    public static SendSmsResponse sendSMS(String sdkAppId, String secretId, String secretKey, String signName, String templateId,
                                         String[] params, String[] phoneNumberSet) {
        SmsClient client = buildSmsClient(secretId, secretKey);
        SendSmsRequest req = buildSmsRequest(sdkAppId, signName, templateId, params, phoneNumberSet);
        log.info("发送短信请求:{}", JsonUtils.toJson(req));
        try {
            SendSmsResponse res = client.SendSms(req);
            return res;
        } catch (TencentCloudSDKException e) {
            log.error("发送短信失败: {}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 构建短信请求对象
     */
    private static SendSmsRequest buildSmsRequest(String sdkAppId, String signName, String templateId, String[] params, String[] phoneNumberSet) {
        SendSmsRequest req = new SendSmsRequest();
        req.setSmsSdkAppId(sdkAppId);
        req.setSignName(signName);
        req.setSenderId("");
        req.setSessionContext("");
        req.setExtendCode("");
        req.setTemplateId(templateId);
        req.setPhoneNumberSet(phoneNumberSet);
        req.setTemplateParamSet(params);
        return req;
    }

    /**
     * 构建短信客户端
     */
    private static SmsClient buildSmsClient(String secretId, String secretKey) {
        Credential cred = new Credential(secretId, secretKey);
        HttpProfile httpProfile = new HttpProfile();
        httpProfile.setReqMethod("POST");
        httpProfile.setConnTimeout(60);
        httpProfile.setEndpoint(EnvUtils.get("TENCENT_SMS_ENDPOINT", "sms.tencentcloudapi.com"));

        ClientProfile clientProfile = new ClientProfile();
        clientProfile.setSignMethod("HmacSHA256");
        clientProfile.setHttpProfile(httpProfile);

        SmsClient client = new SmsClient(cred, EnvUtils.get("TENCENT_SMS_REGION", "ap-guangzhou"), clientProfile);
        return client;
    }

    /**
     * 批量发送短信
     */
    public static SendSmsResponse sendSmsBat(TxSmsConfig config, String[] phoneList, String[] params) {
        return sendSMS(config.getAppId(), config.getSecretId(), config.getSecretKey(),
                config.getSmsSign(), config.getTemplateId(), params, phoneList);
    }

    /**
     * 发送单条短信
     */
    public static SendSmsResponse sendSms(TxSmsConfig config, String areaCode, String phone, String code) {
        String[] phoneList = new String[]{areaCode + phone};
        String[] params = {code};
        return sendSMS(config.getAppId(), config.getSecretId(), config.getSecretKey(),
                config.getSmsSign(), config.getTemplateId(), params, phoneList);
    }
}

单元测试:TencentSmsUtilsTest ​

通过单元测试验证短信发送功能。

java
import org.junit.Test;

import com.example.auth.app.config.EnoteMysqlDbConfig;
import com.example.auth.app.model.TxSmsConfig;
import com.example.auth.app.services.system.SystemConfigService;
import com.example.auth.app.utils.TencentSmsUtils;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.boot.tesing.TioBootTest;
import nexus.io.tio.utils.json.JsonUtils;
import nexus.io.tio.utils.environment.EnvUtils;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;

public class TencentSmsUtilsTest {

    @Test
    public void testSendSms() {
        TioBootTest.runWith(EnoteMysqlDbConfig.class);
        SystemConfigService systemConfigService = Aop.get(SystemConfigService.class);
        TxSmsConfig txSmsConfig = systemConfigService.getLoginSmsConfig();
        SendSmsResponse sendSms = TencentSmsUtils.sendSms(txSmsConfig, "86", "15836475191", "123456");
        System.out.println(JsonUtils.toJson(sendSms));
    }
}

发送的数据格式 ​

发送短信时,数据格式如下:

json
{
  "templateId": "1339563",
  "phoneNumberSet": ["8615836475191"],
  "templateParamSet": ["123456"],
  "sessionContext": "enote",
  "smsSdkAppId": "1400646561",
  "extendCode": "",
  "signName": "牛加技术",
  "senderId": ""
}

发送成功返回信息 ​

成功发送短信后,返回的信息格式如下:

json
{
  "sendStatusSet": [
    {
      "message": "send success",
      "code": "Ok",
      "sessionContext": "enote",
      "phoneNumber": "+8615836475191",
      "isoCode": "CN",
      "serialNo": "4412:45808295717294761577817519",
      "fee": "1"
    }
  ],
  "requestId": "fbac04a7-bc02-49e4-954c-8ce939103696"
}

接口实现 ​

在实现短信发送功能时,需要考虑以下两个关键点:

  1. 防止重复发送:通过查询数据库中已存在的未过期的验证码记录,避免频繁发送短信。
  2. 生成验证码并存储数据库:生成随机验证码并将其存储至数据库,以便后续验证。

枚举定义 ​

定义验证码类型和手机类型的枚举,便于代码中的类型管理和可读性。

SmsCodeType ​

java
import java.util.HashMap;
import java.util.Map;

public enum SmsCodeType {
    REGISTER(1, "注册"),
    LOGIN(2, "登录"),
    CHANGE_PHONE(3, "更换手机号码"),
    RESET_PWD(4, "重置登录密码");

    private Integer type;
    private String desc;

    SmsCodeType(Integer type, String desc) {
        this.type = type;
        this.desc = desc;
    }

    public Integer getType() {
        return type;
    }

    public String getDesc() {
        return desc;
    }

    public static String getDesc(int type) {
        for (SmsCodeType item : SmsCodeType.values()) {
            if (item.getType().equals(type)) {
                return item.getDesc();
            }
        }
        return "";
    }

    public static Map<Integer, String> getMap() {
        Map<Integer, String> map = new HashMap<>();
        for (SmsCodeType item : SmsCodeType.values()) {
            map.put(item.getType(), item.getDesc());
        }
        return map;
    }
}

PhoneType ​

java
package com.example.auth.app.enums;

public enum PhoneType {
    MAIN_LAND("86", "大陆手机号码"),
    HK("852", "香港手机号码");

    private String code;
    private String desc;

    PhoneType(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }

    public static PhoneType getPhoneType(String areaCode) {
        if (areaCode != null && !areaCode.isEmpty()) {
            if (areaCode.equals("+852") || areaCode.equals("852")) {
                return HK;
            }
        }
        return MAIN_LAND;
    }
}

Service 层:TencentSmsService ​

负责处理短信发送的业务逻辑,包括防止重复发送和验证码生成。

java
import com.example.auth.app.db.model.SysSmsCode;
import com.example.auth.app.model.TxSmsConfig;
import com.example.auth.app.services.system.SystemConfigService;
import com.example.auth.app.utils.TencentSmsUtils;
import com.jfinal.kit.Kv;
import nexus.io.db.activerecord.Db;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.utils.hutool.RandomUtils;
import nexus.io.tio.utils.snowflake.SnowflakeIdUtils;

public class TencentSmsService {

    /**
     * 发送短信验证码
     *
     * @param codeType 验证码类型
     * @param areaCode 国家代码
     * @param phone    手机号码
     * @param ip       用户IP地址
     * @return 响应结果
     */
    public RespBodyVo send(Integer codeType, String areaCode, String phone, String ip) {
        // 防止重复发送:检查是否存在未过期的验证码记录
        String sql = "SELECT 1 FROM sys_sms_code WHERE NOW() <= create_time + INTERVAL valid_seconds SECOND " +
                     "AND area_code=? AND phone=? AND tenant_id=0 AND deleted=0 LIMIT 1";
        boolean exists = Db.existsBySql(sql, areaCode, phone);
        if (exists) {
            return RespBodyVo.fail("请求过于频繁,请稍后再试。");
        }

        // 生成随机验证码
        int code = RandomUtils.nextInt(100000, 999999);

        // 获取短信配置信息
        SystemConfigService systemConfigService = Aop.get(SystemConfigService.class);
        TxSmsConfig loginSmsConfig = systemConfigService.getSmsConfig(codeType);

        // 发送短信
        TencentSmsUtils.sendSms(loginSmsConfig, areaCode, phone, String.valueOf(code));

        // 生成唯一ID并保存验证码记录
        long id = SnowflakeIdUtils.id();
        SysSmsCode sysSmsCode = new SysSmsCode();
        sysSmsCode.setId(id)
                  .setAreaCode(areaCode)
                  .setPhone(phone)
                  .setCode(String.valueOf(code))
                  .setIp(ip)
                  .setValidSeconds(Long.valueOf(loginSmsConfig.getValidSeconds()))
                  .setCodeType(codeType);

        sysSmsCode.set("platform", loginSmsConfig.getPlatform());
        boolean save = sysSmsCode.save();
        if (!save) {
            return RespBodyVo.fail("验证码保存失败,请稍后再试。");
        }

        return RespBodyVo.ok(Kv.by("id", id));
    }
}

测试类:TencentSmsServiceTest ​

通过测试类验证短信发送服务的正确性和防止重复发送的逻辑。

java
import org.junit.Test;

import com.example.auth.app.config.EnoteMysqlDbConfig;
import com.example.auth.app.enums.SmsCodeType;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.tesing.TioBootTest;

public class TencentSmsServiceTest {

    @Test
    public void testSendSms() {
        TioBootTest.runWith(EnoteMysqlDbConfig.class);
        TencentSmsService smsService = Aop.get(TencentSmsService.class);
        RespBodyVo response1 = smsService.send(SmsCodeType.LOGIN.getType(), "86", "15836475191", "192.168.1.2");
        System.out.println(response1);

        // 尝试重复发送,预期失败
        RespBodyVo response2 = smsService.send(SmsCodeType.LOGIN.getType(), "86", "15836475191", "192.168.1.2");
        System.out.println(response2);
    }
}

请求参数模型:SmsCodeSendVo ​

定义发送验证码接口的请求参数模型。

java
package com.example.auth.app.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class SmsCodeSendVo {
    private Integer codeType; // 验证码类型: 1-注册, 2-登录, 3-更换手机号码, 4-重置登录密码
    private String areaCode;  // 国家或地区代码,如中国为 "86"
    private String phone;     // 手机号码
}

Controller 层:SmsController ​

处理前端发送验证码的请求。

java
import com.example.auth.app.enums.PhoneType;
import com.example.auth.app.model.SmsCodeSendVo;
import com.example.auth.app.services.sms.TencentSmsService;
import com.example.auth.app.utils.RegexUtil;
import nexus.io.annotation.EnableCORS;
import nexus.io.annotation.RequestPath;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.utils.HttpIpUtils;

@RequestPath("/sms")
@EnableCORS
public class SmsController {

    private TencentSmsService smsService = Aop.get(TencentSmsService.class);

    /**
     * 发送验证码接口
     *
     * @param vo SmsCodeSendVo 请求参数
     * @return 响应结果
     */
    public RespBodyVo send(SmsCodeSendVo vo) {
        Integer codeType = vo.getCodeType();
        String areaCode = vo.getAreaCode();
        String phone = vo.getPhone();

        // 参数校验
        if (codeType == null) {
            return RespBodyVo.fail("验证码类型不能为空");
        }
        if (areaCode == null || areaCode.isEmpty()) {
            return RespBodyVo.fail("国家代码不能为空");
        }
        if (phone == null || phone.isEmpty()) {
            return RespBodyVo.fail("手机号码不能为空");
        }

        // 手机号格式校验(针对大陆号码)
        PhoneType phoneType = PhoneType.getPhoneType(areaCode);
        if (phoneType == PhoneType.MAIN_LAND && !RegexUtil.isMobileSimple(phone)) {
            return RespBodyVo.fail("手机号码格式不正确");
        }

        // 获取真实IP
        String realIp = HttpIpUtils.getRealIp(TioRequestContext.getRequest());

        // 发送验证码
        return smsService.send(codeType, areaCode, phone, realIp);
    }
}

登录注册逻辑 ​

实现用户通过短信验证码进行登录或注册的业务逻辑。

数据层:SysUserInfoDao ​

负责用户信息的数据库操作,包括判断用户是否存在和保存新用户。

java
package com.example.auth.app.dao;

import com.example.auth.app.utils.UserIdUtils;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;

public class SysUserInfoDao {

    public static final String tableName = "sys_user_info";

    /**
     * 判断用户名是否存在
     *
     * @param username 用户名
     * @return 存在返回true,否则返回false
     */
    public boolean existsUsername(String username) {
        return Db.exists(tableName, "username", username);
    }

    /**
     * 通过手机号保存新用户信息
     *
     * @param areaCode 国家代码
     * @param phone    手机号码
     * @param username 用户名
     * @param nickName 昵称
     * @return 新用户的ID,保存失败返回null
     */
    public Long saveByPhone(String areaCode, String phone, String username, String nickName) {
        long userId = UserIdUtils.random();
        Row row = new Row();
        row.set("id", userId)
              .set("username", username)
              .set("area_code", areaCode)
              .set("phone", phone)
              .set("nick_name", nickName);
        boolean save = Db.save(tableName, row);
        return save ? userId : null;
    }
}

参数模型:LoginVo ​

定义提交验证码接口的请求参数模型。

java
package com.example.auth.app.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class LoginVo {
    private Long id;         // 验证码ID
    private String areaCode; // 国家代码
    private String phone;    // 手机号码
    private Integer codeType;// 验证码类型
    private Integer code;    // 验证码
}

Service 层:AuthService ​

负责处理通过验证码进行登录或注册的业务逻辑。

java
import com.example.auth.app.dao.SysUserInfoDao;
import com.example.auth.app.model.LoginVo;
import com.jfinal.kit.Kv;
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.utils.hutool.RandomUtils;

import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AuthService {

    /**
     * 通过用户名和密码登录
     *
     * @param username 用户名
     * @param password 密码
     * @return 登录结果
     */
    public TableResult<Kv> doLogin(String username, String password) {
        String sql = "SELECT id FROM sys_user_info WHERE username=? AND password=?";
        Long userId = Db.queryLong(sql, username, password);
        if (userId != null && userId > 0) {
            Kv kv = doLogin(userId);
            return TableResult.ok(kv);
        } else {
            return TableResult.fail("用户名或密码不正确");
        }
    }

    /**
     * 执行登录操作
     *
     * @param userId 用户ID
     * @return 登录结果
     */
    public Kv doLogin(Long userId) {
        StpUtil.login(userId);
        String tokenValue = StpUtil.getTokenValue();
        long tokenTimeout = StpUtil.getTokenTimeout();
        Kv kv = new Kv();
        kv.set("token", tokenValue);
        kv.set("tokenTimeout", tokenTimeout);
        Row row = Db.findById("sys_user_info", userId);
        kv.set(row.toMap());
        return kv;
    }

    /**
     * 通过验证码进行登录或注册
     *
     * @param vo LoginVo 请求参数
     * @return 响应结果
     */
    public RespBodyVo loginByPhone(LoginVo vo) {
        Long id = vo.getId();
        Integer code = vo.getCode();
        String areaCode = vo.getAreaCode();
        String phone = vo.getPhone();
        Integer codeType = vo.getCodeType();

        // 登录注册只接受对应用途;不允许重置密码验证码用于登录。
        if (id == null || code == null || codeType == null || (codeType != 1 && codeType != 2)) {
            return RespBodyVo.fail("验证码参数无效。");
        }
        String sqlCode = "SELECT 1 FROM sys_sms_code WHERE id=? AND tenant_id=0 AND platform=? "
                + "AND area_code=? AND phone=? AND code=? AND code_type=? AND deleted=0 "
                + "AND TIMESTAMPDIFF(SECOND, create_time, NOW()) BETWEEN 0 AND valid_seconds LIMIT 1";
        if (!Db.existsBySql(sqlCode, id, "tencent", areaCode, phone, code, codeType)) {
            return RespBodyVo.fail("验证码无效或已过期。");
        }

        // 判断用户是否存在
        String sql = "SELECT id FROM sys_user_info WHERE area_code=? AND phone=? AND deleted=0";
        Long userId = Db.queryLong(sql, areaCode, phone);
        if (userId != null) {
            // 用户存在,执行登录
            Kv loginResult = this.doLogin(userId);
            return RespBodyVo.ok(loginResult);
        } else {
            // 用户不存在,自动注册新用户
            log.info("自动注册新用户:{}", phone);
            SysUserInfoDao sysUserInfoDao = Aop.get(SysUserInfoDao.class);
            userId = sysUserInfoDao.saveByPhone(areaCode, phone, phone, "User_" + RandomUtils.nextInt(1000, 9999));
            if (userId == null) {
                return RespBodyVo.fail("用户注册失败,请稍后再试。");
            }
            Kv loginResult = this.doLogin(userId);
            return RespBodyVo.ok(loginResult);
        }
    }
}

Controller 层:AuthController ​

处理用户通过验证码进行登录或注册的请求。

java
import com.example.auth.app.model.LoginVo;
import com.example.auth.app.services.auth.AuthService;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.annotation.EnableCORS;
import nexus.io.annotation.RequestPath;

@RequestPath("/auth")
@EnableCORS
public class AuthController {

    private AuthService authService = Aop.get(AuthService.class);

    /**
     * 通过验证码登录或注册
     *
     * @param vo LoginVo 请求参数
     * @return 响应结果
     */
    public RespBodyVo loginByPhone(LoginVo vo) {
        Long id = vo.getId();
        Integer code = vo.getCode();

        // 参数校验
        if (id == null) {
            return RespBodyVo.fail("验证码ID不能为空");
        }
        if (code == null) {
            return RespBodyVo.fail("验证码不能为空");
        }

        return authService.loginByPhone(vo);
    }
}

接口文档 ​

手机验证码登录注册 ​

本文档详细描述了发送验证码和提交验证码进行登录或注册的接口规范,帮助前端开发人员正确调用相关接口。


发送验证码接口 ​

Endpoint:
POST /sms/send

请求参数:

json
{
  "codeType": 1, // 验证码类型: 1-注册, 2-登录, 3-更换手机号码, 4-重置登录密码
  "areaCode": "86", // 国家代码,如中国为 "86"
  "phone": "15836475191" // 手机号
}

参数说明:

  • codeType: 指定验证码的用途类型,具体如下:
    1. 注册
    2. 登录
    3. 更换手机号码
    4. 重置登录密码
  • areaCode: 国家或地区的代码,例如中国为 "86"。
  • phone: 用户的手机号码。

响应示例:

json
{
  "data": {
    "id": "437986347671658496" // 验证码ID,用于提交验证码时关联
  },
  "msg": null,
  "code": 1, // 1 表示成功
  "ok": true // 请求成功标识
}

响应参数说明:

  • data.id: 唯一的验证码 ID,后续验证时需要使用。
  • code: 状态码,1 表示成功,其他值表示失败。
  • ok: 请求是否成功的布尔标识。

提交验证码接口 ​

Endpoint:
POST /auth/loginByPhone

请求参数:

json
{
  "id": "437986347671658496", // 验证码ID
  "code": 440263, // 发送到手机的验证码
  "phone": "15836475191", // 手机号
  "areaCode": "86", // 国家代码
  "codeType": 1 // 验证码类型: 1-注册, 2-登录, 3-更换手机号码, 4-重置登录密码
}

参数说明:

  • id: 发送验证码时返回的唯一验证码 ID。
  • code: 用户收到的验证码。
  • phone: 用户的手机号码。
  • areaCode: 国家或地区的代码,例如中国为 "86"。
  • codeType: 验证码的用途类型,与发送验证码时的类型保持一致。

逻辑说明:
loginByPhone 接口会根据手机号码判断用户是否存在,若不存在,则自动创建新用户。不设置永久有效的测试验证码,测试时使用隔离的短信发送适配器和真实的验证码校验流程。

响应示例:

json
{
  "data": {
    "tenant_id": "0",
    "wx_user_info": null,
    "remark": null,
    "locale": null,
    "config_info": null,
    "updater": "",
    "header_pic_id": null,
    "password": null,
    "update_time": 1729589458000,
    "id": "52317188646659035", // 用户ID
    "app_id": null,
    "email": null,
    "creator": "",
    "create_time": 1729589458000,
    "area_code": "86",
    "sex": null,
    "user_from": null,
    "tokenTimeout": "2592000", // Token 有效期(秒)
    "facebook_user_info": null,
    "wx_open_id": null,
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjUyMzE3MTg4NjQ2NjU5MDM1LCJyblN0ciI6ImNRSWRlWG4yUGQwT2h5NkRZUzRnb2kxbWlkQ2ZQb01sIn0.Rc2ieAAIt6JchEVRp_tQ22MtAye3Gh20sg_3ooAFkPg", // 登录 Token
    "facebook_id": null,
    "deleted": 0,
    "user_channel": null,
    "phone": null,
    "v": 0,
    "nick_name": "User_3990", // 用户昵称
    "name": null,
    "user_level": null,
    "username": "15836475191", // 用户名(手机号)
    "status": 0
  },
  "code": 1, // 1 表示成功
  "msg": null,
  "ok": true // 请求成功标识
}

响应参数说明:

  • data: 包含用户信息和登录 Token。
    • id: 用户唯一 ID。
    • token: 登录后的 Token,用于后续的认证。
    • tokenTimeout: Token 的有效期,单位为秒。
    • username: 用户名,即手机号码。
    • nick_name: 用户昵称。
    • 其他字段为用户的相关信息,可根据需要进行扩展。
  • code: 状态码,1 表示成功,其他值表示失败。
  • ok: 请求是否成功的布尔标识。

总结 ​

通过本指南,您已经了解了如何使用腾讯云短信服务实现基于短信验证码的用户登录和注册功能。整个流程包括短信模板配置、数据库设计、短信发送工具类开发、业务逻辑实现以及接口文档编写。确保各个环节正确配置和实现,可以有效提升用户体验和系统的安全性。

在实际应用中,建议根据具体需求进一步优化和扩展功能,例如增加验证码的复杂度、实现多语言支持、加强安全性措施等。

MIT Licensed