跳至主要內容

05 增加高性能用户登录接口

Java突击队大约 4 分钟

05 增加高性能用户登录接口

前言

之前我们的商城系统用户登录权限验证, 是使用Spring Security框架来做的。

但我们秒杀系统,处于性能方面的考虑,手写一个简单的验证即可,不用做的太复杂。

在用户秒杀商品之前,先要登录。

我们接下来,提供一个支持高并发的用户登录接口,希望对你会有所帮助。

1 迁移公共代码

我们的秒杀系统,虽说提供的接口不多,但为了方便后期的维护,还有非常有必要把所有接口的响应做个统一封装。

还有些连接Redis和生成token的代码,秒杀系统也可能会用到。

因此,需要将这些公共的代码,迁移到susan-common模块下,这个模块以后可以被多个项目依赖使用。

将项目几个类从susan_mall项目的其他模块下,迁移到susan-common模块下:

其中UserTokenHelper类是新增的:

package cn.net.susan.helper;

import cn.net.susan.util.RedisUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @author 苏三,该项目是知识星球:java突击队 的内部项目
 * @date 2024/6/25 下午4:34
 */
@Slf4j
@Component
public class UserTokenHelper {

    private static final String TOKEN_PREFIX = "token:";
    private static final String USER_PREFIX = "user:";

    @Getter
    @Value("${mall.mgt.tokenSecret:123456test}")
    private String tokenSecret;
    @Value("${mall.mgt.tokenExpireTimeInRecord:3600}")
    private int tokenExpireTimeInRecord;

    @Autowired
    protected RedisUtil redisUtil;

    /**
     * 生成token
     * @param username 用户名
     * @param json 用户信息
     * @return
     */
    public String generateToken(String username, String json) {
        String token = Jwts.builder()
                .setSubject(username)
                .setExpiration(generateExpired())
                .signWith(SignatureAlgorithm.HS512, tokenSecret)
                .compact();
        redisUtil.set(getTokenKey(username), token, tokenExpireTimeInRecord);
        redisUtil.set(getUserKey(username), json, tokenExpireTimeInRecord);
        return token;
    }


    protected String getTokenKey(String username) {
        return getKey(TOKEN_PREFIX, username);
    }

    protected String getUserKey(String username) {
        return getKey(USER_PREFIX, username);
    }

    /**
     * 计算过期时间
     *
     * @return Date
     */
    protected Date generateExpired() {
        return new Date(System.currentTimeMillis() + tokenExpireTimeInRecord * 1000);
    }


    protected String getKey(String prefix, String userName) {
        return String.format("%s%s", prefix, userName);
    }
}

将TokenHelper继承UserTokenHelper类,其中generateToken方法改成这样的:

/**
 * 生成token
 *
 * @param userDetails 用户信息
 * @return token
 */
public String generateToken(UserDetails userDetails) {
    return super.generateToken(userDetails.getUsername(), JSON.toJSONString(userDetails));
}

为什么要增加UserTokenHelper类呢?

主要是为了解耦。

让秒杀系统不依赖Spring Security的认证功能。

还需要再susan-common模块的pom.xml文件中引入:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

由于处理用户密码时会用到spring-security-core这包,因此需要引入一下。

此外,还引入了jjwt的依赖。

2 增加用户登录接口

在seckill-business模块的pom.xml文件中引入mall-common的依赖:

  <dependency>
      <groupId>cn.net.susan</groupId>
      <artifactId>mall-common</artifactId>
      <version>1.0.0</version>
  </dependency>

在seckill-api模块下的controller包下创建WebUserController类:

package cn.net.susan.seckill.api.controller;

import cn.net.susan.entity.auth.AuthUserEntity;
import cn.net.susan.entity.auth.TokenEntity;
import cn.net.susan.seckill.business.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

/**
 * @author 苏三,该项目是知识星球:java突击队 的内部项目
 * @date 2024/6/25 下午3:40
 */
@Api(tags = "web用户操作", description = "web用户接口")
@RestController
@RequestMapping("/v1/web/user")
@Validated
public class WebUserController {

