Skip to content

业务讲解-用户登录和退出流程解析

登录

本文重点讲解关于通过手机号或者邮箱的登录方式,如图:

1720778583170-9668a4fc-a747-4f8e-a4a6-a5f9ffafe1a7.jpeg

说明

由于将用户表进行了分库分表的操作,在其他操作中,比如查询用户信息,修改用户信息,订单的用户信息等都是是用的userId查询的

这就产生了问题,由于登录支持使用手机号和邮箱登录,那么进行分库分表的分片键要怎么选择呢?上述中提到了很多的都是靠userId作为条件的,那么还是要以userId作为分片键,但是手机号和邮箱怎么处理?为了解决这个问题,我们使用了互联网公司常用的附属表的方案,另外设计了用户手机表和用户邮箱表,这样先用手机号和邮箱查询到userId,然后userId再去操作用户表,这样就解决了分片建的问题。

关于此问题的详细介绍,可查看用户的分库分表相关文档

分库分表-用户服务-用户表

流程

模块: damai-user-service

入参

java
@Data
@ApiModel(value="UserLoginDto", description ="用户登录")
public class UserLoginDto {
    
    @ApiModelProperty(name ="code", dataType ="String", value ="渠道code 0001:pc网站", required = true)
    @NotBlank
    private String code;
    
    @ApiModelProperty(name ="name", dataType ="String", value ="用户手机号")
    private String mobile;
    
    @ApiModelProperty(name ="email", dataType ="String", value ="用户邮箱")
    private String email;
    
    @ApiModelProperty(name ="password", dataType ="String", value ="密码", required = true)
    @NotBlank
    private String password;
}

com.damai.service.UserService#login

java
/**
 * 登录
 * @param userLoginDto 登录入参
 * @return 用户信息
 * */
