Redis从入门到精通(四)Redis实战(一)短信登录

文章目录

  • 前言
  • 第4章 Redis实战
    • 4.1 短信登录
      • 4.1.1 基于session实现短信登录
        • 4.1.1.1 短信登录逻辑梳理
        • 4.1.1.2 创建测试项目
        • 4.1.1.3 实现发送短信验证码功能
        • 4.1.1.4 实现用户登录功能
        • 4.1.1.5 实现登录拦截功能
        • 4.1.1.6 session共享问题
      • 4.1.2 基于Redis实现短信登录
        • 4.1.2.1 Key-Value的结构设计
        • 4.1.2.2 发送短信验证码功能改造
        • 4.1.2.3 用户登录功能改造
        • 4.1.2.4 登录拦截功能改造

前言

前面三章我们对Redis的基础知识进行了深入的学习,已经掌握了Redis的基本使用方法。

Redis从入门到精通(一)Redis安装与启动、Redis客户端的使用
Redis从入门到精通(二)Redis的数据类型和常见命令介绍
Redis从入门到精通(三)Jedis客户端、SpringDataRedis客户端

接下来的第4章开始进入实战环节,来学习一个Redis实战项目:短信登录。

第4章 Redis实战

4.1 短信登录

短信登录功能可以基于session实现,也可以基于Redis实现,下面分别介绍这两种方式。

4.1.1 基于session实现短信登录

4.1.1.1 短信登录逻辑梳理
  • 1)发送验证码

    用户在登录页面输入手机号,点击“发送验证码”按钮。后台收到请求后,校验手机号是否符合格式,如果不符合,则要求用户重新输入手机号。

    如果符合,后台随机生成6位数字的验证码,并将验证码保存到session,然后再通过短信的方式将验证码发送给用户(由于没有短信网关,可以使用打印日志的方式模拟)。

  • 2)登录与注册

    用户获取到验证码后,输入手机号和验证码,并点击“登录”按钮。后台收到请求后,从session中获取之前保存好的验证码,并与用户提交的验证码进行比对,如果不一致,则登录失败。

    如果一致,则根据手机号查询数据库的用户信息,如果用户不存在,则创建一个新的用户保存到数据库,如果存在,则直接获取;然后将用户信息保存到session中,方便后续获取当前登录用户信息。

  • 3)校验登录状态

    用户发起的请求,除了获取验证码、用户登录等少数指定的请求外,一般都要求用户必须处于登录状态。如果用户并非处于登录状态,说明这是一个非法请求,必须进行拦截。

    用户发起这些请求时,后台进行拦截,然后从session中拿到用户信息。如果没有获取到用户信息,则表示没有用户没有登录,要进行拦截。如果获取到了用户信息,则说明用户已经登录了,则放行。

在这里插入图片描述

4.1.1.2 创建测试项目

下面以一个SpringBoot项目来进行测试。

由于项目的创建不是学习的重点,这里不进行详述。该测试项目的代码已打包上传,有需要请到本文顶部下载绑定的代码资源。

4.1.1.3 实现发送短信验证码功能
  • 接口文档

    项目说明
    请求方式GET
    请求路径/user/code
    请求参数phone
    返回值
  • 代码实现

controller目录下的UserController类中实现该接口:

@GetMapping("/code")
public BaseResult<String> sendCode(String phone, HttpSession httpSession) {
    // 1.校验手机号格式
    if(RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return BaseResult.setFail("手机号输入有误!");
    }
    // 3.符合,随机生成6位数验证码
    String code = String.valueOf((int)(Math.random() * 900000 + 100000));
    // 4.将验证码保存到session
    httpSession.setAttribute("code", code);
    // 5.短信方式发送验证码
    log.info("发送短信验证码成功,验证码:{}", code);
    // 6.返回成功
    return BaseResult.setOk("短信验证码已成功发送至手机号" + phone + ",请注意查收!");
}
  • 功能测试

在这里插入图片描述

在这里插入图片描述

特别要注意的是,由于我们是使用HTTP工具进行发包测试的,所以需要设置一下Cookies,因为后端是利用Cookies中的JSESSIONID参数来创建session的。为了确保多次请求拿到的session是同一个,Cookies也必须要一致。

