基于Session实现登录、校验的流程以及存在的问题
这种使用session在单体tomcat服务器时没有问题,当在tomcat集群时,tomcat的session存储空间不是共享的,当在集群下,某台tomcat服务器的session存储了用户信息时,当用户再次发起请求,负载均衡将请求映射到其他tomcat时,此时的tomcat的session没有用户的信息,导致用户需要再次登录。所以,基于这种弊端,使用redis缓存用户信息。
提示:使用tomcat服务器session存储信息时,尽量存储少量信息,存储信息较大时会影响tomcat的性能。
这种使用session在单体tomcat服务器时没有问题,当在tomcat集群时,tomcat的session存储空间不是共享的,当在集群下,某台tomcat服务器的session存储了用户信息时,当用户再次发起请求,负载均衡将请求映射到其他tomcat时,此时的tomcat的session没有用户的信息,导致用户需要再次登录。所以,基于这种弊端,使用redis缓存用户信息。
下面介绍使用redis实现共享session登录校验。大体登录、校验流程不变,使用redis代替session。
发送验证码
Result sendCode(String phone, HttpSession session);
@Override
public Result sendCode(String phone, HttpSession session) {
if (RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误!");
}
String code = RandomUtil.randomNumbers(6);
//session.setAttribute("code",code);
redisTemplate.opsForValue().set(LOGIN_CODE_KEY +":" + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
logger.info("验证码为:" + code);
return Result.ok();
}
@PostMapping("/code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone,session);
}
短信验证码登录、注册
Result login(LoginFormDTO loginForm, HttpSession session);
public Result login(LoginFormDTO loginForm, HttpSession session) {
System.out.println("UserServiceImpl:" +session.toString());
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误!");
}
String code = loginForm.getCode();
//String cacheCode = (String)session.getAttribute("code");
String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY +":" + phone);
if (cacheCode == null || !cacheCode.equals(code)){
return Result.fail("验证码错误");
}
User user = query().eq("phone", phone).one();
if (user == null){
user = createUserWithPhone(phone);
}
//session.setAttribute("user",user);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue)->fieldValue.toString()));
String token = UUID.randomUUID().toString(true);
String tokenKey = LOGIN_USER_KEY + ":"+ token ;;
redisTemplate.opsForHash().putAll(tokenKey,map);
redisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
return Result.ok(token);
}
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
Result login = userService.login(loginForm, session);
return login;
}
项目中的拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RefreshTokenIncepter(redisTemplate)).order(0);
registry.addInterceptor(new LoginIncepter())
.excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**",
"/shop-type/**","/upload/**","/voucher/**").order(1);
}
}
拦截器代码:每次请求后会刷新redis中的token(只会刷新与用户信息相关操作的token,浏览店铺等与用户无关的token不会刷新)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (StrUtil.isBlank(token)){
return true;
}
String key = RedisContants.LOGIN_USER_KEY + ":" + token;
Map<Object, Object> usermap = redisTemplate.opsForHash().entries(key);
if (usermap == null){
return true;
}
UserDTO userDTO = new UserDTO();
BeanUtil.fillBeanWithMap(usermap,userDTO,false);
UserHolder.saveUser(userDTO);
//刷新token
redisTemplate.expire(key,RedisContants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
可以看出,这些与用户信息无关的路径不需要拦截,那么拦截器拦截的路径都是与用户信息有关(与登录有关的路径)。当用户浏览店铺等与用户信息相关的路径时,达到缓存时间删除用户信息后,用户再次返回下单、加购等与用户相关信息时,缓存中已经没有用户信息,此时用户还需要登录,这显然不符合业务场景。
改进:双拦截器
第一个拦截器拦截所有请求,如果用户已经登录,则只要用户发起任何非logout操作,都会刷新token,这样可以解决上述情况的问题。如果用户没有登录,经过第一个拦截器则放行,经过第二个拦截器时发现用户要发起下单、加购等请求时,会拦截,如果发起浏览店铺等请求则放行。