用户登录是一种常用功能。这里记录一下基于Redis实现用户登录的代码。
下面是登录的流程图:
- 用户先提交手机号和验证码,服务器以手机号为key校验redis中存储的验证码,存在,则查询数据库中是否存在用户,不存在则创建并将token作为key,用户信息作为value保存在Redis中。
- 用户登录成功后的请求需要在session中携带token,来进行身份的鉴权。
1.用户短信验证码登录、注册代码
- Controller层
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// TODO 实现登录功能
return userService.login(loginForm, session) ;
}
- Service层
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
// 1. 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式不正确");
}
// 2. 从Redis获取验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.equals(code)){
// 3. 不一致报错
return Result.fail("验证码错误");
}
// 4. 一致根据手机号查用户
User user = query().eq("phone", phone).one();
if(user == null){
// 5. 用户不存在
user = createUserWithPhone(phone);
// 6. 不存在创建用户
}
// 7. 保存用户信息到session
// 7.1 生成token
String token = UUID.randomUUID().toString(true);
// 7.2 保存到redis,hash存储,使用Map减少与服务器交互次数
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 将所有字段转为string
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fileName,fileValue)->fileValue.toString()));
// 7.3 存储
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
// 7.4 设置过期时间
stringRedisTemplate.expire(LOGIN_USER_KEY+token, LOGIN_USER_TTL, TimeUnit.SECONDS);
// 返回token
System.out.println(token);
return Result.ok(token);
}
2.校验登录状态代码
这段功能基于拦截器实现。考虑到用户的任何请求都可以刷新token的有效期,如下图所示,这部分由两个拦截器组成,第一个拦截器拦截所有请求,但都会放行,唯一的作用的token存在,刷新其在Redis中的有效期。第二个拦截没有token的请求。
- 拦截器配置
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns(
"/user/code",
"/user/login",
"/user/logout",
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot"
).order(1);
}
}
- 第一个拦截器
public class RefreshTokenInterceptor implements HandlerInterceptor {
// 没有注册为Bean,只能手动引入
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
this.stringRedisTemplate = redisTemplate;
}
@Override
public boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求头的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 2. 基于token获取用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
// 3. 用户是否存在
if(userMap.isEmpty()){
// 4. 放行
return true;
}
// 5. 转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6. 保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
// 刷新有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 7. 放行
return true;
}
@Override
public void afterCompletion(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 4. 清理资源
UserHolder.removeUser();
}
}
- 第二个拦截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler) throws Exception {
// 1. 拦截
if(UserHolder.getUser() == null){
response.setStatus(401);
return false;
}
// 7. 已登录,放行
return true;
}
@Override
public void afterCompletion(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 4. 清理资源
UserHolder.removeUser();
}
}
- 其中UserHolder为:
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}