黑马点评-01基于Redis实现短信登陆的功能

news2024/11/20 8:35:14

环境准备

当前模型

nginx服务器的作用

  • 手机或者app端向nginx服务器发起请求,nginx基于七层模型走的是HTTP协议,可以实现基于Lua直接绕开tomcat访问Redis

  • nginx也可以作为静态资源服务器,轻松扛下上万并发并负载均衡到下游的tomcat服务器,利用集群支撑起整个项目

  • 使用nginx部署前端项目后还可以做到动静分离,进一步降低tomcat服务的压力

企业级MySQL加上固态硬盘能够支撑的并发大概就是4000起~7000左右,对于上万的并发如果让tomcat直接访问Mysql,瞬间会让Mysql服务器的cpu和硬盘全部打满

  • 在高并发场景下需要选择使用MySQL集群,同时为了进一步降低MySQL的压力增加访问的性能,一般还需要使用Redis集群

在这里插入图片描述

导入数据/前端/后端

执行项目所需要的SQL脚本,MySQL的版本要采用5.7及以上版本,否则执行脚本时部分SQL语句无法执行

说明
tb_user用户表
tb_user_info用户详情表
tb_shop商户信息表
tb_shop_type商户类型表
tb_blog用户日记表(达人探店日记)
tb_follow用户关注表
tb_voucher优惠券表
tb_voucher_order优惠券的订单表

导入后端项目,将项目放到你的idea工作空间,然后利用idea打开即可

  • 第一步: 修改application.yaml文件中的MySQL和Reids的连接地址为自己服务所在的地址
  • 第二步: 启动项目,在浏览器访问http://localhost:8081/shop-type/list,如果可以看到JSON数据则说明导入成功

导入前端工程,将nginx的解压目录放到一个自己指定的目录(确保目录不含中文,特殊字符和空格)

  • 第一步: 在ngnix所在目录打开一个CMD窗口,执行start nginx.exe命令启动ngnix服务
  • 第二步: 打开浏览器并将页面调整为手机模式,访问http://localhost:8080/访问项目首页(页面的数据是从后端查询得到的)
server {
        listen       9999;
        server_name  localhost;
        # 指定前端项目所在的位置
        location / {
            root   html/hmdp;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

		# 监听的路径
        location /api {  
            default_type  application/json;
            #internal;  
            keepalive_timeout   30s;  
            keepalive_requests  1000;  
            #支持keep-alive  
            proxy_http_version 1.1;  
            rewrite /api(/.*) $1 break;  
            proxy_pass_request_headers on;
            #more_clear_input_headers Accept-Encoding;  
            proxy_next_upstream error timeout;  
	     	# 反向代理的位置
          	proxy_pass http://127.0.0.1:8081;
            #proxy_pass http://backend;
        }
    }

短信登录

基于Session实现登录流程

在这里插入图片描述

实现发送短信验证码功能

用户点击发生验证码按钮发起请求

在这里插入图片描述

UserController中的sendCode方法处理发生验证码请求,在UserServiceImpl编写具体的业务逻辑

  • 使用邮箱验证时我们还需要去数据库中修改phone的字段类型,将varchar(11)改为varchar(100)
/**
*发送手机验证码
*/	
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    return userService.sendcode(phone,session);
}
/**
* 发送手机验证码
*/
@Override
public Result sendcode(String phone, HttpSession session) {
    // 1.校验手机号,RegexUtils是我们创建的工具类,里面还需要用到RegexPatterns工具类
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.符合生成验证码,RandomUtil是hutool-all的工具类 
    String code = RandomUtil.randomNumbers(6);
    // 4.保存验证码到session
    session.setAttribute("code",code);
    // 5.发送验证码
    log.debug("发送短信验证码成功,验证码:{}", code);
    // 6.返回成功的信息
    return Result.ok();
}

/**
* 发送邮箱验证码
*/
@Override
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) throws MessagingException {
    // TODO 发送短信验证码并保存验证码
    if (RegexUtils.isEmailInvalid(phone)) {
        return Result.fail("邮箱格式不正确");
    }
    String code = MailUtils.achieveCode();
    session.setAttribute(phone, code);
    log.info("发送登录验证码:{}", code);
    MailUtils.sendTestMail(phone, code);
    return Result.ok();
}

实现登录功能

用户点击登录按钮发起请求
在这里插入图片描述

