【黑马点评 - 实战篇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实现短信登录】功能总结回顾一下,主要梳理【发送验证码】【短信验证码登录、注册】【登录校验】三个模块的开发业务逻辑。