在这里插入图片描述

4.1.1.4 实现用户登录功能
  • 接口文档

    项目说明
    请求方式POST
    请求路径/user/login
    请求参数phone,code
    返回值
  • 代码实现

在UserController类中编写一个用户登录方法:

@Resource
private IUserService userService;

@PostMapping("/login")
public BaseResult login(@RequestBody LoginForm loginForm, HttpSession httpSession) {
    log.info("用户开始登录...{}", loginForm.toString());
    // 1.校验手机号
    if(RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
        // 2.如果不符合,返回错误信息
        return BaseResult.setFail("手机号输入有误!");
    }
    // 3.从session中获取验证码并校验
    Object cacheCode = httpSession.getAttribute("code");
    if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())) {
        // 4.验证码不一致,返回错误信息
        return BaseResult.setFail("验证码错误!");
    }
    // 5.一致,根据手机号查询用户
    User user = userService.query().eq("phone", loginForm.getPhone()).one();
    if(user == null) {
        // 6.用户不存在,则创建一个用户
        user = new User();
        user.setPhone(loginForm.getPhone());
        user.setNickName(loginForm.getPhone());
        userService.save(user);
    }
    // 7.将用户信息保存到session中
    httpSession.setAttribute("user", user);
    log.info("{} 登录成功...", loginForm.getPhone());
    return BaseResult.setOk("登录成功");
}
  • 功能测试

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.1.1.5 实现登录拦截功能
  • 接口文档

    用户发起的请求,除了获取验证码、登录等少数指定的请求外,一般都要求用户必须处于登录状态。如果用户并非处于登录状态,说明这是一个非法请求,必须进行拦截。

    可以通过拦截器来实现这个功能。

  • 代码实现

    要创建一个拦截器,只需要创建一个类LoginInterceptor,实现org.springframework.web.servlet.HandlerInterceptor接口,并重写其preHandle()方法。

    public class LoginInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1.获取session
            HttpSession session = request.getSession();
            // 2.获取session中的用户
            Object user = session.getAttribute("user");
            // 3.判断用户是否存在
            if(user == null){
                // 4.不存在,拦截,返回401状态码
                response.setStatus(401);
                return false;
            }
            // 5.存在,放行
            return true;
        }
    }
    

    其次,要对自定义的拦截器进行注册,让其生效:

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 登录拦截器,排除获取验证码和登录请求
            registry.addInterceptor(new LoginInterceptor())
                    .excludePathPatterns(
                            "/user/code",
                            "/user/login"
                    ).order(1);
        }
    }
    
  • 功能测试

在未登录的情况下发送请求/user/info,报401,说明没有通过拦截器的校验:

在这里插入图片描述

4.1.1.6 session共享问题

基于session实现短信登录,在服务端单机的情况下是没问题的,但如果服务端采用集群方式,则会出现session共享问题。

每个tomcat中都有一份属于自己的session。假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上肯定没有第一台服务器存放的session,所以此时整个登录拦截功能就会出现问题。

早期的解决方案是session拷贝,即当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样就可以实现session的共享。但这种方法也有弊端:第一,每台服务器中都有完整的一份session数据,服务器压力过大;第二,session拷贝数据时,可能会出现延迟

基于此,更好的解决方案是基于Redis来完成,而且Redis数据本身就是共享的。

4.1.2 基于Redis实现短信登录

4.1.2.1 Key-Value的结构设计

由于本案例中要存入Redis的数据比较简单,因此可以考虑使用String类型或Hash类型来存储数据。

这两种方式各有优点,String类型以JSON字符串保存数据,比较直观;而Hash类型可以将对象的每个字段独立存储,可以针对单个字段做CRUD,比较方便。最终根据实际需要选择即可,本案例选择使用String类型。

在基于session实现时,每个用户都有一个独享的session。但Redis的Key是共享的,因此不能再使用基于session方式中的"code""user"作为Key值。

在设计Key时,需要满足两点要求:第一,Key要有唯一性;第二,Key要方便携带。

