【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 完成登录功能。

相关推荐
旖旎夜光3 小时前
C++(17)
c++·学习
大布布将军4 小时前
⚡️ 深入数据之海:SQL 基础与 ORM 的应用
前端·数据库·经验分享·sql·程序人生·面试·改行学it
专注于大数据技术栈4 小时前
java学习--StringBuilder
java·学习
川贝枇杷膏cbppg4 小时前
Redis 的 RDB 持久化
前端·redis·bootstrap
JIngJaneIL4 小时前
基于java+ vue农产投入线上管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
源代码•宸5 小时前
goframe框架签到系统项目(BITFIELD 命令详解、Redis Key 设计、goframe 框架教程、安装MySQL)
开发语言·数据库·经验分享·redis·后端·mysql·golang
川贝枇杷膏cbppg5 小时前
Redis 的 AOF
java·数据库·redis
qcwl665 小时前
操作系统 真象还原 学习笔记#13
笔记·学习
车载测试工程师5 小时前
CAPL学习-CAN相关函数-概述
网络协议·学习·capl·canoe
roman_日积跬步-终至千里5 小时前
【人工智能导论】08-学习-如何让计算机理解序列数据——用RNN/LSTM建模时序依赖,用文本嵌入表示序列元素
人工智能·rnn·学习