使用Redis实现短信验证码登录功能

news2024/11/15 22:40:23

一、概述

目前微信小程序或网站的登录方式大部分采取了微信扫码或短信验证码等方式,为什么短信验证码登录方式会受到互联网公司的青睐,因为其确实有许多好处:

  • 方便快捷:用户无需记忆复杂的用户名和密码,只需通过短信验证码就可以快速登录。
  • 安全性高:短信验证码通常是由运营商或第三方服务提供商发送的,具有较高的安全性。
  • 防止恶意攻击:通过短信验证码登录可以有效防止恶意攻击,例如密码被盗、网站挂马等情况。
  • 方便记忆:短信验证码可以使用户更方便地记忆密码,不用担心密码被遗忘。
  • 兼容性好:短信验证码适用于各种类型的网站和应用程序,可以在不同的设备上使用。
    综上所述,短信验证码登录是一种安全、方便、快捷的登录方式,有助于提高用户的使用体验。

本文主要讲解的是如何使用Redis实现短信验证码登录的过程。

如图:
在这里插入图片描述

二、登录流程

当用户注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。

流程图:

在这里插入图片描述

三、具体实现

1、前端页面

1.1 html部分

<div id="app">
  <div class="login-container">
    <div class="header">
      <div class="header-back-btn" @click="goBack" ><i class="el-icon-arrow-left"></i></div>
      <div class="header-title">手机号码快捷登录&nbsp;&nbsp;&nbsp;</div>
    </div>
    <div class="content">
      <div class="login-form">
        <div style="display: flex; justify-content: space-between">
          <el-input style="width: 60%" placeholder="请输入手机号" v-model="form.phone" >
          </el-input>
          <el-button style="width: 38%" @click="sendCode" type="success" :disabled="disabled">{{codeBtnMsg}}</el-button>
        </div>
        <div style="height: 5px"></div>
        <el-input placeholder="请输入验证码" v-model="form.code">
        </el-input>
        <div style="text-align: center; color: #8c939d;margin: 5px 0">未注册的手机号码验证后自动创建账户</div>
        <el-button @click="login" style="width: 100%; background-color:#f63; color: #fff;">登录</el-button>
        <div style="text-align: right; color:#333333; margin: 5px 0"><a href="/login2.html">密码登录</a></div>
      </div>   
    </div>
  </div>
</div>

1.2 JS部分

发送验证码
sendCode(){
        if (!this.form.phone) {
          this.$message.error("手机号不能为空");
          return;
        }
        // 发送验证码
        axios.post("/user/code?phone="+this.form.phone)
          .then(() => {})
          .catch(err => {
            console.log(err);
            this.$message.error(err)
          });
        // 禁用按钮
        this.disabled = true;
        // 按钮倒计时
        let i = 60;
        this.codeBtnMsg = (i--) + '秒后可重发'
        let taskId = setInterval(() => this.codeBtnMsg = (i--) + '秒后可重发', 1000);
        setTimeout(() => {
          this.disabled = false;
          clearInterval(taskId);
          this.codeBtnMsg = "发送验证码";
        }, 59000)
      }
提交登录
login(){
        if(!this.radio){
          this.$message.error("请先确认阅读用户协议!");
          return
        }
        if(!this.form.phone || !this.form.code){
          this.$message.error("手机号和验证码不能为空!");
          return
        }
        axios.post("/user/login", this.form)
        .then(({data}) => {
			debugger
			console.log("data=======" , data)
            if(data){
              // 保存用户信息到session
              sessionStorage.setItem("token", data);
            }
            // 跳转到首页
            location.href = "/index.html"
        })
        .catch(err => {
			debugger
			console.log("err=======" , err)
			this.$message.error(err)
		})
      },

1.3 axios拦截器

let commonURL = "/api";
// 设置后台服务地址
axios.defaults.baseURL = commonURL;
axios.defaults.timeout = 2000;
// request拦截器,将用户token放入头中
let token = sessionStorage.getItem("token");
axios.interceptors.request.use(
  config => {
    if(token) config.headers['authorization'] = token
    return config
  },
  error => {   
    return Promise.reject(error)
  }
)
axios.interceptors.response.use(function (response) {
  // 判断执行结果
  if (!response.data.success) {
    return Promise.reject(response.data.errorMsg)
  }
  return response.data;
}, function (error) {
  // 一般是服务端异常或者网络异常  
  if(error.response.status == 401){
    // 未登录,跳转
    setTimeout(() => {
      location.href = "/login.html"
    }, 200);
    return Promise.reject("请先登录");
  }
  return Promise.reject("服务器异常");
});
axios.defaults.paramsSerializer = function(params) {
  let p = "";
  Object.keys(params).forEach(k => {
    if(params[k]){
      p = p + "&" + k + "=" + params[k]
    }
  })
  return p;
}

