当前位置: 首页 > news >正文

【黑马点评 - 实战篇01】Redis项目实战(Windows安装Redis6.2.6 + 发送验证码 + 短信验证码登录注册 + 拦截器链 - 登录校验)

目录

一、安装Redis

1、卸载旧版本Redis

2、Windows安装6.2.6版本Redis

二、导入黑马点评项目

1、导入数据库

2、导入后端代码

3、配置maven

4、修改application.yml和config配置

5、启动后端

6、启动前端

三、基于Session实现短信登录

1、业务流程概览

(1)session机制介绍

2、发送短信验证码 - POST接口

(1)需求分析

(2)代码开发

3、短信验证码登陆、注册 - POST接口

(1)需求分析

(2)代码开发

4、登录校验 - 拦截器 LoginInterceptor

(1)需求分析

(2)代码开发

四、基于Redis实现短信校验

1、session共享问题

2、Redis实现短信登录业务流程

(1)发送短信验证码流程

(2)短信验证码登录、注册流程

(3)登录校验流程

3、代码优化

(1)发送短信验证码 - 优化

(2)短信验证码登录、注册 - 优化

(3)登录校验 - 优化

4、解决状态登录刷新问题 - 拦截器链

(1)刷新Token拦截器

(2)登录校验拦截器

(3)MvcConfig配置类

五、复习回顾 - Redis实现短信登录

1、发送验证码 - redis流程

2、短信验证码登录、注册 - redis流程

3、登录校验 - 拦截器链流程


一、安装Redis

1、卸载旧版本Redis

如果你的redis版本太低,先进入Redis安装目录,输入cmd启动控制台,然后输入下面命令进行卸载

然后再把redis文件夹删除就OK了

2、Windows安装6.2.6版本Redis

参考教程:https://yonghongtech.csdn.net/681d632ea5baf817cf49a9ed.html?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7Ebaidujs_utm_term%7Eactivity-1-142692413-blog-140069813.235%5Ev43%5Epc_blog_bottom_relevance_base2&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7Ebaidujs_utm_term%7Eactivity-1-142692413-blog-140069813.235%5Ev43%5Epc_blog_bottom_relevance_base2&utm_relevant_index=1

https://link.csdn.net/?target=https%3A%2F%2Fgithub.com%2Fbinghe021%2Fredis-setup%2Freleases%3Flogin%3Dfrom_csdn

上面是下载链接,也可以通过置顶的资源进行下载

解压后双击运行,一直下一步即可

如果要修改密码,参考上面的教程链接,新手不建议设置密码,我这里就先不设置了

在redis安装路径文件夹输入cmd启动控制台

