(黑马点评)二、短信登录功能实现

2.1 基于传统Session实现的短信登录及其校验

2.1.1 基于Session登录校验的流程设计

2.1.2 实现短信验证码发送功能

请求接口/user/code
请求类型post
请求参数phone
返回值
    /*** 发送手机验证码*/@PostMapping("/code")public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {log.info("发送验证码, 手机号:{}", phone);return userService.sendCode(phone, session);}/*** 发送验证码* @param phone* @param session* @return*/@Overridepublic Result sendCode(String phone, HttpSession session) {// 1. 校验手机号码if(RegexUtils.isPhoneInvalid(phone)){return Result.fail("手机号码格式错误!");}// 2. 生成验证码String code = RandomUtil.randomNumbers(6);// 3. 将验证码保存到Session中session.setAttribute("code", code);//TODO 4. 调用阿里云 将短信信息发送到指定手机log.info("发送短信验证码成功,验证码:{}", code);return Result.ok();}

2.1.3 实现登录、注册功能

请求接口/user/login
请求类型post
请求参数LoginForm---> phone,code,[password]
返回值
    /*** 登录功能* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码*/@PostMapping("/login")public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){log.info("用户登录, 参数:{}", loginForm);return userService.login(loginForm, 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. 校验验证码Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();if(cacheCode==null || !cacheCode.toString().equals(code)){return Result.fail("验证码错误!");}// 3. 根据手机号查询用户 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();//    if 0 :创建新用户,保存数据库,将用户信息存储到Session//if(user == null){user = createUserWithPhone(phone);}//else: 登录成功,将用户信息存储到Sessionsession.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));return Result.ok();}/*** 根据手机号创建用户* @param phone* @return*/private User createUserWithPhone(String phone) {//创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));//保存用户save(user);// 返回return user;}

2.1.4 实现登录状态校验拦截器

        由于日后项目功能会越来越多,需要登录才能进行访问的界面也会越来越多,我们必须想办法将登录状态校验抽离出来形成一个前置校验的条件,再放行到后续逻辑。

1. 封装TreadLocal工具类

将用户信息保存到 TreadLocal中 并封装TreadLocal工具类用于 保存用户、获取用户、移除用户

在 urils / UserHolder 

/*** TreadLocal工具类*/
public class UserHolder {private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();// 保存用户public static void saveUser(UserDTO user){tl.set(user);}// 获取ThreadLocal中的用户public static UserDTO getUser(){return tl.get();}// 清空ThreadLocalpublic static void removeUser(){tl.remove();}
}
2. 创建登录拦截器

在 urils / LoginInterceptor 

public class LoginInterceptor implements HandlerInterceptor {/*** 前置拦截器*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 获取sessionHttpSession session = request.getSession();// 2. 获取session中的用户Object user = session.getAttribute("user");// 3. 判断用户是否存在if(user == null){response.setStatus(401);return false;}// 4. 如果存在,用户信息保存到 ThreadLocal 并放行UserHolder.saveUser((UserDTO) user);return true;}/*** 后置拦截器(移除用户)*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
3. 添加配置,生效拦截器,并配置放行路径

在 config/ MvcConfig

@Configuration
public class MvcConfig implements WebMvcConfigurer {/*** 添加拦截器* @param registry*/public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/host","/shop/**","/shop-type/**","/voucher/**");}
}

2.1.5 实现获取用户请求

前端点击我的,发送请求到后端,获取当前登录状态,方能进入个人中心

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

2.1.6 (附加)用户信息脱敏处理

为防止出现以下这种情况(将用户隐私信息暴露过多),我们采用UserDTO对象对用户信息脱敏处理:

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

并借助拷贝工具 进行对象拷贝

session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

2.2 传统Session在集群环境下的弊端

Session共享问题

多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

解决策略

1. 让Session可以共享

Tomcat提供了Session拷贝功能,但是这会增加服务器的额外内存开销,并且带来数据一致性问题

2. 【推荐】使用Redis进行替代

数据共享、内存存储(快)、key-value结构

2.3 基于Redis实现短信登录功能

2.3.1 基于Redis实现短信登录流程设计

发送短信验证码逻辑
校验登录状态逻辑

        对于验证码,使用 手机号码作为KEY,确保了正确的手机对应着正确的短信验证码。

        对于用户信息唯一标识使用 UUID生成的Token作为 KEY,而不使用手机号码,从而提高了用户数据安全性。

2.3.2 修改发送短信验证码功能

只需要在Session的基础上,将第三步保存到Redis中

格式:

keyvalueTTL
login:code:[手机号码][验证码]120S
//        // 3. 将验证码保存到Session中
//        session.setAttribute("code", code);// 3. 将验证码保存到Redis中stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);

2.3.3 修改登录、注册功能

1. 手机号校验

2. 从Redis中取出验证码进行校验

3.查询用户信息

4. 将用户信息存储到Redis ---> 需要以Hash结构进行存储 ----> 需要将user对象转成 Map对象

5. 将token返回给客户端 ,充当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. 校验验证码 REDIS
//        Object cacheCode = session.getAttribute("code");// 2.1 从Redis中获取验证码String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);// 2.2 校验验证码String code = loginForm.getCode();if(redisCode==null || !redisCode.equals(code)){return Result.fail("验证码错误!");}// 3. 根据手机号查询用户 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();//    if 0 :创建新用户,保存数据库,将用户信息存储到Sessionif(user == null){user = createUserWithPhone(phone);}
//        //else: 登录成功,将用户信息存储到Session
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));// 4. 将用户信息存储到Redis中// 1. 随机生成token,作为登录令牌 ---> UUID导入工具包中的方法,不要导入java自带的String token = UUID.randomUUID().toString(true);// 2. 以hash结构进行存储UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);//TODO 这里报错了,因为UserDTO中有个id属性,不是字符串,在Redis序列化下报错
//        Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);Map<String,Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));// 3. 存储到Redis中stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);// 给token设置有效期// 超过30分钟不访问任何界面就会剔除,所以还需要设置在访问过程中不断更新token的有效期// 实现方式: 在登录拦截器中进行处理stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);// 5. 返回token到客户端,客户端保存到浏览器中return Result.ok(token);}

2.3.4 添加刷新Token拦截器逻辑 (只做判断,不做拦截)

        首先,由于需要在自定义的拦截器中使用StringRedisTemplate对象,由于不是交由spring管理的,所以我们需要自己写构造函数进行导入。同时在MvcConfig中直接交给Spring管理

        其次,这里选择了新建一个专门负责刷新Token的“拦截器”,只做判断不做拦截。确保请求在经过登录校验拦截器之前,会统一先被该“拦截器”获取,并对Token进行判断,如果没有Token,则会被接下来的登录拦截器进行拦截

package com.hmdp.config;import com.hmdp.Interceptor.LoginInterceptor;
import com.hmdp.Interceptor.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 添加拦截器* @param registry*/public void addInterceptors(InterceptorRegistry registry) {// 刷新token拦截器 全部拦截 只做判断registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**");registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/host","/shop/**","/shop-type/**","/voucher/**");}
}
package com.hmdp.Interceptor;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;/*** Token缓存刷新拦截器 只会放行不会拦截*/
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;/*** 手动创建的对象,需要手动注入,所以需要构造方法* @param 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.存在,保存用户信息到 ThreadLocalUserHolder.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.3.5 (补充)退出登录功能实现

        前端点击退出登录时发送logout请求,我们只需要将TreadLocal的用户对象给清除掉,这样一来前端的请求就获取不到用户信息,强制被拦截到登录界面了

 

    /*** 登出功能* @return 无*/@PostMapping("/logout")public Result logout(){// 清除用户登录状态UserHolder.removeUser();return Result.ok();}

2.3.6 (补充)查看用户首页功能实现

点击用户头像,可以进入用户的首页

查看用户首页
    /*** 根据id查询用户* @param userId* @return*/@GetMapping("/{id}")public Result getUserById(@PathVariable("id") Long userId){User user = userService.getById(userId);if(user == null){return Result.ok();}UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);return Result.ok(userDTO);}

2.3.7【故障排查】关于一登陆后前端立马闪退回登录界面的问题

        在跟着视频练习的过程中,在所以代码开发完成后,我发现了每当自己点击登录校验成功后,前端又会重复的闪退回登录界面。对此我进行了以下排除手段:

1. 检查Redis是否 将 短信验证码及其用户token信息保存成功,有则证明这两个大环节没有问题

2. 猜测是拦截器问题:

         一开始,我们模仿老师将刷新Token的逻辑一并写到了登录校验拦截器上。但是!登录校验拦截器在开发的过程中,有一些需要Token【或是说需要校验TreadLocal对象】的接口并没有被拦截器拦截下来,导致前端认为该用户操作并未携带Token【没被存储到TreadLocal中】,从而误判为未登录状态,从而剔除该用户,强制跳转到登录界面。

        为此,秉承单一职责原则,对于Token,我们需要新建一个将所有界面都“拦截”的拦截器,这样子可以保证在进入后续拦截器的请求,不会再有被误判的情况出现。

    而且,也确保了不管用户进行了什么操作,Token都能刷新时长

(该故障经过拆分拦截器已正常解决,但是题主并没有深刻去揪到底是拿一些请求被误判了,这个故障原因也是我分析,如有高见请分享一下)

2.4 (TODO) 基于阿里云完善验证码功能

TODO

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

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

相关文章

Ubunutu 的 Bash 没有颜色

终端没有颜色&#xff1a; 取消注释 force_color_promptyes &#xff1a; 这时候就有颜色了&#xff1a;

three.js shader 实现天空中白云

three.js shader 实现天空中白云 预览&#xff1a; https://threehub.cn/#/codeMirror?navigationThreeJS&classifyshader&idwhiteCloud 更多案例 可见 预览&#xff1a; https://threehub.cn import * as THREE from "three"; import { OrbitControls …

按摩上门预约小程序源码系统 在线评价+即时服务 带完整的安装代码包以及搭建部署教程

系统概述 按摩上门预约小程序源码系统是一款专为按摩行业量身定制的移动端应用解决方案。它利用先进的互联网技术&#xff0c;将传统按摩服务与线上平台相结合&#xff0c;实现了用户与服务商之间的无缝对接。该系统不仅简化了预约流程&#xff0c;提高了服务效率&#xff0c;…

【Python】探索 PluginBase:Python 插件系统的灵活构建

我承认这道菜有赌的成分&#xff0c;果然还是赌输了。 在现代软件开发中&#xff0c;插件系统为应用程序提供了极大的灵活性和扩展性。Python&#xff0c;作为一种流行的编程语言&#xff0c;拥有丰富的库和框架来支持插件的开发。今天&#xff0c;我们将深入探讨一个名为Plug…

23.面试题02.07链表相交

public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {ListNode apheadA;ListNode bpheadB;int lenA0,lenB0;//求两个链表长度while(ap!null){apap.next;lenA;}while(bp!null){bpbp.next;lenB;}apheadA;bpheadB;int len0;//用来计算让…

BAS模型论文阅读

论文全名&#xff1a;Background Activation Suppression for Weakly Supervised Object Localization and Semantic Segmentation 论文pdf下载地址&#xff1a;2309.12943 (arxiv.org) 论文会议版全名&#xff1a;Background Activation Suppression for Weakly Supervised O…

AI产品经理面试20个问题汇总(含面试解题技巧、注意事项)

这题我会&#xff01;这是一个包含AI产品经理问题的备考文章&#xff0c;本文主要讲解AI产品经理的备考注意事项、真题展示、解题技巧及高效刷题方法&#xff0c;相信大家看完就一定能掌握技巧并且顺利通关&#xff01; 一、AI产品经理面试问题展示(20道) 1. 请描述一下你过…

Parallels Desktop 20(Mac虚拟机) v20.0.0 for Mac 最新破解版(支持M系列)

Parallels Desktop 20 for Mac 正式发布&#xff0c;完全支持 macOS Sequoia 和 Windows 11 24H2&#xff0c;并且在企业版中引入了全新的管理门户。 据介绍&#xff0c;新版本针对 Windows、macOS 和 Linux 虚拟机进行了大量更新&#xff0c;最大的亮点是全新推出的 Parallels…

[Leetcode] 227.基本计算器

标题&#xff1a;[Leetcode] 227.基本计算器 个人主页&#xff1a;水墨不写bug &#xff08;图片来源于网络&#xff09; // _ooOoo_ // // o8888888o // // …

PCIe扫盲(五)

系列文章目录 PCIe扫盲&#xff08;一&#xff09; PCIe扫盲&#xff08;二&#xff09; PCIe扫盲&#xff08;三&#xff09; PCIe扫盲&#xff08;四&#xff09; PCIe扫盲&#xff08;五&#xff09; 文章目录 系列文章目录TLP Header详解&#xff08;一&#xff09;Byte En…

Linux系统编程入门 | 模拟实现 ls -l 命令

模拟实现代码 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <pwd.h> #include <grp.h> #include <time.h> #include <string.h>int main(int argc, char* argv[]) {if (a…

为什么嫁人就要嫁公务员?稳定、收入高、福利好、资源多

在现代社会&#xff0c;择偶不仅仅是感情问题&#xff0c;更涉及到经济、社会地位和未来生活的方方面面。 对于很多女性来说&#xff0c;选择一个稳定、可靠的伴侣至关重要。而公务员作为一个备受尊敬的职业&#xff0c;成为了很多人心目中的理想对象。 那么&#xff0c;为什…

使用密钥文件登陆Linux服务器

假设A服务器为登陆目标,已经运行ssh服务。 B服务器作为登陆发起端。 登陆A服务器,账户S。 运行命令: ssh-keygen -t rsa 此时账户S家目录下会自动创建目录“.ssh”,目录下会有id_rsa和id_rsa.pub两个文件。 id_rsa为私钥,id_rsa.pub为公钥。 id_rsa文件内容下载到B服务…

web基础—dvwa靶场(七)SQL Injection

SQL Injection&#xff08;SQL注入&#xff09; SQL Injection&#xff08;SQL注入&#xff09;&#xff0c;是指攻击者通过注入恶意的SQL命令&#xff0c;破坏SQL查询语句的结构&#xff0c;从而达到执行恶意SQL语句的目的。SQL注入漏洞的危害是巨大的&#xff0c;常常会导致…

勒索软件和四重勒索策略:使用易备数据备份软件进行保护

文章内容&#xff1a; 1. 勒索行为类型 2. 勒索软件的演变&#xff1a;四重勒索 3. 遭遇勒索软件攻击时应遵循的准则 4. 防御勒索攻击的工具 5. 使用易备数据备份软件进行预防和备份 2024 年&#xff0c;勒索软件仍然是全球网络安全面临的最大威胁之一。威胁形势不断演变&#…

win11 下载安装MYSQL 5.7.30(保姆教程)

目录 一、下载安装包 二、安装 三、试一下 四、解决问题 1.如果出现“mysql不是内部或外部命令&#xff0c;也不是可运行的程序” 1.配置环境变量 2.重新打开cmd测试 一、下载安装包 进入下载链接&#xff1a;https://www.mysql.com/why-mysql/windows/https://www.mysq…

00898 互联网软件应用与开发自考复习题

资料来自互联网软件应用与开发大纲 南京航空航天大学 高纲4295和JSP 应用与开发技术(第 3 版) 马建红、李学相 清华大学出版社2019年 第一章 一、选择题 通过Internet发送请求消息和响应消息使用&#xff08;&#xff09;网络协议。 FTP B. TCP/IP C. HTTP D. DNS Web应…

OpenAI 的最强模型 o1 的“护城河”失守?谷歌 DeepMind 早已揭示相同原理

发布不到一周&#xff0c;OpenAI 的最新模型 o1 的“护城河”似乎已经失守。 近日&#xff0c;有人发现谷歌 DeepMind 早在今年 8 月发表的一篇论文&#xff0c;揭示了与 o1 模型极其相似的工作原理。 这项研究指出&#xff0c;在模型推理过程中增加测试时的计算量&#xff0c…

SAP SPROXY 配置

事务码SPROXY 然后找到目标的地址 然后创建新对象即可

基于springboot的在线视频点播系统

文未可获取一份本项目的java源码和数据库参考。 国外研究现状&#xff1a; 与传统媒体不同的是&#xff0c;新媒体在理念和应用上都采用了新颖的媒介或媒体。新媒体是指应用在数字技术、在传统媒体基础上改造、或者更新换代而来的媒介或媒体。新兴媒体与传统媒体在理念和应用…