2、SpringBoot后端

2.1 引入依赖

<dependency>
     <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
  </dependency>

2.2 属性配置

spring: 
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 3
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s

2.3 发送验证码实现

 /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
    }
@Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1、验证是否合法
        if(RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机不合法,格式错误!");
        }
        //3、符合,生成验证码
        String randomNumbers = RandomUtil.randomNumbers(6);
        //4、保证验证码到session
        session.setAttribute(phone, randomNumbers);
        redisTemplate.opsForValue().set(RedisKey.SMS_PRE + phone, randomNumbers, 2, TimeUnit.MINUTES);
        //5、发送验证码
        log.debug("发送验证码成功!验证码是:{}" , randomNumbers);

        //6、返回ok
        return Result.ok("发送成功!");
    }

2.4 登录实现

 /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        return userService.login(loginForm, session);
    }
  • 1、验证是否合法
  • 2、如果不符合,返回错误信息
  • 3、验证验证码是否一致
  • 4、根据手机号查询用户
  • 5、不存在创建用户
  • 6、生成token
  • 7 、保存到Redis
  • 8、设置token有效期
@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1、验证是否合法
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机不合法,格式错误!");
        }
//        String smsCode = (String) session.getAttribute("smsCode");
        String smsCode = redisTemplate.opsForValue().get(RedisKey.SMS_PRE + loginForm.getPhone());

        //3、验证验证码是否一致
        if(MyStrUtil.isEmpty(smsCode) || !smsCode.equals(loginForm.getCode())){
            //4、如果不符合,返回错误信息
            return Result.fail("验证码错误!");
        }

        //4、根据手机号查询用户
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>();
        wrapper.eq(User::getPhone, phone);
        User user = this.getOne(wrapper);

        //5、创建用户
        if(user == null){
            user = new User();
            user.setPhone(phone);
            user.setPassword("123456");
            user.setCreateTime(LocalDateTime.now());
            user.setUpdateTime(LocalDateTime.now());
            user.setNickName(RedisKey.NICK_PRE + RandomUtil.randomString(5));
            this.save(user);
        }
        //6、保存到session
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

        //7 生成token
        String token = UUID.randomUUID().toString(true);
//        redisTemplate.opsForValue().set(RedisKey.LOGIN_USER, JSON.toJSONString(userDTO));
//        session.setAttribute(RedisKey.LOGIN_USER, userDTO);


        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())
                );
        redisTemplate.opsForHash().putAll(RedisKey.TOKEN_PRE + token, userMap);
        //token 1小时过期
        redisTemplate.expire(RedisKey.TOKEN_PRE + token, 1, TimeUnit.HOURS);

        return Result.ok(token);
    }

2.5 获取登录用户信息

@GetMapping("/me")
    public Result me(){
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

2.6 登录用户信息拦截器

  • 1 获取请求头中的token
  • 2 根据token获取session中的用户
  • 3 判断用户是否存在
  • 4 如果存在,存入ThreadLocal
  • 5 刷新token的有效期
public class LoginInterceptor implements HandlerInterceptor {

//    @Autowired
    private StringRedisTemplate redisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1 获取请求头中的token
        String token = request.getHeader("authorization");
        if(MyStrUtil.isEmpty(token)){
            response.setStatus(401);
            return false;
        }
        String key = RedisKey.TOKEN_PRE + token;
        //2 根据token获取session中的用户
        Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key);
        //3 判断用户是否存在
        if(userMap == null || userMap.isEmpty()){
            //5 不存在,拦截
            response.setStatus(401);
            return false;
        }

        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //4 如果存在,存入ThreadLocal
        UserHolder.saveUser(user);

        //7 刷新token的有效期
        redisTemplate.expire(key, 1, TimeUnit.HOURS);
        //8 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

2.7 注册拦截器

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(redisTemplate))
                .addPathPatterns("/**")
                //放行的路径
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

四、总结

