05 增加高性能用户登录接口
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_mall
商城项目tag:https://gitee.com/dvsusan/susan_mall/tree/tag_059
秒杀系统gitee:https://gitee.com/dvsusan/susan_seckill
秒杀系统tag:https://gitee.com/dvsusan/susan_seckill/tree/tag_002