weblog/doc/5、整合 SaToken 实现 JWT 登录功能/5.12 用户注册登录接口开发(2).md
2025-02-17 10:05:44 +08:00

12 KiB
Raw Blame History

本小节中,我们继续开发 —— 注册/登录接口 将手机号验证码方式登录的剩余逻辑补充完整。

添加公共枚举类

编辑 xiaoha-common 公共模块,添加 /eumns 包,用于放置全局通用的枚举类。并添加以下两个枚举,等会业务层中,自动注册用户需要用到:

  • 逻辑删除枚举;
  • 开启/禁用状态枚举;
package com.quanxiaoha.framework.common.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @date: 2023-08-15 10:33
 * @description: 逻辑删除
 **/
@Getter
@AllArgsConstructor
public enum DeletedEnum {

    YES(true),
    NO(false);

    private final Boolean value;
}
package com.quanxiaoha.framework.common.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @date: 2023-08-15 10:33
 * @description: 状态
 **/
@Getter
@AllArgsConstructor
public enum StatusEnum {
    // 启用
    ENABLE(0),
    // 禁用
    DISABLED(1);

    private final Integer value;
}

生成 RBAC 模型其他表的 DO、Mapper 接口、XML 文件

上小节 中,已经将 t_user 表对应的 DO 实体类、Mapper 接口、XML 文件已经生成好了,这里顺手将其他几张表,也通过 Mybatis 代码生成器插件生成一下,如下图所示:

Tip

: 这里就不贴代码了,小伙伴们有需要的话,可下载本小节源码来查看。

Redis 全局 ID 自增

当新用户登录时,系统需要为该手机号自动注册一个用户,同时,还需要分配一个小红书 ID, 如小红薯10000、小红薯10001 ,一直自增的方式,并且需要保证全局唯一。要如何实现呢?

这里我们可以借助 Redis 实现,执行如下命令,设置一个 keyxiaohashu_id_generator 的生成器,初始值设置为 10000

set xiaohashu_id_generator 10000

然后,通过 INCR 命令即可实现每次对其自增 1 , 命令如下:

INCR xiaohashu_id_generator

添加全局 ID 生成器常量 KEY

方案定好后,编辑 RedisKeyConstants 全局常量类,添加小哈书全局 ID 生成器 KEY 代码如下:

package com.quanxiaoha.xiaohashu.auth.constant;

public class RedisKeyConstants {

    // 省略...

    /**
     * 小哈书全局 ID 生成器 KEY
     */
    public static final String XIAOHASHU_ID_GENERATOR_KEY = "xiaohashu_id_generator";

    // 省略...
}

添加角色全局常量类

接着,在 /constant 常量包下,再创建一个 RoleConstants 角色全局常量类,用于放置角色相关的全局常量,代码如下:

package com.quanxiaoha.xiaohashu.auth.constant;

/**
 * @author: 犬小哈
 * @date: 2024/5/21 15:04
 * @version: v1.0.0
 * @description: 角色全局常量
 **/
public class RoleConstants {


    /**
     * 普通用户的角色 ID
     */
    public static final Long COMMON_USER_ROLE_ID = 1L;

}

定义一个普通用户的角色 ID, 该角色数据,已经在上小节中提前准备好了。等会自动注册用户时,需要为用户自动分配上该角色。

用户角色 Redis Key

在系统自动注册用户完成后,还需要将该用户的角色,存储到 Redis 缓存中,方便后续鉴权使用。编辑 RedisKeyConstants 常量类,添加用户角色数据 KEY 前缀 代码如下:

package com.quanxiaoha.xiaohashu.auth.constant;

public class RedisKeyConstants {

    // 省略...

    /**
     * 用户角色数据 KEY 前缀
     */
    private static final String USER_ROLES_KEY_PREFIX = "user:roles:";


    /**
     * 构建验证码 KEY
     * @param phone
     * @return
     */
    public static String buildUserRoleKey(String phone) {
        return USER_ROLES_KEY_PREFIX + phone;
    }
}

完善用户注册逻辑

前置工作完成后,准备编辑 UserServiceImpl 业务实现类,补充上自动注册用户的逻辑,代码如下:

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

import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.quanxiaoha.framework.common.enums.DeletedEnum;
import com.quanxiaoha.framework.common.enums.StatusEnum;
import com.quanxiaoha.framework.common.exception.BizException;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.framework.common.util.JsonUtils;
import com.quanxiaoha.xiaohashu.auth.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.auth.constant.RoleConstants;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.PermissionDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.RoleDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserRoleDO;
import com.quanxiaoha.xiaohashu.auth.domain.mapper.UserDOMapper;
import com.quanxiaoha.xiaohashu.auth.domain.mapper.UserRoleDOMapper;
import com.quanxiaoha.xiaohashu.auth.enums.LoginTypeEnum;
import com.quanxiaoha.xiaohashu.auth.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.auth.model.vo.user.UserLoginReqVO;
import com.quanxiaoha.xiaohashu.auth.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

@Service
@Slf4j
public class UserServiceImpl implements UserService {

	// 省略...
	
    @Resource
    private UserRoleDOMapper userRoleDOMapper;


