基于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生成token String 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 { //添加拦截器 @Autowired private StringRedisTemplate redisTemplate; @Override public 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 { //有注册拦截器注入bean private StringRedisTemplate stringRedisTemplate; public ReFreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1.获取token String 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; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //避免造成内存泄露 UserHolder.removeUser(); } }
public class LoginInterceptor implements HandlerInterceptor { //有注册拦截器注入bean @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1.只需要判断ThreadLocal中有没有用户信息 if(UserHolder.getUser()==null){ //响应未授权的状态信息 response.setStatus(401); //拦截 return false; } //放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //避免造成内存泄露 UserHolder.removeUser(); } }