public UserLoginVo login(UserLoginDto userLoginDto) {
    UserLoginVo userLoginVo = new UserLoginVo();
    String code = userLoginDto.getCode();
    String mobile = userLoginDto.getMobile();
    String email = userLoginDto.getEmail();
    String password = userLoginDto.getPassword();
    //如果手机号和邮箱同时不存在,那么直接抛出异常
    if (StringUtil.isEmpty(mobile) && StringUtil.isEmpty(email)) {
        throw new DaMaiFrameException(BaseCode.USER_MOBILE_AND_EMAIL_NOT_EXIST);
    }
    Long userId;
    if (StringUtil.isNotEmpty(mobile)) {
        //检查输入的手机号是否达到输入错误次数限制
        String errorCountStr = 
                redisCache.get(RedisKeyBuild.createRedisKey(RedisKeyManage.LOGIN_USER_MOBILE_ERROR, mobile), String.class);
        //如果达到限制的阈值则不再往下执行
        if (StringUtil.isNotEmpty(errorCountStr) && Integer.parseInt(errorCountStr) >= ERROR_COUNT_THRESHOLD) {
            throw new DaMaiFrameException(BaseCode.MOBILE_ERROR_COUNT_TOO_MANY);
        }
        //如果手机号存在,则用手机号查询用户id
        LambdaQueryWrapper<UserMobile> queryWrapper = Wrappers.lambdaQuery(UserMobile.class)
                .eq(UserMobile::getMobile, mobile);
        UserMobile userMobile = userMobileMapper.selectOne(queryWrapper);
        if (Objects.isNull(userMobile)) {
            //如果查询手机号不存在,则放入redis中将手机号输入错误的计数器加1
            redisCache.incrBy(RedisKeyBuild.createRedisKey(RedisKeyManage.LOGIN_USER_MOBILE_ERROR,mobile),1);
            redisCache.expire(RedisKeyBuild.createRedisKey(RedisKeyManage.LOGIN_USER_MOBILE_ERROR,mobile),1,TimeUnit.MINUTES);
            throw new DaMaiFrameException(BaseCode.USER_MOBILE_EMPTY);
        }
        userId = userMobile.getUserId();
    }else {
        //检查输入的邮箱是否达到输入错误次数限制
        String errorCountStr = 
                redisCache.get(RedisKeyBuild.createRedisKey(RedisKeyManage.LOGIN_USER_EMAIL_ERROR, email), String.class);
        //如果达到限制的阈值则不再往下执行
        if (StringUtil.isNotEmpty(errorCountStr) && Integer.parseInt(errorCountStr) >= ERROR_COUNT_THRESHOLD) {
            throw new DaMaiFrameException(BaseCode.EMAIL_ERROR_COUNT_TOO_MANY);
        }
        //用邮箱查询用户id    
        LambdaQueryWrapper<UserEmail> queryWrapper = Wrappers.lambdaQuery(UserEmail.class)
                .eq(UserEmail::getEmail, email);
        UserEmail userEmail = userEmailMapper.selectOne(queryWrapper);
        if (Objects.isNull(userEmail)) {
            //如果查询手机号不存在,则放入redis中将手机号输入错误的计数器加1
            redisCache.incrBy(RedisKeyBuild.createRedisKey(RedisKeyManage.LOGIN_USER_EMAIL_ERROR,email),1);
            redisCache.expire(RedisKeyBuild.createRedisKey(RedisKeyManage.LOGIN_USER_EMAIL_ERROR,email),1,TimeUnit.MINUTES);
            throw new DaMaiFrameException(BaseCode.USER_EMAIL_NOT_EXIST);
        }
        userId = userEmail.getUserId();
    }
    //从库中查询用户
    LambdaQueryWrapper<User> queryUserWrapper = Wrappers.lambdaQuery(User.class)
            .eq(User::getId, userId).eq(User::getPassword, password);
    User user = userMapper.selectOne(queryUserWrapper);
    //用户不存在,抛出异常
    if (Objects.isNull(user)) {
        throw new DaMaiFrameException(BaseCode.NAME_PASSWORD_ERROR);
    }
    //将用户信息放到缓存中
    redisCache.set(RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN,code,user.getId()),user,
            tokenExpireTime,TimeUnit.MINUTES);
    userLoginVo.setUserId(userId);
    //生成token
    userLoginVo.setToken(createToken(user.getId(),getChannelDataByCode(code).getTokenSecret()));
    return userLoginVo;
}

通过code查询token秘钥

java
private GetChannelDataVo getChannelDataByCode(String code){
    //从redis查询
    GetChannelDataVo channelDataVo = getChannelDataByRedis(code);
    if (Objects.isNull(channelDataVo)) {
        //调用base-data服务查询
        channelDataVo = getChannelDataByClient(code);
        //放到redis中
        setChannelDataRedis(code,channelDataVo);
    }
    return channelDataVo;
}

private GetChannelDataVo getChannelDataByRedis(String code){
    return redisCache.get(RedisKeyBuild.createRedisKey(RedisKeyManage.CHANNEL_DATA,code),GetChannelDataVo.class);
}

private GetChannelDataVo getChannelDataByClient(String code){
    GetChannelDataByCodeDto getChannelDataByCodeDto = new GetChannelDataByCodeDto();
    getChannelDataByCodeDto.setCode(code);
    ApiResponse<GetChannelDataVo> getChannelDataApiResponse = baseDataClient.getByCode(getChannelDataByCodeDto);
    if (Objects.equals(getChannelDataApiResponse.getCode(), BaseCode.SUCCESS.getCode())) {
        return getChannelDataApiResponse.getData();
    }
    throw new DaMaiFrameException("没有找到ChannelData");
}

生成token

java
public String createToken(Long userId,String tokenSecret){
    Map<String,Object> map = new HashMap<>(4);
    map.put("userId",userId);
    return TokenUtil.createToken(String.valueOf(uidGenerator.getUid()), JSON.toJSONString(map),tokenExpireTime * 60 * 1000,tokenSecret);
}