在本案例中,如果采用手机号作为Key当然可以,它具备唯一性且方便携带,并且和验证码息息相关。但从安全角度看,手机号毕竟属于敏感数据,每次请求都携带手机号是不合适的。

综合考虑,本案例将采用login:code:{phone}作为保存验证码的Key;而保存用户信息的Key,会在后台生成一个随机串token,采用login:user:{token}作为Key,让用户每次请求都携带这个token。

4.1.2.2 发送短信验证码功能改造
  • 代码实现(关注修改部分)
@Resource
private StringRedisTemplate stringRedisTemplate;

@GetMapping("/code")
public BaseResult<String> sendCode(String phone, HttpSession httpSession) {
    // 1.校验手机号
    if(RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return BaseResult.setFail("手机号输入有误!");
    }
    // 3.符合,随机生成验证码
    String code = String.valueOf((int)(Math.random() * 900000 + 100000));
    // 4.将验证码保存到session
    // httpSession.setAttribute("code", code);

    // 修改:将验证码保存到Redis
    // 采用 login:code:{phone} 作为保存验证码的Key
    stringRedisTemplate.opsForValue().set("login:code:" + phone, code);

    // 5.短信方式发送验证码
    log.info("发送短信验证码成功,验证码:{}", code);
    // 6.返回成功
    return BaseResult.setOk("短信验证码已成功发送至手机号" + phone + ",请注意查收!");
}
  • 功能测试

调用获取验证码接口/user/code?phone=18922102123后,查看Redis中的数据:

在这里插入图片描述

4.1.2.3 用户登录功能改造
  • 代码实现(关注修改部分)
@PostMapping("/login")
public BaseResult<String> login(@RequestBody LoginForm loginForm, HttpSession httpSession) throws JsonProcessingException {
    log.info("用户开始登录...{}", loginForm.toString());
    // 1.校验手机号
    if(RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
        // 2.如果不符合,返回错误信息
        return BaseResult.setFail("手机号输入有误!");
    }
    // 3.从session中获取验证码并校验
    // Object cacheCode = httpSession.getAttribute("code");

    // 修改:从Redis中获取验证码
    String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + loginForm.getPhone());

    if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())) {
        // 4.验证码不一致,返回错误信息
        return BaseResult.setFail("验证码错误!");
    }
    // 5.一致,根据手机号查询用户
    User user = userService.query().eq("phone", loginForm.getPhone()).one();
    if(user == null) {
        // 6.用户不存在,则创建一个用户
        user = new User();
        user.setPhone(loginForm.getPhone());
        user.setNickName(loginForm.getPhone());
        userService.save(user);
    }
    // 7.将用户信息保存到session中
    // httpSession.setAttribute("user", user);

    // 修改:将用户信息保存到Redis中
    // 随机生成token
    String token = UUID.randomUUID().toString();
    log.info("token = {}", token);
    // 保存到Redis
    stringRedisTemplate.opsForValue().set("login:user:" + token, new ObjectMapper().writeValueAsString(user));
    // 设置token有效期:2小时
    stringRedisTemplate.expire("login:user:" + token, 2, TimeUnit.HOURS);
    
    log.info("{} 登录成功...", loginForm.getPhone());
    // 将token返回给前端
    return BaseResult.setOkWithData(token);
}
  • 功能测试

调用用户登录接口/user/login后,查看Redis中的数据:

在这里插入图片描述

在这里插入图片描述

4.1.2.4 登录拦截功能改造
  • 代码实现(关注修改部分)
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        // HttpSession session = request.getSession();
        // 2.获取session中的用户
        // Object user = session.getAttribute("user");

        // 修改:基于用户token获取Redis中的用户信息
        // 获取用户携带的token
        String token = request.getHeader("authorization");
        log.info("token from client => {}", token);
        // 基于token获取Redis中的用户信息
        String userJosn = stringRedisTemplate.opsForValue().get("login:user:" + token);
        log.info("user from redis => {}", userJosn);
        // 转为Java对象
        User user = null;
        if(StrUtil.isNotBlank(userJosn)) {
            user = new ObjectMapper().readValue(userJosn, User.class);
        }

        // 3.判断用户是否存在
        if(user == null){
            // 4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        // 5.存在,放行
        return true;
    }
}