/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    return userService.login(loginForm, session);
}
// loginForm中封装了登录参数,包含手机号、验证码或者手机号、密码
@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号的格式
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.将从session中获取的验证码与用户提交的验证码进行比较
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    // 从session中默认获取的对象都是Object类型,所以我们需要转化为String类型
    if(cacheCode == null || !cacheCode.toString().equals(code)){
        // 4.验证码不一致则报错
        return Result.fail("验证码错误");
    }
    // 5.验证码一致则根据用户提交的手机号取数据库中查询用户
    User user = query().eq("phone", phone).one();

    // 判断用户是否存在
    if(user == null){
        //6.不存在则创建
        user =  createUserWithPhone(phone);
    }
    //7.将用户的信息保存到session中
    session.setAttribute("user",user);
	// 返回成功的信息
    return Result.ok();
}

// 创建一个新用户
private User createUserWithPhone(String phone) {
    //创建用户
    User user = new User();
    //设置手机号
    user.setPhone(phone);
    //设置昵称(默认名),一个固定前缀+随机字符串,USER_NICK_NAME_PREFIX是工具类中的系统常量
    user.setNickName(USER_NICK_NAME_PREFIX+ RandomUtil.randomString(8));
    //将用户信息保存到数据库
    save(user);
    return user;
}

实现登录验证功能(拦截器)

当我们登录成功后,前端还会发起一个user/me的请求用于登录校验,由于访问每个Controller都需要登录校验,所以我们可以把校验流程写在拦截器中

在这里插入图片描述

第一步; 定义一个工具类UserHolder专门用来存储用户的信息,而且每个线程都对应一个自己的用户信息

public class UserHolder {
    private static final ThreadLocal<User> tl = new ThreadLocal<>();
    public static void saveUser(User user){
        // 保存用户的信息,默认的key就是当前线程
        tl.set(user);
    }
    public static User getUser(){
        // 获取用户信息,默认的key就是当前线程
        return tl.get();
    }
    public static void removeUser(){
        // 删除用户信息,默认的key就是当前线程
        tl.remove();
    }
}

第二步: 创建一个LoginInterceptor类实现HandlerInterceptor接口,防止用户直接通过url路径访问项目的功能,重写前置拦截器方法和完成处理方法

  • preHandle方法: 用于我们登陆之前的权限校验,同时将从session中获取的用户信息保存到UserHolder的ThreadLocal中,方便以后在Controller中获取用户信息
  • afterCompletion方法: 用于处理登录后的信息避免内存泄露
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.用户存在则将用户信息保存到UserHolder的Threadlocal中
        UserHolder.saveUser((User)user);
        //6.放行
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws 			Exception {
        UserHolder.removeUser();
    }
}

第三步:编写配置类,注册登录拦截器并设置该拦截器需要拦截的请求

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册登录拦截器使其生效
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(// 设置拦截器不需要拦截的请求
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

第四步: 获取当前线程对应的登录用户信息并响应到前端完成登录校验

@GetMapping("/me")
public Result me() {
    User user = UserHolder.getUser();
    return Result.ok(user);
}

隐藏用户敏感信息

在进行登录校验时将登录用户的全部信息响应给浏览器的行为是不安全的,所以我们应当在返回登录用户信息之前将用户的敏感信息进行隐藏

{
    "success":true,
    "data":{
        "id":1010,
        "phone":"1586385296",
        "password":"",
        "nickName":"user_i1b3ir09",
        "icon":"",
        "createTime":"2022-10-22T14:20:33",
        "updateTime":"2022-10-22T14:20:33"
    }
}

第一步: 新建一个不含用户敏感信息UserDto对象,在进行登录校验时返回的含有用户敏感信息的User对象转化成UserDto对象

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

第二步: 修改UserHolder工具类中ThreadLocal的泛型

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();
    }
}

第三步: 修改login方法中,将保存到session域中的User对象转换成UserDto对象

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号的格式
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.将从session中获取的验证码与用户提交的验证码进行比较
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    // 从session中默认获取的对象都是Object类型,所以我们需要转化为String类型
    if(cacheCode == null || !cacheCode.toString().equals(code)){
        // 4.验证码不一致则报错
        return Result.fail("验证码错误");
    }
    // 5.验证码一致则根据用户提交的手机号取数据库中查询用户
    User user = query().eq("phone", phone).one();

    // 判断用户是否存在
    if(user == null){
        //6.不存在则创建
        user =  createUserWithPhone(phone);
    }
    //7.将用户的信息隐藏后再存到到session中
    session.setAttribute("user",user);
    //UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);      
	//session.setAttribute("user", userDTO);
	session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));
    // 返回成功的信息
    return Result.ok();
    
}

