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

news2024/11/24 8:50:26

文章目录

  • 前言
  • 第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为面试赋能(持续更新中…)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web3 游戏周报(3.31-4.6)

【3.31-4.6】Web3 游戏行业动态&#xff1a; Xai 基金会宣布与 Reboot 达成合作拟支持 Pixel Vault: BattiePlan 游戏生态迁移 加密游戏 Hytopia 在节点销售获得 800 万美元后将于本月推出测试版 新加坡 Web3 游戏初创公司 Gomble Games 完成 1,000 万融资 NFT 卡牌游戏 Par…

PyCharm+PyQt5配置方法

一、前言 PyQt5PyQt5是一套Python绑定Digia QT5应用的框架。Qt库是最强大的GUI库之一PyQt5-toolsPyQt5中没有提供常用的Qt工具&#xff0c;比如图形界面开发工具Qt Designer&#xff0c;PyQt5-tools中包含了一系列常用工具Qt Designer可以通过Qt Designer来编写UI界面&#xf…

SPI外设简介

SPI外设简介 简介部分 可配置8/16位数据帧、高位先行/低位先行 SPI和I2C都是高位先行&#xff0c;串口是低位先行 PCLK是外设时钟&#xff0c;APB2是72MHz、APB1是36MHz SPI1的时钟频率比SPI2的大一倍 如果需要快速大量传输数据&#xff0c;可以使用DMA数据转运&#xff0…

Chrome谷歌下载入口

​hello&#xff0c;我是小索奇 发现好多人说谷歌浏览器在哪里下载呀&#xff0c;哪里可以找到&#xff1f; 你可能会心想&#xff0c;一个浏览器你还不会下载啊&#xff1f; 还真是&#xff0c;有很多伙伴找不到下载入口&#xff0c;为什么呢&#xff1f; Bing进行搜索&am…

2、java语法之循环、数组与方法(找工作版)

写在前面&#xff1a;整个系列文章是自己学习慕课相关视频&#xff0c;进行的一个总结。文章只是为了记录学习课程的整个过程&#xff0c;方便以后查漏补缺&#xff0c;找到对应章节。 文章目录 一、Java循环结构1、while循环2、do-while循环3、for循环4、嵌套循环5、break语句…

【技术揭秘】爬取网站或APP应用的几种常用方案:RPA、抓包工具、Python爬虫,你了解多少?

本来准备空闲之余尝试用RPA软件抓取数据&#xff0c;【AIRPA系列】1、利用AIRPA提升工作效率 应用场景 &#xff0c; 最近工作项目有点忙&#xff0c; RPA实操系列可能会晚点了&#xff08;自己真正实操后再写&#xff0c;copy别人的没啥意思&#xff09;。这里简单整理下爬取…

Docker快速上手及常用命令速查

Docker快速上手 安装 在ubuntu上安装docker: sudo apt-get install docker docker -v #查看版本在centos7上安装docker&#xff1a;(docker在YUM源的Extras仓库中) yum install docker systemctl start dockerdocker常用命令速查 #查看docker信息 docker info #查看本地镜…

【面试题】redis在工作中的使用场景有哪些?

前言&#xff1a;在实际工作中&#xff0c;Redis作为一种高性能的内存数据库和缓存系统&#xff0c;可以应用于多种场景&#xff0c;同时在面试过程中也经常被问到类似的问题&#xff0c;我们经常会被问的一脸懵逼&#xff0c;那今天我们就来总结一下redis的一些使用场景。 数据…

Linux--进程间的通信-匿名管道

进程间的通信 进程间通信&#xff08;IPC&#xff0c;Interprocess Communication&#xff09;是指在不同进程之间传输数据和交换信息的一种机制。它允许多个进程在同一操作系统中同时运行&#xff0c;并实现彼此之间的协作。 进程间通信方式&#xff1a; 管道&#xff08;Pi…

前端开发中地图定位与距离计算的应用实践

前端开发中地图定位与距离计算的应用实践 在前端开发中&#xff0c;地图功能的应用日益广泛&#xff0c;无论是用户位置的定位、目标距离的计算&#xff0c;还是地址的解析与展示&#xff0c;地图都发挥着不可替代的作用。本文将重点介绍前端开发中实现地图定位、距离计算以及…