注册LoginInterceptor时传入StringRedisTemplate实例:

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器,排除获取验证码和登录请求
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/user/code",
                        "/user/login"
                ).order(1);
    }
}
  • 功能测试

调用查询用户详情接口/user/info(项目中暂未编写该Controller方法)。当携带一个错误token时,报401,说明未通过拦截器校验:

在这里插入图片描述

携带一个正确token,报404,说明已经通过了拦截器校验:

在这里插入图片描述

本节完,更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • 再探Java为面试赋能(持续更新中…)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/520971.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MATLAB - 用命令行设计 MPC 控制器

系列文章目录 前言 本例演示如何通过命令行创建和测试模型预测控制器。 一、定义工厂模型 本示例使用《使用 MPC Designer 设计控制器》中描述的工厂模型。创建工厂的状态空间模型&#xff0c;并设置一些可选的模型属性&#xff0c;如输入、状态和输出变量的名称和单位。 % co…

(阿里云万网)-域名注册购买实名流程

1&#xff0c;进入阿里云网万官网 输入网址 https://wanwang.aliyun.com/?spm5176.161059.J_3207526240.33.581fa505OGhzsW 注册域名 &#xff0c;域名推荐com&#xff08;国际顶级域名&#xff09; &#xff0c;cn&#xff08;国内顶级域名&#xff09;。其中cn价钱比com便…

状态机高阶讲解-16

2534 01:44:41,942 --> 01:44:44,140 那我们&#xff0c;你看这里 2535 01:44:45,170 --> 01:44:46,452 你看这里改之后 2536 01:44:46,452 --> 01:44:48,833 它代码就做新的调整了嘛 2537 01:44:48,833 --> 01:44:49,200 对吧 2538 01:44:50,150 --> 01:44…

smbms:超市订单管理系统(项目分析)

smbms&#xff1a;超市订单管理系统&#xff08;项目分析&#xff09; 文章目录 smbms&#xff1a;超市订单管理系统&#xff08;项目分析&#xff09;前言一、项目介绍&#xff1a;二、项目来源&#xff1a;三、架构图&#xff1a;&#xff08;流程图&#xff09;四、使用了什…

【论文阅读】CompletionFormer:深度完成与卷积和视觉变压器

【论文阅读】CompletionFormer:深度完成与卷积和视觉变压器 文章目录 【论文阅读】CompletionFormer:深度完成与卷积和视觉变压器一、介绍二、联系工作深度完成Vision Transformer 三、方法四、实验结果 CompletionFormer: Depth Completion with Convolutions and Vision Tran…

腾讯云添加域名后不生效

问题原因 添加域名后不生效可能是因为没有加CDN域名解析 解决步骤

手动实现Tomcat底层机制+自己设计Servlet

文章目录 1.Tomcat整体架构分析自己理解 2.第一阶段1.实现功能2.代码1.TomcatV1.java 3.调试阶段1.阻塞在readLine导致无法返回结果 4.结果演示 3.第二阶段1.实现功能2.代码1.RequestHander.java2.TomcatV2.java 3.调试阶段1.发现每次按回车会接受到两次请求 4.结果演示 4.第三…

【Python使用】嘿马头条完整开发md笔记第4篇:数据库,1 方案选择【附代码文档】

嘿马头条项目从到完整开发笔记总结完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;课程简介&#xff0c;ToutiaoWeb虚拟机使用说明1 产品介绍,2 原型图与UI图,3 技术架构,4 开发,1 需求,2 注意事项。数据库&#xff0c;理解ORM1 简介,2 安装,3 数据库连接…

C++要点细细梳理(下)(内存分配、异常处理、template和文件读写)

4. 类动态内存分配 4.1 C语言动态内存分配&#xff1a;malloc和free 4.2 C动态内存分配&#xff1a;new和delete 思考&#xff1a;定义一个对象和定义一个普通变量有何区别? 普通变量:分配足够空间即可存放数据对象:除了需要空间&#xff0c;还要构造/析构 类比&#xff1a;…

