【Redis场景1】用户登录注册

细节回顾:

关于cookiesession不熟悉的朋友;

建议阅读该博客https://www.cnblogs.com/ityouknow/p/10856177.html

执行流程:

【Redis场景1】用户登录注册

在单体模式下,一般采用这种模式来存储,传递、认证用户登录、注册等信息;

如果浏览器禁用Cookies,****如何保障整个机制的正常运转。

  1. url拼接或者POST请求:每个请求都携带SessionID
  2. Token机制:在用户登录或者注册的时候,与用户信息绑定一个随机字符串,用于用户状态管理

在分布式下的Session问题:

【Redis场景1】用户登录注册

为了支撑更大的流量,后台往往需要在多台服务器中部署,那如果用户在 A 服务器登录了,第二次请求跑到服务 B 就会出现登录失效问题如何解决?

  • Nginx ip_hash 策略,服务端使用 Nginx 代理,每个请求按访问 IP hash分配,这样来自同一 IP 固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次分发到服务器 B 的现象。
  • Session 复制,任何一个服务器上的 Session 发生改变(增删改),该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点。
  • 共享 Session,服务端无状态话,将用户的 Session 等信息使用缓存中间件来统一管理,保障分发到每一个服务器的响应结果都一致。

第一种策略(Nginx ip_hash 策略)可以看该博客:https://www.cnblogs.com/xbhog/p/16929786.html

我们主要实现第三种:通过缓存中间件来统一管理。

解决痛点:

在原来场景中存在的Session不互通的问题(Session数据拷贝),该解决方式有以下问题:

  1. 每台服务器中都有完整的一份session数据,服务器压力过大。
  2. session拷贝数据时,可能会出现延迟

所以我们需要采用的中间件需要有以下特征(Redis):

  1. 数据共享
  2. 基于内存读取
  3. 满足数据存储格式(KEY:VALUE)

实现场景:

该场景实现流程:以下分析结合部分代码(聚焦于redis的实现);

完整后端代码可在Github中获取:https://github.com/xbhog/hm-dianping

【Redis场景1】用户登录注册

开发流程:

【获取验证码流程】前端根据手机号提交获取验证码请求:触发sendCode方法:

  1. 校验手机号合法性
  2. 生成验证码
  3. 保存验证码到redis
  4. 发送验证码(模拟实现,未调用第三方平台)
  5. 结束

在第3步的实现如下:

stringRedisTemplate.opsForValue().set(PHONE_CODE_KEY+phone,code,2L,TimeUnit.MINUTES); 

使用的stringRedisTemplate继承于RedisTemplate,局限:key和value必须是String类型:

public class StringRedisTemplate extends RedisTemplate<String, String> { ...... } 

设置验证码Key值:phone:code:,code为随机6位字符串。

设置key的过期时间。

【登录功能】前端根据手机号和验证码发送登录功能(如上图):触发login方法:

  1. 校验手机号合法性

  2. 校验验证码合法性:从redis中获取

  3. 数据库通过手机号查询用户

    1. 不存在:创建用户
    2. 存在:执行后续逻辑(4)
  4. UUID生成随机TOKEN

  5. 将用户信息存入Redis中,设置过期时间

  6. 返回前端Token

第2步的实现如下:

String redisCode = stringRedisTemplate.opsForValue().get(PHONE_CODE_KEY + loginForm.getPhone()); if(StringUtils.isBlank(loginForm.getCode()) || !loginForm.getCode().equals(redisCode)){     return Result.fail("验证码错误"); } 

第4、5步的实现如下:

//随机生成token,作为登录令牌 String token = UUID.randomUUID().toString(true); //保存到redis中 String tokenKey = LOGIN_USER_KEY + token; stringRedisTemplate.opsForHash().putAll(tokenKey,userBeanToMap); //设置过期时间 stringRedisTemplate.expire(tokenKey,30L, TimeUnit.MINUTES); 

设置用户KEY"login:token:",这里redis的使用的数据结构是Map,方便对单一字段操作。

使用了hashmap结构,需要单独对tokenKey设置过期时间(30m);

场景问题:

Redis Key续期问题:

在设置token的时候,在redis给的过期时间是30分钟,这里就有个问题,用户在30分钟内,结束请求,那没有问题,但只要用户的在线时间超过30分钟,redis删除token,直接给用户强制下线了;这个实在是不符合实际场景。

解决方式:

在请求的过程总给加入一层拦截器,用来刷新Token的存活时间。

子问题:

对于拦截器,我们不能拦截所有的路径,比如获取验证码请求,用户登录,首页等;

    1. 用户在所拦截的范围内:则可执行刷新Token操作
    2. 用户不在拦截的范围内:无法执行刷新Token操作

子问题解决:

增加两层拦截器,第一层拦截全部请求路径,第二层拦截器基于第一层信息,对未登录的请求进行拦截。

需要设置两层拦截器的优先级,

order():指定要使用执行器顺序。默认值为0(最高)

@Configuration public class MybatisConfig implements WebMvcConfigurer {      @Resource     private StringRedisTemplate stringRedisTemplate;      @Override     public void addInterceptors(InterceptorRegistry registry) {         registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(                 "/shop/**",                 "/voucher/**",                 "/shop-type/**",                 "/upload/**",                 "/blog/hot",                 "/user/code",                 "/user/login"         ).order(1);         registry.addInterceptor(new RefreshTokeInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);     } } 

【Redis场景1】用户登录注册

【拦截器1实现】所有路径拦截,刷新登录Token令牌存活时间。

  1. 获取token

  2. 查询redis用户

    1. 存在,不拦截(执行3)
    2. 不存在,拦截
  3. 用户信息保存到threadLocal

  4. 刷新Token有效期

  5. 放行

相关代码:

/**  * @author xbhog  * @describe: 拦截器实现:校验用户登录状态  * @date 2022/12/7  */ public class RefreshTokeInterceptor implements HandlerInterceptor {      private StringRedisTemplate stringRedisTemplate;      public RefreshTokeInterceptor(StringRedisTemplate stringRedisTemplate){         this.stringRedisTemplate = stringRedisTemplate;     }      @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         String token = request.getHeader("authorization");         //如果页面没有登录,则没有token,直接放行给下一个拦截器         if(StringUtils.isEmpty(token)){             return true;         }         String tokenKey = LOGIN_USER_KEY + token;         Map<Object, Object> userRedis = stringRedisTemplate.opsForHash().entries(tokenKey);         if(userRedis.isEmpty()){             return true;         }         UserDTO userDTO = BeanUtil.fillBeanWithMap(userRedis, new UserDTO(), false);         //用户存在,放到threadLocal         UserHolder.saveUser(userDTO);         //登录续期         stringRedisTemplate.expire(tokenKey,30L, TimeUnit.MINUTES);         return true;     }      @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         UserHolder.removeUser();     } } 

【拦截器2实现】需要登录的路径拦截;

实现代码:

/**  * @author xbhog  * @describe: 拦截器实现:校验用户登录状态  * @date 2022/12/7  */ public class LoginInterceptor implements HandlerInterceptor {     @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;     } } 

输入及参考

cookie和session

斐波那契散列和hashMap实践

发表评论

评论已关闭。

相关文章