第四步: 修改拦截器中将从session中获取的隐藏了用户信息的UserDTO对象保存到UserHolder类的ThreadLocal中

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中隐藏了用户信息的UserDTO类型的用户对象,并保存到UserHolder类的ThreadLocal中
        UserDTO user = (UserDTO) session.getAttribute("user");
        UserHolder.saveUser(user);        
        //3.判断用户是否存在
        if(user == null){
              //4.不存在则拦截当前请求并返回401状态码
              response.setStatus(401);
              return false;
        }
        //5.若存在保存用户的隐藏信息到Threadlocal
        UserHolder.saveUser((User)user);
        //6.放行
        return true;
    }
}

第五步: 修改登录校验的方法,将UserHolder中的ThreadLocal保存的UserDTO对象返回

// 修改获取的类型为用户的隐藏信息
@GetMapping("/me")
public Result me() {
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

第六步: 重启服务器,登录校验后返回的用户信息已经不含敏感信息

{
    "success":true,
    "data":{
        "id":1016,
        "nickName":"user_zkhf7cfv",
        "icon":""
    }
}

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

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

相关文章

volatile使用方法

volatile使用方法 编译优化。使用等级3的话&#xff0c;可能将优化了一些变量。 这为什么会开启等第三呢&#xff1f;这是关于单片机的内存容量比较小&#xff0c;所以开启优化的话&#xff0c;可以可以省一些空间&#xff0c;但是如果。会出现些变量的问题&#xff0c;需要通过…

基于A4988/DRV8825的四路步进电机驱动器

概述 简化板的CNC sheild V3.0&#xff0c;仅保留步进电机速度与方向的控制引脚STEP/DIR、使能端EN、芯片供电VCC\GND&#xff0c;共计11个引脚。PCB四周开设四个M3通孔&#xff0c;以便于安装固定。此外&#xff0c;将板载的焊死的保险丝更改为可更换的保险座保险丝&#xff…

Linux系统之安装cook菜谱工具

Linux系统之安装cook菜谱工具 一、cook菜谱工具介绍二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本3.3 检查系统是否安装pnpm 四、部署Node.js环境4.1 下载Node.js安装包4.2 解压Node.js安装包4.3 复制二进制…

蓝桥杯每日一题2023.10.6

题目描述 门牌制作 - 蓝桥云课 (lanqiao.cn) 题目分析 #include<bits/stdc.h> using namespace std; int ans; int main() {for(int i 1; i < 2020; i ){int x i;while(x){int a x % 10;if(a 2)ans ;x / 10;}}cout << ans;return 0; } 题目描述 既约分数…

Python3操作Redis最新版|CRUD基本操作(保姆级)

Python3中类的高级语法及实战 Python3(基础|高级)语法实战(|多线程|多进程|线程池|进程池技术)|多线程安全问题解决方案 Python3数据科学包系列(一):数据分析实战 Python3数据科学包系列(二):数据分析实战 Python3数据科学包系列(三):数据分析实战 Win11查看安装的Python路…

数据结构P46(2-1~2-4)

2-1编写算法查找顺序表中值最小的结点&#xff0c;并删除该结点 #include <stdio.h> #include <stdlib.h> typedef int DataType; struct List {int Max;//最大元素 int n;//实际元素个数 DataType *elem;//首地址 }; typedef struct List*SeqList;//顺序表类型定…

一年一度的国庆节又结束了

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

Iphone文件传到电脑用什么软件,看这里

在数字化时代&#xff0c;文件传输已经成为我们日常生活中不可或缺的一部分。然而&#xff0c;苹果用户在将手机文件传输到电脑时&#xff0c;往往会面临一些困扰。曾经的“文件传输助手”并不能完全满足用户的需求。于是&#xff0c;很多人开始寻找更便捷的解决方案。在本文中…

MySQL概念

原理定义概念 定义 数据库(Database)是按照数据结构来组织、存储和管理数据的建立在计算机存储设备上的仓库 数据库是长期储存在计算机内、有组织的、可共享的数据集合 分类&#xff1a; &#xff08;1&#xff09;非结构化数据&#xff1a; 数据相对来讲没有固定的特点&#…

充分理清限制与条件+构造二分图+最小割:ARC142E

https://www.luogu.com.cn/problem/AT_arc142_e 他的充要条件是是什么&#xff1a; a i , a j ≥ m i n ( b i , b j ) a_i,a_j\ge min(b_i,b_j) ai​,aj​≥min(bi​,bj​)存在 a i ≥ m a x ( b i , b j ) a_i\ge max(b_i,b_j) ai​≥max(bi​,bj​) 第一个条件直接预处理一…

【目标检测】——PE-YOLO精读

yolo&#xff0c;暗光目标检测 论文&#xff1a;PE-YOLO 1. 简介 卷积神经网络&#xff08;CNNs&#xff09;在近年来如何推动了物体检测的发展。许多检测器已经被提出&#xff0c;而且在许多基准数据集上的性能正在不断提高。然而&#xff0c;大多数现有的检测器都是在正常条…

初识jmeter及简单使用

目录 1、打开页面&#xff1a; 2、添加线程组&#xff1a; 3、线程组中设置参数&#xff1a; 4、添加请求 5、添加一个http请求后&#xff0c;设置请求内容 6、添加察看结果树 7、执行&#xff0c;查看结果 一般步骤是&#xff1a;在测试计划下面新建一个线程组&#xf…

【Java项目推荐之黑马头条】项目中的内容安全和自动审核如何实现的?

前言 在学习Java的路上还是遇到了很多不错的好项目的&#xff0c;今天分享给大家&#xff0c;希望能对大家面试有所帮助&#xff01; 后续会继续推荐其他好的项目&#xff0c;这次推荐的是B站开源的视频黑马头条项目&#xff0c;来吧学会它一起去虐面试官&#xff01;&#x…

mac清理垃圾的软件有哪些?这三款我最推荐

没错&#xff0c;Mac电脑真的好用&#xff0c;但是清理系统垃圾可不是件容易的事。由于Mac系统的封闭性&#xff0c;系统的缓存垃圾常常隐藏得让人发现不了。不过&#xff0c;别担心&#xff01;有一些专业的Mac清理软件可以帮你解决这一系列问题&#xff0c;让清理垃圾变得轻松…

【面试】C/C++面试八股

C/C面试八股 编译过程的四个阶段C和C语言的区别简单介绍一下三大特性多态的实现原理虚函数的构成原理虚函数的调用原理虚表指针在什么地方进行初始化的&#xff1f;构造函数为什么不能是虚函数为什么建议将析构函数设为虚函数虚函数和纯虚函数的区别抽象类类对象的对象模型内存…

python开发幸运水果抽奖大转盘

概述 当我女朋友跟我说要吃水果&#xff0c;又不知道吃啥水果时候&#xff0c;她以为难为到我了&#xff0c;有啥事难为到程序员的呢&#xff01; 今天用python利用第三方tkinterthreadingtime库开发一个幸运水果抽奖大转盘&#xff01;抽到啥吃啥 详细 老规矩&#xff01;咱…

PyTorch入门之【AlexNet】

参考文献&#xff1a;https://www.bilibili.com/video/BV1DP411C7Bw/?spm_id_from333.999.0.0&vd_source98d31d5c9db8c0021988f2c2c25a9620 AlexNet 是一个经典的卷积神经网络模型&#xff0c;用于图像分类任务。 目录 大纲dataloadermodeltraintest 大纲 各个文件的作用&…

【将文本编码为图像灰度级别】以 ASCII 编码并与灰度级别位混合将文本字符串隐藏到图像像素的最低位中,使其不明显研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

HTTP进阶,Cookie,响应的回报结果含义,ajax,form表单,不同状态码代表的结果

目录 一、Cookie 二、响应的回报结果含义 三、实际开发中的选择 一、Cookie Cookie是浏览器本地存储数据的一种机制, 在浏览器访问服务器之间&#xff0c;此时你的浏览器对着个服务器之间是一点也不了解的&#xff0c;你的浏览器上是没有任何和着个服务器相关的数据的。 浏览…

【LinuxC】时间、时区,相关命令、函数

文章目录 一、序1.1 时间和时区1.11 时间1.12 时区 1.2 查看时间时区的命令1.21 Windows1.22 Linux 二、C语言函数2.1 通用2.11 函数简介2.12 数据类型简介 2.2 windows 和 Linux特有函数2.3 C语言示例 一、序 1.1 时间和时区 1.11 时间 时间是一种用来描述物体运动变化的量…