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

相关推荐
爱喝水的鱼丶2 小时前
SAP-ABAP:S/4 HANA ABAP 新特性入门:与 ECC ABAP 的核心差异、架构优势与学习路径规划
运维·数据库·性能优化·sap·abap·hana·开发交流
余槐i9 小时前
PostgreSQL 函数索引报错 must be immutable:3种绕法与代价
数据库·后端·sql·postgresql·性能优化
数据库小学妹9 小时前
PostgreSQL表膨胀怎么办?autovacuum清不动的根因与排查命令
数据库·postgresql·表膨胀·mvcc·数据库运维
mister_guo9 小时前
MySQL-1267错误:排序规则冲突的原因与解决方案
数据库·mysql
骇客野人10 小时前
在麒麟操作系统上 分别安装 jdk1.8, 达梦数据库,redis ,memcache 的操作实施方案和步骤
数据库·redis·memcached
遇印记10 小时前
传输层,网络层
运维·服务器·网络·windows·学习
clorinda11 小时前
PyQt5学习:从界面搭建到与机器学习实战
学习
美狐美颜SDK开放平台11 小时前
直播APP如何实现美白滤镜功能?视频美颜SDK技术开发方案详解
前端·数据库·人工智能·音视频·美颜sdk·直播美颜sdk·视频美颜sdk
花间相见13 小时前
【计算基础|MySQL01】【上手】安装连接、数据类型与建库建表
数据库·后端
花间相见13 小时前
【计算基础|MySQL02】【上手】SQL 增删改查
数据库·后端