用户登录是一种常用功能。这里记录一下基于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层
@Overridepublic 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 生成tokenString token = UUID.randomUUID().toString(true);// 7.2 保存到redis,hash存储,使用Map减少与服务器交互次数UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 将所有字段转为stringMap<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);// 返回tokenSystem.out.println(token);return Result.ok(token);}
2.校验登录状态代码
这段功能基于拦截器实现。考虑到用户的任何请求都可以刷新token的有效期,如下图所示,这部分由两个拦截器组成,第一个拦截器拦截所有请求,但都会放行,唯一的作用的token存在,刷新其在Redis中的有效期。第二个拦截没有token的请求。
- 拦截器配置
@Configuration
public class MvcConfig implements WebMvcConfigurer {@ResourceStringRedisTemplate stringRedisTemplate;@Overridepublic 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;}@Overridepublic boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler) throws Exception {// 1. 获取请求头的tokenString 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. 转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6. 保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);// 刷新有效期stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);// 7. 放行return true;}@Overridepublic 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 {@Overridepublic 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;}@Overridepublic 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();}
}