Redis代替session的业务流程主要有以下优点:

  1. 高度可配置性:Redis可以通过配置参数来灵活地控制数据的存储方式、过期时间、过期后的自动清除等,这使得Redis可以很容易地替换掉传统的session实现。
  2. 数据持久化:Redis支持将数据持久化到磁盘,这意味着即使在服务器关闭的情况下,数据也不会丢失。这对于需要保证数据安全性的应用场景非常重要。
  3. 支持高并发:Redis可以支持高并发请求,因为它的内存数据结构比较紧凑,并且支持多个读写操作同时进行,这使得Redis适合于高并发的应用场景。
  4. 支持分布式:Redis可以实现分布式部署,这使得多个Redis实例可以部署在不同的机器上,从而实现分布式应用。
  5. 方便的操作语法:Redis提供了简单直观的命令操作语法,这使得Redis的使用比较简单,并且不需要学习复杂的Session技术。

五、源码下载

gitee.com/charlinchenlin/koo-erp

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

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

相关文章

Python共享文件 - Python快速搭建HTTP web服务实现文件共享并公网远程访问

文章目录 1. 前言2. 视频教程3. 本地文件服务器搭建3.1 python的安装和设置3.2 cpolar的安装和注册 4. 本地文件服务器的发布4.1 Cpolar云端设置4.2 Cpolar本地设置 5. 公网访问测试6. 结语 转载自内网穿透工具的文章&#xff1a;Python一行代码实现文件共享【内网穿透公网访问…

全域兴趣电商:国货品牌的新策略、新玩法

【潮汐商业评论/原创】 消费的方向标已经变了。 在消费市场的滚滚浪潮里&#xff0c;国人的“衣食住行”在全面的“国货化”&#xff0c;一个个有颜值有实力的国货品牌如雨后春笋般出现在寻常百姓家&#xff0c;如今在这片肥沃的土壤上正结出适合国人使用的果实。 01 国货二…

Openai+Coursera: ChatGPT Prompt Engineering(二)

这是我写的ChatGPT Prompt Engineerin的第二篇博客&#xff0c;如何还没看过第一篇的请先看我写的第一篇博客&#xff1a; ChatGPT Prompt Engineerin(一) Summarizing(总结/摘要&#xff09; 今天我们的重点关注按特定主题来总结文本。 设置参数 import openai openai.api_…

【备战秋招】每日一题:3月18日美团春招第二题:题面+题目思路 + C++/python/js/Go/java 带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…

深入理解递归算法

文章目录 概述单路递归 Single RecursionE01. 阶乘E02. 反向打印字符串E03. 二分查找 多路递归 Multi RecursionE01. 斐波那契数列 递归优化-记忆法递归优化-尾递归递归时间复杂度-Master theorem递归时间复杂度-展开求解 概述 定义 计算机科学中&#xff0c;递归是一种解决计…

Unity UI -- (5)增加基础按钮功能

分析分析一些常见UI 良好的UI设计会清晰地和用户沟通。用户知道他们能和屏幕上哪些东西交互&#xff0c;哪些不能。如果他们进行了交互&#xff0c;他们也要清楚地知道交互是否成功。换句话说&#xff0c;UI要提供给用户很多反馈。 我们可以来看看在Unity里或者在计算机上的任何…

一款适合国内多场景的免费ChatGPT镜像网站【建议收藏】

随着人工智能技术的不断进步&#xff0c;智能问答系统正逐渐成为我们生活中必不可少的助手。而在这个领域中&#xff0c;ChatGPT中文版-知否AI问答凭借其出色的性能和广泛的应用场景&#xff0c;成为了引领智能问答新时代的重要代表。本文将带您深入了解ChatGPT中文版-知否AI问…

LabVIEWCompactRIO 开发指南25 实施LabVIEW FPGA代码的方法

LabVIEWCompactRIO 开发指南25 实施LabVIEW FPGA代码的方法 开始开发时&#xff0c;应在LabVIEW项目的FPGA目标下创建VI&#xff0c;以便使用LabVIEW FPGA选板进行编程&#xff0c;该选板是LabVIEW选板的子集&#xff0c;包括一些LabVIEW FPGA特定函数。 应该在仿真模式下开…

每日一个MySQL知识点:主从表大小相差巨大和一个BUG

一、主从相同表空间相差巨大 1.1 问题描述 我们知道MySQL主从基本上是逻辑的复制&#xff0c;那么有少量的空间差异没有问题&#xff0c;但是本案例主库表只有10G&#xff0c;但是从库表有100G&#xff0c;这么大的差距比较少见&#xff0c;需要分析原因。 1.2 问题分析 实…