    /**
     * 登录与注册
     *
     * @param userLoginReqVO
     * @return
     */
    @Override
    public Response<String> loginAndRegister(UserLoginReqVO userLoginReqVO) {
		// 省略...

        // 判断登录类型
        switch (loginTypeEnum) {
            case VERIFICATION_CODE: // 验证码登录
				// 省略...

                // 判断是否注册
                if (Objects.isNull(userDO)) {
                    // 若此用户还没有注册,系统自动注册该用户
                    userId = registerUser(phone);
                } else {
                    // 已注册,则获取其用户 ID
                    userId = userDO.getId();
                }
                break;
            case PASSWORD: // 密码登录
                // todo

                break;
            default:
                break;
        }

        // SaToken 登录用户,并返回 token 令牌
        // todo

        return Response.success("");
    }

    /**
     * 系统自动注册用户
     * @param phone
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public Long registerUser(String phone) {
        // 获取全局自增的小哈书 ID
        Long xiaohashuId = redisTemplate.opsForValue().increment(RedisKeyConstants.XIAOHASHU_ID_GENERATOR_KEY);

        UserDO userDO = UserDO.builder()
                .phone(phone)
                .xiaohashuId(String.valueOf(xiaohashuId)) // 自动生成小红书号 ID
                .nickname("小红薯" + xiaohashuId) // 自动生成昵称, 如小红薯10000
                .status(StatusEnum.ENABLE.getValue()) // 状态为启用
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .isDeleted(DeletedEnum.NO.getValue()) // 逻辑删除
                .build();

        // 添加入库
        userDOMapper.insert(userDO);

        // 获取刚刚添加入库的用户 ID
        Long userId = userDO.getId();

        // 给该用户分配一个默认角色
        UserRoleDO userRoleDO = UserRoleDO.builder()
                .userId(userId)
                .roleId(RoleConstants.COMMON_USER_ROLE_ID)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .isDeleted(DeletedEnum.NO.getValue())
                .build();
        userRoleDOMapper.insert(userRoleDO);

        // 将该用户的角色 ID 存入 Redis 中
        List<Long> roles = Lists.newArrayList();
        roles.add(RoleConstants.COMMON_USER_ROLE_ID);
        String userRolesKey = RedisKeyConstants.buildUserRoleKey(phone);
        redisTemplate.opsForValue().set(userRolesKey, JsonUtils.toJsonString(roles));

        return userId;
    }

}

解释一下 registerUser 用户注册方法的逻辑:

  • 为方法添加 @Transactional(rollbackFor = Exception.class) 事务注解,保证方法块内代码的原子性,要么全部成功,要么全部失败;

  • 操作 Redis , 获取全局自增的小哈书 ID

  • 构建 UserDO 实体类,包括分配小红书 ID, 昵称等;

  • 插入用户数据,并获取其主键 ID ;

  • 给该用户分配一个普通用户角色,并入库;

  • 最后将给用户的角色数据,存储到 Redis 中,供后续鉴权使用;

  • 返回用户 ID ;

Mybatis 获取自增 ID

上面的业务逻辑中,用户数据插入表中后,需要获取到该用户的主键 ID, Mybatis 要如何获取呢? 可编辑 xml 文件中的 insert 方法,添加 useGeneratedKeys="true" keyProperty="id" 代码如下。当数据新增成功后Mybatis 会自动将该条记录的主键 ID 设置到入参中,我们直接从入参 UserDO 中,即可获取主键 ID

<insert id="insert" parameterType="com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserDO" useGeneratedKeys="true" keyProperty="id">
    insert into t_user (xiaohashu_id, `password`, 
      nickname, avatar, birthday, 
      background_img, phone, sex, 
      `status`, introduction, create_time, 
      update_time, is_deleted)
    values (#{xiaohashuId,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, 
      #{nickname,jdbcType=VARCHAR}, #{avatar,jdbcType=VARCHAR}, #{birthday,jdbcType=DATE}, 
      #{backgroundImg,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR}, #{sex,jdbcType=TINYINT}, 
      #{status,jdbcType=TINYINT}, #{introduction,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP}, 
      #{updateTime,jdbcType=TIMESTAMP}, #{isDeleted,jdbcType=BIT})
  </insert>

Tip

自动生成的 xml 文件中 insert SQL 中的 id 项可以删掉,让其自增即可,无需手动填入。

返回 Token 令牌

注册用户逻辑编写完毕后,再来补充一下返回 Token 令牌部分代码。前面我们已经获取到了登录用户的 ID ,可直接通过 StpUtil.login() 方法完成登录,并从 SaTokenInfo 对象中获取 Token 令牌,代码如下:

        // 省略...
        
        // SaToken 登录用户, 入参为用户 ID
        StpUtil.login(userId);

        // 获取 Token 令牌
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();

        // 返回 Token 令牌
        return Response.success(tokenInfo.tokenValue);
        
        // 省略...

至此,手机号验证码登录注册的整体功能就开发完毕了。

自测一波

重启项目,自测一波登录接口。先调用获取验证码接口,拿到一个新的验证码,然后,将该验证码填入到登录接口的入参中,点击发送,如下图所示:

可以看到,成功返回了一个 Token 令牌,另外,确认一下 t_user 表中,是否有自动注册该手机号的用户信息:

以及 t_user_role 表中,是否有该用户的角色信息:

OK, 一切正常。

本小节源码下载

https://t.zsxq.com/t03dT