【Redis学习 | 第4篇】Redis代替Session实现登录 —— 黑马点评的短信登录功能

文章目录

  • 完成任务
    • [1. 集群 Session 共享问题](#1. 集群 Session 共享问题)
      • [1.1 什么是 session 共享问题?](#1.1 什么是 session 共享问题?)
      • [1.2 Session 替代方案 ------ Redis](#1.2 Session 替代方案 —— Redis)
    • [2. Redis 代替 Session 实现登录](#2. Redis 代替 Session 实现登录)
  • 总结

完成任务

1. 集群 Session 共享问题

1.1 什么是 session 共享问题?

session共享问题就是:多态 Tomcat 不共享 Session 存储空间,当请求不同 Tomcat 服务器时,会导致数据丢失的问题。

为了并发,对 Tomcat 做水平扩展,形成多个负载均衡的集群 。**当请求进入 Nginx,会进行负载均衡,在多台 Tomcat 之间做轮询。**每个 tomcat 有自己的 session 空间。

当第一次请求负载到第一台 tomcat 时,会将数据存储在该 tomcat 的 session 中,比如说验证码。

当第二次请求负载到另一台 tomcat 时,session 中没有存储验证码数据,此时,就会出现这种情况:明明验证码是正确的,但一直报错 "验证码不正确"。

1.2 Session 替代方案 ------ Redis

寻找能够替代 Session 的解决方案,应满足:

  1. 数据共享:Session 的问题就是数据共享问题
  2. 内存存储:Session 是基于内存存储的
  3. key、value结构:Session 中保存的数据是 key-value 结构的

------> Redis

2. Redis 代替 Session 实现登录

  1. 将生成的验证码保存到 Redis
  2. 登录注册时根据 key 获取 Redis 中的验证码,验证成功,将 token 作为 key ,返回用户数据
    返回用户数据时,使用 token,而不使用每个用户的手机号 ,原因在于:要将 key 返回给前端,如果将手机作为 key 返回给前端,有信息泄露的风险。
  3. 携带 token 校验登录状态,根据能否从 Redis 中获取用户,判断是否登录。

发送验证码,利用 StringRedisTemplate 将验证码存入 Redis:

java 复制代码
	@Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 发送手机验证码
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1. 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2. 不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3. 符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4. 将验证码存入 Redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5. 发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);

        // 6. 返回成功信息
        return Result.ok();
    }

登录:

(1)从 Redis 中获取验证码,进行校验

(2)将查询到的用户信息保存到 Redis,以 token 作为 key,并设置有效期

(3)将 token 返回给前端

java 复制代码
	/**
     * 登录
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号
        if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
            // 不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }

        // 2. 从Redis中获取验证码, 并校验验证码
        String checkCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+loginForm.getPhone());
        if (checkCode == null || !checkCode.equals(loginForm.getCode())) {
            // 3. 不一致,报错
            return Result.fail("验证码错误!");
        }

        // 4. 一致,根据手机号查询用户
        User user = query().eq("phone", loginForm.getPhone()).one();

        // 5. 判断用户是否存在
        if (user == null) {
            // 6. 不存在,创建新用户并保存
            user = createUserWithPhone(loginForm.getPhone());
        }

        // 7. 保存用户信息到 Redis
        // 7.1 随机生成 token,作为登录令牌
        String token = UUID.randomUUID().toString(true);  // UUID 使用的是 hutool 的实现
        // 7.2 将 UserDTO 对象转为 HashMap 存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3 将 token 存入 Redis
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4 设置 token 有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8. 返回token
        return Result.ok(token);
    }

在登录拦截器中,校验登录状态,前端在请求头中携带 token:

java 复制代码
	private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(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 (StrUtil.isBlank(token)) {
            // 不存在,拦截,返回 401 状态码
            response.setStatus(401);
            return false;
        }
        // 2. 基于 token 获取 redis 中的用户信息
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3. 判断用户是否存在
        if (userMap.isEmpty()) {
            // 4. 不存在,拦截,返回 401 状态码
            response.setStatus(401);
            return false;
        }
        // 5. 将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 6. 保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7. 刷新 token 有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8. 放行
        return true;
    }

总结

了解 Session 存在的共享问题,Redis 的出现解决这种问题。利用 Redis 完成登录功能。

相关推荐
jiayou642 小时前
KingbaseES 实战:审计追踪配置与运维实践
数据库
NineData14 小时前
NineData 迁移评估功能正式上线
数据库·dba
雨中飘荡的记忆18 小时前
大流量下库存扣减的数据库瓶颈:Redis分片缓存解决方案
java·redis·后端
NineData19 小时前
数据库迁移总踩坑?用 NineData 迁移评估,提前识别所有兼容性风险
数据库·程序员·云计算
赵渝强老师21 小时前
【赵渝强老师】PostgreSQL中表的碎片
数据库·postgresql
全栈老石1 天前
拆解低代码引擎核心:元数据驱动的"万能表"架构
数据库·低代码
曲幽1 天前
FastAPI分布式系统实战:拆解分布式系统中常见问题及解决方案
redis·python·fastapi·web·httpx·lock·asyncio
倔强的石头_2 天前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
jiayou643 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤4 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库