总结

  • 如果手机号存在,则去用户手机表查询用户id
  • 如果邮箱存在,则去用户邮箱表查询用户id
  • 然后根据手机号或者邮箱判断是否达到输入次数错误的限制,如果达到则锁定该用户1分钟不允许登录
  • 根据用户id,去用户表中查询用户信息
  • 将用户信息放入到缓存中作为登录信息,并根据token.expire.time配置来设置过期时间,默认是40分钟
  • 根据code查询到token秘钥
  • 根据用户id,token秘钥来生成token,过期时间和token.expire.time配置的相同
  • 将数据返回给前端

token工具类

生成token的工具类,用于token的生成和解析,并且提供了生成和解析的示例

模块:damai-common

com.damai.jwt.TokenUtil

java
@Slf4j
public class TokenUtil {
    
    /**
     * 指定签名的时候使用的签名算法,也就是header那部分。
     * 
     */
     private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法
     *
     * @param id        标识
     * @param info      登录成功的user对象
     * @param ttlMillis jwt过期时间
     * @param tokenSecret 私钥
     * @return
     */
    public static String createToken(String id, String info, long ttlMillis, String tokenSecret) {
        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        
        //创建一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
//                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(id)
                //iat: jwt的签发时间
                .setIssuedAt(new Date(nowMillis))
                //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串。
                .setSubject(info)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(SIGNATURE_ALGORITHM, tokenSecret);
        if (ttlMillis >= 0) {
            //设置过期时间
            builder.setExpiration(new Date(nowMillis + ttlMillis));
        }
        return builder.compact();
    }


    /**
     * Token的解密
     *
     * @param token 加密后的token
     * @param tokenSecret 私钥
     * @return
     */
    public static String parseToken(String token, String tokenSecret) {
        try {
            return Jwts.parser()
                    //设置签名的秘钥
                    .setSigningKey(tokenSecret)
                    //设置需要解析的jwt
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
        }catch (ExpiredJwtException jwtException) {
            log.error("parseToken error",jwtException);
            throw new DaMaiFrameException(BaseCode.TOKEN_EXPIRE);
        }
        
    }
    
    public static void main(String[] args) {
        
         String tokenSecret = "CSYZWECHAT";
        //生成token的实力
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("001key", "001value");
        jsonObject.put("002key", "001value");
		
        String token1 = TokenUtil.createToken("1", jsonObject.toJSONString(), 10000, tokenSecret);
        System.out.println("token:" + token1);
        
        //解析token的示例
        String token2 = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNjg4NTQyODM3LCJzdWIiOiJ7XCIwMDJrZXlcIjpcIjAwMXZhbHVlXCIsXCIwMDFrZXlcIjpcIjAwMXZhbHVlXCJ9IiwiZXhwIjoxNjg4NTQyODQ3fQ.vIKcAilTn_CR3VYssNE7rBpfuCSCH_RrkmsadLWf664";
        String subject = TokenUtil.parseToken(token2, tokenSecret);
        System.out.println("解析token后的值:" + subject);
    }
}

退出

入参

java
@Data
@ApiModel(value="UserLoginDto", description ="用户退出登录")
public class UserLogoutDto {
    
    @ApiModelProperty(name ="code", dataType ="String", value ="渠道code 0001:pc网站", required = true)
    @NotBlank
    private String code;
    
    @ApiModelProperty(name ="id", dataType ="Long", value ="用户id", required =true)
    @NotNull
    private Long id;
}

com.damai.service.UserService#logout

java
public void logout(UserLogoutDto userLogoutDto) {
    User user = userMapper.selectById(userLogoutDto.getId());
    if (Objects.isNull(user)) {
        throw new DaMaiFrameException(BaseCode.USER_EMPTY);
    }
    redisCache.del(RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN,userLogoutDto.getCode(),user.getId()));
}

退出登录的逻辑比较简单,根据用户id和code删除缓存的用户信息

更新: 2025-10-29 11:34:28
原文: https://www.yuque.com/u22210564/ykdrdh/bvlg9phc26rwpsie

Java 后端面试知识库