ResNet (深度残差网络)

ResNet 算法概述 解决的核心问题&#xff1a;网络的退化现象 网络层数在变深之后&#xff0c;性能不如浅层时候的性能 。注意&#xff1a;网络退化既不是梯度消失也不是梯度爆炸。 那是如何解决退化现象的呢&#xff1f;引入残差模块 把模型的输入分成两条路&#xff1a;右边…

SQL 大全(四)|数据库迁移升级时常用 SQL 语句

作者 | JiekeXu 来源 |公众号 JiekeXu DBA之路&#xff08;ID: JiekeXu_IT&#xff09; 如需转载请联系授权 | (个人微信 ID&#xff1a;JiekeXu_DBA) 大家好&#xff0c;我是 JiekeXu,很高兴又和大家见面了,今天和大家一起来看看SQL 大全&#xff08;四&#xff09;|数据库迁移…

由浅入深了解 深度神经网络优化算法

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 导言 优化是从一组可用的备选方案中选择最佳方案。优化无疑是深度学习的核心。基于梯度下降的方法已经成为训练深度神经网络的既定方法。 在最简单的情况下&#xff0c;优化问题包括通过系统地从允许集合中…

Jenkins+GitLab+Docker搭建前端自动化构建镜像容器部署

前言 &#x1f680; 需提前安装环境及知识点&#xff1a; 1、Docker搭建及基础操作 2、DockerFile文件描述 3、Jenkins搭建及基础点 &#x1f680; 目的&#xff1a; 将我们的前端项目打包成一个镜像容器并自动发布部署&#xff0c;可供随时pull访问 一、手动部署镜像及容器 1…

6-《网络面试》

6-《网络面试》 1.http是什么&#xff1f;http的工作机制&#xff1f;http报文&#xff1f;1.1 http工作机制&#xff1a;1.2 URL和http报文 2. HTTP请求方法和状态码3.Get和Post的区别4.HTTP的Header解析1.text/html2.x-www-form-urlencoded3.multipart/form-data4.applicatio…

中断相关概念并利用中断实现按键点亮LED灯

一.中断相关概念 什么是中断&#xff1f; 中断是指计算机运行过程中&#xff0c;出现某些意外情况需主机干预时&#xff0c;机器能自动停止正在运行的 程序并转入处理新情况的程序&#xff0c;处理完毕后又返回原被暂停的程序继续运行。 什么是EXTI&#xff1f; 外部…

【vue上传文件——hash】

vue上传文件 要求:只能上传视频,先计算文件的hash值,hash值一样则不需要上传,不一样在执行上传 分析:因为el-upload没有找到合适的属性,本次用的是原生的input的type属性为file上传 代码: html: 通过点击选取文件按钮调用input上传 js 第一步:点击上传文件先效验是否…

windows下免U盘安装manjaro

建议 建议先看这个https://www.bilibili.com/read/cv23161386/ 背景 新到了一块硬盘&#xff0c;想把这台主机做成双系统的&#xff0c;windowsmanjaro系统。 为什么选择manjaro&#xff1f; win中的虚拟机已经有了日常使用的ubuntu。体验一下manjaro。ubuntu用来开发办公要…

python 之 logging的使用

一、日志模块 import logginglogging.debug("调试日志") logging.info(消息日志) logging.warning("告警日志") logging.error(错误日志) logging.critical(严重错误日志)debug&#xff08;调试&#xff09;级别用于输出调试信息&#xff0c;这些信息主…

JDK源码阅读环境搭建

本次针对jdk8u版本的搭建 1.新建项目 新建java项目JavaSourceLearn &#xff0c;这里我创建的是maven 2.获取JDK源码 打开Project Structure 找到本地JDK安装位置将src.zip解压到项目java包中 整理下项目结构&#xff0c;删除用不到的目录 提示: 添加源码到项目之后首次运行…

【BBQ: A Hand-Built Bias Benchmark for Question Answering 论文精读】

BBQ: A Hand-Built Bias Benchmark for Question Answering 论文精读 InformationAbstract1 Introduction2 Related Work3 The Dataset3.1 Coverage3.2 Template Construction3.3 Vocabulary4 Validation5 Evaluation6 Results7 Discussion8 Conclusion9 Ethical Consideration…