    @Autowired
    private UserService userService;

    /**
     * 用户登录
     *
     * @param authUserEntity 用户实体
     * @return 影响行数
     */
    @ApiOperation(notes = "用户登录", value = "用户登录")
    @PostMapping("/login")
    public TokenEntity login(@Valid @RequestBody AuthUserEntity authUserEntity) {
        return userService.login(authUserEntity);
    }
}

在seckill-business模块的service包下创建UserService类:

package cn.net.susan.seckill.business.service;

import cn.net.susan.entity.auth.AuthUserEntity;
import cn.net.susan.entity.auth.TokenEntity;
import cn.net.susan.exception.BusinessException;
import cn.net.susan.helper.UserTokenHelper;
import cn.net.susan.util.AssertUtil;
import cn.net.susan.util.PasswordUtil;
import cn.net.susan.util.RedisUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import static cn.net.susan.util.AssertUtil.ASSERT_ERROR_CODE;

/**
 * 用户Service
 *
 * @author 苏三,该项目是知识星球:java突击队 的内部项目
 * @date 2024/6/25 下午3:59
 */
@Slf4j
@Service
public class UserService {

    private static final String REGISTER_USER_PREFIX = "registerUser:";
    private static final String CAPTCHA_PREFIX = "captcha:";
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private PasswordUtil passwordUtil;
    @Autowired
    private UserTokenHelper userTokenHelper;
    @Autowired
    private PasswordEncoder passwordEncoder;


    /**
     * 用户登录接口
     *
     * @param authUserEntity 用户信息
     * @return token信息
     */
    public TokenEntity login(AuthUserEntity authUserEntity) {
        String code = redisUtil.get(getCaptchaKey(authUserEntity.getUuid()));
        AssertUtil.hasLength(code, "该验证码不存在或者已失效");
        AssertUtil.isTrue(code.trim().equals(authUserEntity.getCode().trim()), "验证码错误");

        try {
            String username = authUserEntity.getUsername();
            String decodePassword = passwordUtil.decodeRsaPassword(authUserEntity);
            String encodePassword = redisUtil.get(REGISTER_USER_PREFIX + username);
            if (!StringUtils.hasLength(encodePassword)) {
                throw new BusinessException(ASSERT_ERROR_CODE, "该用户不存在");
            }

            if (!passwordEncoder.matches(decodePassword, encodePassword)) {
                throw new BusinessException(ASSERT_ERROR_CODE, "密码错误");
            }
            String token = userTokenHelper.generateToken(authUserEntity.getUsername(), JSON.toJSONString(authUserEntity));
            redisUtil.del(getCaptchaKey(authUserEntity.getUuid()));
            return new TokenEntity(authUserEntity.getUsername(), token, Lists.newArrayList());
        } catch (Exception e) {
            log.info("登录失败:", e);
            if (e instanceof BusinessException) {
                throw e;
            }
            throw new BusinessException(ASSERT_ERROR_CODE, "用户名或密码错误");
        }
    }


    private String getCaptchaKey(String uuid) {
        return String.format("%s%s", CAPTCHA_PREFIX, uuid);
    }

}

在该类中提供了login用户登录接口。

该方法会先从Redis校验验证码,如果校验失败,则直接提示错误。

如果验证码校验成功,则从Redis校验用户名和密码,如果校验失败,则抛异常。

如果用户名和密码校验成功,则生成token,保存到Redis当中。

然后删除Redis当中的验证码。

最后返回token信息。

整条流程没有使用数据库,并且没有使用Spring Security,只用到Redis,登录接口的性能得到了不少的提升。

商城项目gitee:https://gitee.com/dvsusan/susan_mallopen in new window

商城项目tag:https://gitee.com/dvsusan/susan_mall/tree/tag_059open in new window

秒杀系统gitee:https://gitee.com/dvsusan/susan_seckillopen in new window

秒杀系统tag:https://gitee.com/dvsusan/susan_seckill/tree/tag_002open in new window