接着输入下面的指令(注意:先粘贴前面一段,按空格,然后再粘贴后面一段,最后按回车

redis-server.exe redis.windows.conf

如果出现以上界面说明安装成功,接下来测试一下

重新启动一个控制框

如果出现上面内容说明成功!

二、导入黑马点评项目

1、导入数据库

2、导入后端代码

3、配置maven

下载依赖如果报错:Failure to transfer org.springframework.boot:spring-boot-starter-data-redis:jar:2.3.12.RELEASE from http://maven.aliyun.com/nexus/content/groups/public/ was cached in the local repository, resolution will not be reattempted until the update interval of alimaven has elapsed or updates are forced.这个错误表明阿里云仓库连接超时

解决方法:在C盘User -> .m2 -> settings.xml中加入下面的代码(更换镜像),然后回到IDE重新下载maven就可以了

<?xml version="1.0" encoding="UTF-8"?>
<settings><mirrors><mirror><id>huaweicloud</id><mirrorOf>*</mirrorOf><name>华为云</name><url>https://repo.huaweicloud.com/repository/maven/</url></mirror></mirrors>
</settings>

如果你的User-.m2文件夹下没有setting.xml,那就创建一个,在.m2文件夹下新建文本文件,然后重命名为【settings.xml】,用记事本方式打开,再粘贴上面的代码,最后回到IDE重新下载maven就好了

4、修改application.yml和config配置

然后修改RedissonConfig

5、启动后端

http://localhost:8081/shop-type/list

如果能获取到网页数据,说明后端配置成功

6、启动前端

把资料包中的nginx解压到一个全英文路径的文件夹下

在nginx所在目录下打开cmd,输入下列命令

start nginx.exe

最后在刚刚的页面中,按F12调出检查框,然后点击右上角的小手机图标,切换成手机模式

把网址端口改为8080,如果能成功显示,说明前端配置成功

三、基于Session实现短信登录

1、业务流程概览

(1)session机制介绍

  • Session是服务器端的用户状态管理机制。
  • 每个用户首次访问时,服务器会创建唯一的Session ID并通过Cookie返回给浏览器。
  • 后续请求浏览器自动携带此ID,服务器据此找到对应的Session对象。
  • Session数据存储在服务器内存中,包含用户登录状态、验证码等个人信息,各用户的Session完全隔离,有效保障数据安全和会话独立性。

2、发送短信验证码 - POST接口

(1)需求分析

前端页面点击【我的】,则会弹出登陆界面,如果点击【发送验证码】按钮,则前端会向后端发送一个请求,后端负责接收该请求并做相应处理

(2)代码开发

【1】controller层

    /*** 发送手机验证码*/@PostMapping("code")public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {// 发送短信验证码并保存验证码return userService.sendCode(phone, session);}

【2】service层

  • 验证手机号格式是否正确
  • 若错误,返回错误信息
  • 若正确,生成验证码
  • 将验证码存入session,用于后续和用户输入的验证码比对
  • 发送验证码(模拟发送,不作为重点)
  • 返回成功信息
    /*** 发送验证码* @param phone* @param session* @return*/@Overridepublic Result sendCode(String phone, HttpSession session) {//1.验证手机号if (RegexUtils.isPhoneInvalid(phone)) {//2.如果手机号无效,返回错误信息return Result.fail("手机号格式错误!");}//3.如果有效,生成验证码String code = RandomUtil.randomNumbers(6);//4.将验证码存入sessionsession.setAttribute("code",code);//5.发送验证码(模拟)log.debug("您的验证码是:{}",code);//6.返回成功return Result.ok();}

3、短信验证码登陆、注册 - POST接口

(1)需求分析

(2)代码开发

【1】controller层

    /*** 登录功能* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码*/@PostMapping("/login")public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){// 实现登录功能return userService.login(loginForm, session);}

【2】service层

  • 校验手机号
  • 经验验证码
  • 若不一致,返回错误信息
  • 若一致,通过手机号查询数据库中是否存在该用户
  • 如果不存在,创建一个新用户保存到数据库中
  • 将用户保存到session中
  • 返回成功信息
    /*** 短信实现登陆、注册* @param loginForm* @param session* @return*/@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机格式错误!");}//2.校验验证码String code = loginForm.getCode();Object cacheCode = session.getAttribute("code");if(cacheCode == null || !cacheCode.equals(code)){//3.不一致,返回错误信息return Result.fail("验证码错误!");}//4.一致,查询数据库是否存在用户User user = query().eq("phone", phone).one(); //mybatis-plus实现用手机号查询用户信息//5.用户不存在,创建新用户if(user == null){user = createUserWithPhone(phone);}//6.将用户保存进session//每一个用户的Session都是分开的,不是公共的//每个用户首次访问时,服务器生成唯一的Session IDsession.setAttribute("user",user);return Result.ok();}private User createUserWithPhone(String phone) {// 1.创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));// 2.保存用户save(user);return user;}

4、登录校验 - 拦截器 LoginInterceptor

(1)需求分析

  • 如果没有拦截器,每个控制器方法都需重复编写登录验证、权限检查、日志记录等通用逻辑
  • 例如:用户权限校验代码会在订单查询、个人中心等数十个接口中重复出现,导致代码冗余、维护困难。
  • 拦截器通过统一预处理将这些横切关注点集中管理,极大提升代码复用性和系统可维护性。

(2)代码开发

【1】controller层

    @GetMapping("/me")public Result me(){// 获取当前登录的用户并返回UserDTO user = UserHolder.getUser();return Result.ok(user);}

【2】拦截器

  • 获取session
  • 获取session中的用户
  • 判断用户是否存在,若不存在则报错401,不放行
  • 若存在,保存用户到ThreadLocal(用UserHolder方法实现)
  • 放行

preHandle 负责进门前的检查和准备,而 afterCompletion 负责离开后的打扫和清理,两者共同构成了一个完整且安全的请求处理闭环

public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取sessionHttpSession session = request.getSession();//2.获取session中的用户Object userDTO = session.getAttribute("user");//3.判断用户是否存在if(userDTO == null){//4.不放行response.setStatus(401);return false;}//5.存在,保存用户信息到ThreadLocalUserHolder.saveUser((UserDTO) userDTO);//6.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();}
}

【3】Mvc配置文件

编写好拦截器后要在配置文件中添加拦截器,并规定【无需拦截的接口】

@Configuration
public class MvcConfig implements WebMvcConfigurer {/*** 添加拦截器* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 添加拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( //排除不需要拦截的接口"/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);}
}


四、基于Redis实现短信校验

1、session共享问题

  • 多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时会导致数据丢失
  • 为了解决共享问题,我们采用独立于多个Tomcat之外的Redis存储器

2、Redis实现短信登录业务流程

(1)发送短信验证码流程

  • 由【保存验证码到session】改变为【保存验证码到Redis】,Redis数据结构采用【String】比较直观【key:phone | value:code

(2)短信验证码登录、注册流程

  • 校验验证码时,以手机号为key,从Redis中获取验证码
  • 由【保存用户信息到session】改变为【保存用户信息到Redis】,Redis数据结构采用【hash】比较合适【key:token | value:name:lisi
  • 最后要把token返回给客户端,用于登录校验

(3)登录校验流程

  • 由【请求并携带token】改变为【请求并携带token】,用token为key,在Redis中获取用户信息
  • 登录校验时,为什么用token作为key从redis中获取用户信息,而不用手机号作为key?
  • 答:

3、代码优化

(1)发送短信验证码 - 优化

    /*** 发送验证码 - session优化* @param phone* @param session* @return*/@Overridepublic Result sendCode(String phone, HttpSession session) {//1.验证手机号if (RegexUtils.isPhoneInvalid(phone)) {//2.如果手机号无效,返回错误信息return Result.fail("手机号格式错误!");}//3.如果有效,生成验证码String code = RandomUtil.randomNumbers(6);//【4.将验证码存入redis】新!!//采用String存储,【key:业务前缀+phone  value:code  过期时间:2min】stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);//5.发送验证码(模拟)log.debug("您的验证码是:{}",code);//6.返回成功return Result.ok();}

(2)短信验证码登录、注册 - 优化

问题1:将用户信息保存进Redis

  • 随机生成token作为登录令牌,也作为用户信息的key
  • 将【User对象】转换为【UserDTO】再转换为【hashmap】,转换为hashmap是方便Redis的hash格式存储(key | value:field:value) 
  • 存入Redis的hash结构中,用putAll可以一次性把用户所有属性存入(适用于传入map结构)
  • 设置token有效期(因为用户会源源不断地登录,如果不把不活跃的用户token清理,redis就会承受不住)

问题2:为什么要把UserDTO转换为String类型的Map?下面这段复杂代码是干什么的?

Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
  • 因为Redis Hash 更适合存储字符串值,而UserDTO中的id为Long类型,为了更加适配Redis Hash,这里需要自定义字段值编辑器,把所有属性类型转换为String

    /*** 短信实现登陆、注册* @param loginForm* @param session* @return*/@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机格式错误!");}//【2.从redis中获取验证码并校验】新!!String code = loginForm.getCode();Object cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);if(cacheCode == null || !cacheCode.equals(code)){//3.不一致,返回错误信息return Result.fail("验证码错误!");}//4.一致,查询数据库是否存在用户User user = query().eq("phone", phone).one(); //mybatis-plus实现用手机号查询用户信息//5.用户不存在,创建新用户if(user == null){user = createUserWithPhone(phone);}//【6.将用户保存进redis】新!!//6.1 随机生成token作为登录令牌String token = UUID.randomUUID().toString(true);//6.2 将User对象转换为hashMapUserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);//将UserDTO对象安全地转换为String类型的Map,忽略空值字段,确保数据格式统一,便于存储到Redis Hash结构中。Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));//6.3 存储 用putAll可以一次性把用户所有属性传进去stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);//6.4 设置token有效期(30min)stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL,TimeUnit.MINUTES);//【7.返回token】新!!return Result.ok(token);}

(3)登录校验 - 优化

问题一:为什么要在拦截器刷新token有效期?

  • 因为拦截器能拦截所有需要登录的请求,确保用户任何有效操作都会触发续期。
  • 用户每次操作都刷新过期时间,保持活跃会话,避免中途退出,同时自动清理不活跃的会话以释放资源。
public class LoginInterceptor implements HandlerInterceptor {//不能使用 @Autowired 是因为拦截器实例是在配置类中手动创建的// Spring 无法对手动 new 的对象进行依赖注入。// 构造函数注入是更明确、更安全的依赖管理方式。private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//【1.获取请求头中的token】新!!String token = request.getHeader("authorization");if(StrUtil.isBlank(token)){//不放行response.setStatus(401);return false;}String key = RedisConstants.LOGIN_USER_KEY + token;//【2.基于token获取redis中的用户】新!!//entries() 方法是用于获取 Redis Hash 结构中的所有字段和值Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);//3.判断用户是否存在if(userMap.isEmpty()){//4.不放行response.setStatus(401);return false;}//【5.将查询到的hash数据转换为DTO】新!!UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);//6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);//7.刷新token有效期//为什么要在拦截器刷新token有效期?//答:因为拦截器能拦截所有需要登录的请求,确保用户任何有效操作都会触发续期。// 用户每次操作都刷新过期时间,保持活跃会话,避免中途退出,同时自动清理不活跃的会话以释放资源。stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);//8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();}
}

4、解决状态登录刷新问题 - 拦截器链

        原方案中,Token的刷新依赖于登录校验拦截器。这意味着只有当用户访问需要登录的接口时,Token有效期才会被续期。如果用户长时间只访问公开接口(如首页、商品列表),即使他处于活跃状态,Token也可能因过期而失效,导致其在后续操作中意外退出。

优化点:

  • 我们解耦了【身份校验】和【Token刷新】这两个关注点,新增一个独立拦截器,并将其置于拦截器链的首位,以拦截所有路径。
  • 新拦截器职责:拦截所有路径,仅负责检查请求是否携带Token,以Token为key查询用户信息是否在Redis中。若Token有效且在Redis中,则将用户信息保存到ThreadLocal线程中,并刷新其有效期,无论该接口是否需要登录。
  • 原拦截器职责:拦截需要登录的路径,仅判断线程ThreadLocal中是否存在该用户,如果存在则放行,否则拦截。

改进优点

  • 此设计确保了只要用户在与应用进行任何交互,其登录状态就能得以保持,从而提供了连贯的用户体验,避免了活跃会话的中断。

(1)刷新Token拦截器

  • 从redis中获取token
    • 如果token不存在 → 放行(这时候用户信息并没有在线程ThreadLocal中,会被登录校验拦截器拦截)
  • 如果token存在,从Redis中获取用户信息
    • 如果用户不存在 → 放行(这时候用户信息并没有在线程ThreadLocal中,会被登录校验拦截器拦截)
  • 如果用户存在,将用户存入线程ThreadLocal
  • 刷新该用户的token
  • 放行
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true; //这里先放行,交给下一个拦截器判断}// 2.基于TOKEN获取redis中的用户String key  = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {return true; //这里先放行,交给下一个拦截器判断}// 5.将查询到的hash数据转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到 ThreadLocal(只有在redis中查到用户信息的才可以保存到ThreadLocal,其他的都会被第二个拦截器拦截)UserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}

(2)登录校验拦截器

  • 看线程ThreadLocal中是否存在该用户
    • 如果不存在 → 报错401,拦截
    • 如果存在 → 放行
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断是否要拦截【ThreadLocal中是否有用户信息】if (UserHolder.getUser() == null) {//不存在该用户,拦截response.setStatus(401);return false;}//存在该用户,放行return true;}
}

(3)MvcConfig配置类

用于添加拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 添加拦截器* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 添加拦截器//第一个拦截器:负责拦截需要【登录】的路径,做出是否拦截判断registry.addInterceptor(new LoginInterceptor()).excludePathPatterns( //排除不需要拦截的接口"/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);//第二个拦截器:负责拦截【所有】路径,负责将存在redis的用户存入ThreadLocal,并刷新tokenregistry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}

五、复习回顾 - Redis实现短信登录

        这个课程相比起苍穹外卖真的很烧脑,所以把今天学习的重点【Redis实现短信登录】功能总结回顾一下,主要梳理【发送验证码】【短信验证码登录、注册】【登录校验】三个模块的开发业务逻辑。

1、发送验证码 - redis流程

2、短信验证码登录、注册 - redis流程

3、登录校验 - 拦截器链流程

http://www.dtcms.com/a/473382.html

相关文章:

  • 汕头市通信建设管理局网站二网站手
  • FreeRTOS小记
  • 数据结构实战:顺序表全解析 - 从零实现到性能分析
  • 【C++进阶】继承上 概念及其定义 赋值兼容转换 子类默认成员函数的详解分析
  • 华为matebook16s 2022禁用触摸板和触摸屏操作
  • GridRow 和 Column 有啥区别
  • 030159网站建设与维护中国科技成就素材
  • Echarts 5.6.0 Grid 坐标系中 Y 轴可视化的优化之路
  • Java 线程池如何知道一个线程的任务已经执行完成
  • JVM字节码与类的加载(一):类的加载过程详解
  • 强军网网站建设网站需要备案才能建设吗
  • 耄大厨——AI厨师智能体(3-工具调用)
  • (二)黑马React(导航/账单项目)
  • SA-LSTM
  • 【Java并发】深入理解synchronized
  • Docker 安装 Harbor 教程
  • Python+Flask+Prophet 汽车之家二手车系统 逻辑回归 二手车推荐系统 机器学习(逻辑回归+Echarts 源码+文档)✅
  • AI_NovelGenerator:自动化长篇小说AI生成工具
  • 济南网站制作开通免费个人简历模板官网
  • 全链路智能运维中的异常检测与根因定位技术
  • 解构 CodexField:创作者经济到模型金融化的代币逻辑与潜力
  • SpringBoot 实现自动数据变更追踪
  • C语言⽂件操作讲解(3)
  • 对网站做数据分析北京市建设工程信息
  • 1.6虚拟机
  • XCP服务
  • Excel - Excel 列出一列中所有不重复数据
  • 如何获取用户右击的Excel单元格位置
  • 昆明企业网站建设公司虹口建设机械网站制作
  • 宁波p2p网站建设黑龙江省建设安全网站