基于redis实现共享session登录
1.集群session共享的问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失问题
替代方案应该满足:
数据共享
内存存储
key、value结构
2.基于redis实现session共享登录
需要生成token令牌,通过token的值去确定哪个用户做出请求
将token作为key用户信息作为值存储到redis中,利用的模板时StringRedisTemplate这样不仅可以节省服务器的空间还增加可读性
@Override public Result login(LoginFormDTO loginForm, HttpSession session) {//1.校验手机号的格式String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {//2.不一致直接报错return Result.fail("手机号错误");}//3.比较验证码String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);String code = loginForm.getCode();if(session==null || !cacheCode.equals(code)){//4.不一致直接报错return Result.fail("错误信息");}//5.根据手机号查询用户LambdaQueryWrapper<User> query = new LambdaQueryWrapper<>();query.eq(User::getPhone,loginForm.getPhone());User user = this.getOne(query);if(user==null){//6.不存在直接创建新用户保存到数据库中user=createUserWithPhone(loginForm.getPhone());}UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);//7.最终将用户信息保存到redis中 使用UserDTO保护用户的隐私//7.1生成tokenString tokenKey = UUID.randomUUID().toString();/** 当用户第一次登录后,服务器生成一个token并将此token返回给客户端,* 以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。* *///7.2 将user转为hash存储Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));//7.3存储stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+tokenKey,userMap);//7.4设置存储的生命周期/** 这里设置过30min会自动从redis中剔除 但我们想要的效果是30min没有用到token时剔除* 这时我们需要去拦截器里面设置并且刷新token* */stringRedisTemplate.expire(LOGIN_USER_KEY+tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);//8.返回给客户端return Result.ok(tokenKey); }
拦截器的设定之前设置的拦截器放行了部分页面,如果我们只访问那种部分页面时拦截器直接放行导致不能够刷新redis中的数据,现在设定两个拦截器,一个负责专门刷新redis的生命周期,另一个负责拦截
//注册拦截器 及其相关配置 @Configuration public class MvcConfig implements WebMvcConfigurer {//添加拦截器@Autowiredprivate StringRedisTemplate redisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//order的值越小 执行的优先级越高//登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);//刷新拦截器 拦截所有registry.addInterceptor(new ReFreshTokenInterceptor(redisTemplate)).order(0).addPathPatterns("/**"); } }
@SuppressWarnings("all") //创建拦截器 public class ReFreshTokenInterceptor implements HandlerInterceptor {//有注册拦截器注入beanprivate StringRedisTemplate stringRedisTemplate; public ReFreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;} @Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取tokenString token = request.getHeader("authorization");if(StringUtils.isEmpty(token)){return true;}//2.通过token从redis中拿到用户信息Map<Object, Object> user = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY+token);if(user.isEmpty()){ return true;}//4.存在 将Map对象转为UserDto对象UserDTO userDTO = BeanUtil.fillBeanWithMap(user, new UserDTO(), false);//5.存储到ThreadLocal中UserHolder.saveUser(userDTO);//6.设置token的刷新时间stringRedisTemplate.expire(LOGIN_CODE_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);//5.放行return true;} @Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//避免造成内存泄露UserHolder.removeUser();} }
public class LoginInterceptor implements HandlerInterceptor {//有注册拦截器注入bean@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.只需要判断ThreadLocal中有没有用户信息if(UserHolder.getUser()==null){//响应未授权的状态信息response.setStatus(401);//拦截return false;}//放行return true;} @Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//避免造成内存泄露UserHolder.removeUser();} }