关于 VScode, 点击文件右键或者在文件夹中没有 【 在vscode中打开选项】 解决办法

关于 VScode, 点击文件右键或者在文件夹中没有 【 在vscode中打开选项】 解决办法 段子手-168 2024-4-6 1、在任意位置创建一个文本文件。如&#xff1a;a.txt 2、复制以下代码到 a.txt 文本文件中。 &#xff08;注&#xff1a; 以 ; 开头的 , 是备注信息 , 不需要做任何修…

代码随想录第19天

654. 最大二叉树 已解答 中等 相关标签 相关企业 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。递归地在最大值 右边 的 子数组后缀…

影院座位选择简易实现(uniapp)

界面展示 主要使用到uniap中的movable-area&#xff0c;和movable-view组件实现。 代码逻辑分析 1、使用movable-area和movea-view组件&#xff0c;用于座位展示 <div class"ui-seat__box"><movable-area class"ui-movableArea"><movab…

【QT学习】5.控件

一。控件的了解 1.控件作用 拖拽页面&#xff0c;所见即所得。 2.创建控件 3.向qt项目中添加资源 3.1显示图片 项目右键--》add new 创建结果&#xff1a; 添加资源到文件中 补充&#xff1a;使用代码的方式添加图片 3.2显示动图 1.添加动图资源 添加资源 2.显示动图 3.3显示…

基于单片机电子硬币储存器的设计

**单片机设计介绍&#xff0c;基于单片机电子硬币储存器的设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机电子硬币储存器的设计概要主要涵盖了硬件设计、软件设计、硬币识别、计数与储存等核心功能。以下是对该设…

OWASP TOP10 漏洞详解

前言 该内容是 OWASP TOP 10 的学习笔记&#xff0c;笔记内容来源 B 站龙哥的视频【12.Top漏洞10&#xff1a;服务器请求伪造_哔哩哔哩_bilibili】 一、访问控制崩溃 概念 未对通过身份验证的用户实施恰当的访问控制。攻击者可以利用这些缺陷访问未经授权的功能或数据&#xf…

【Linux】环境基础开发工具使用——gcc/g++使用

Linux编译器-gcc/g使用 1. 背景知识 1. 预处理&#xff08;进行宏替换 ) 2. 编译&#xff08;生成汇编 ) 3. 汇编&#xff08;生成机器可识别代码&#xff09; 4. 连接&#xff08;生成可执行文件或库文件 ) 2. gcc如何完成 格式 gcc [ 选项 ] 要编译的文件 [ 选…

阿德勒、荣格、埃里克森、霍妮、弗洛姆、沙利文的新精神分析理论

新精神分析理论&#xff0c;强调自我的自主性及其整合与调节功能&#xff0c;强调文化和社会因素对人格的重大影响。 一、阿德勒的个体心理学 阿德勒&#xff0c;是一个男人努力克服自卑感的优秀样板。阿德勒写了《超越与自卑》。 阿德勒&#xff0c;向意识层面扩展精神分析…

c++11的重要特性3

目录 1、lambda表达式 C98中的一个例子 lambda表达式的 lambda表达式语法 函数对象与lambda表达式 3、可变参数模板 递归获取 逗号表达式展开参数包 2、包装器 function包装器 bind 1、lambda表达式 C98中的一个例子 在C98中&#xff0c;如果想要对一个数据集合中的元素进…

NetSuite 自定义记录类型的权限控制

在近期的一个定制项目中&#xff0c;遭受了一次用户洗礼。有个好奇宝宝把我们的一个自定义类型的表记录进行了删除&#xff0c;导致一个重要功能失败。算是给我们扎实上了一课。自定义类型的权限也需要重视起来。所以&#xff0c;今朝我们记录下这个设置&#xff0c;同时写给未…

C++ setw() 函数

C setw() 函数 分类 编程技术 C setw() 函数用于设置字段的宽度&#xff0c;语法格式如下&#xff1a; setw(n) n 表示宽度&#xff0c;用数字表示。 setw() 函数只对紧接着的输出产生作用。 当后面紧跟着的输出字段长度小于 n 的时候&#xff0c;在该字段前面用空格补齐&…