电销卡呼叫必须录音吗

在现代的销售策略中&#xff0c;电话销售&#xff08;电销&#xff09;扮演着至关重要的角色。为了提高电销效率和质量&#xff0c;许多企业采用了电销卡来进行日常的电话营销活动。电销卡通常指的是专为电话销售设计的电话号码或线路&#xff0c;它们通常具备一些特殊的功能&a…

agi入门-大模型开发基础

AGI(Artifical General Inteligence)的到来还有多久&#xff1f; 乐观预测&#xff1a;明年主流预测&#xff1a;3-5年悲观预测&#xff1a;10年 AGI时代&#xff0c;AI无处不在&#xff0c;相关从来者将如何分&#xff1f; AI使用者&#xff1a;使用别人开发的AI产品AI产品…

精准识别更安全,横扫六大手指难题的鹿客指脉锁S6 Max来了

极致的自然动作、极致的精准识别、识别时间600毫秒……在4月10日鹿客指脉锁S6 Max发布会上&#xff0c;高密度的关键词让关注发布会的所有人都意识到&#xff0c;下一代智能锁真的来了。 鹿客也将新品S6 Max称为“行业内、搭载全新一代指脉技术的革新之作”。 1、十年回答&…

idea中输入法被锁定如何清除

今天遇到一个问题&#xff1f;idea中输入法被锁定了&#xff0c;无论怎么切换输入法&#xff0c;切换中英文&#xff0c;在idea中输出的均为英文内容&#xff0c;该如何解决呢&#xff1f;&#xff08;idea官网&#xff1a;JetBrains: 软件开发者和团队的必备工具&#xff09; …

VPP 负载均衡测试代码

1. 均衡的测试思想和流程说明。 先说一下理论&#xff0c; 然后后边才知道 代码逻辑。 调试了两天&#xff0c;这个代码终于通了。 由于时间关系&#xff0c; 画了一个粗略的图。另外这个代码只是流程通了&#xff0c;不过要帮助理解负载均衡我认为已经足够了。 下面是windo…

three.js尝试渲染gbl模型成功!(三)

参照教程&#xff1a;https://cloud.tencent.com/developer/article/2276766?areaSource102001.5&traceId88k805RaN_gYngNdKvALJ &#xff08;作者&#xff1a;九仞山&#xff09; 通过最近两天查three.js入门教程了解到 这玩应支持包括 .obj、.gltf等类型的模型结构。 g…

【vue/uniapp】使用 smooth-signature 实现 h5 的横屏电子签名

通过github链接进行下载&#xff0c;然后代码参考如下&#xff0c;功能包含了清空、判断签名内容是否为空、生成png/jpg图片等。 签名效果&#xff1a; 预览效果&#xff1a; 下载 smooth-signature 链接&#xff1a;https://github.com/linjc/smooth-signature 代码参考&a…

超图SuperMap-Cesium,地形图层,可以渲染一个或多个地形(地形可缓存DEM,TIN方式),webGL代码开发(2024-04-08)

1、缓存文件类型TIN格式&#xff0c;TIN的地形sct只能加一个 const viewer new Cesium.Viewer(cesiumContainer); viewer.terrainProvider new Cesium.CesiumTerrainProvider({isSct: true, // 是否为iServer发布的TIN地形服务,stk地形设置为falserequestWaterMask : true,…

MySQL学习笔记2——基础操作

基础操作 一、增删改查1、添加数据2、删除数据3、修改数据4、查询语句 二、主键三、外键和连接1、外键2、连接 一、增删改查 1、添加数据 INSERT INTO 表名[(字段名[,字段名]…)] VALUES (值的列表); --[]表示里面的内容可选添加数据分为插入数据记录和插入查询结果 插入数据…

[通俗易懂]《动手学强化学习》学习笔记2-第2、3、4章

文章目录 前言小总结&#xff08;前文回顾&#xff09;第二章 多臂老虎机2.2.2形式化描述 第三章 马尔可夫决策过程3.6 占用度量 代码3.6 占用度量 定理2 第四章 动态规划算法4.3.3 策略迭代算法 代码 总结 前言 参考&#xff1a; 《动手学强化学习》作者&#xff1a;张伟楠&a…