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

二刷 黑马点评 短信登陆功能

概括:

使用nginx可以实现基于Lua脚本直接绕开tomcat访问redis
在没有nginx的情况下一台4核8G的tomcat最多处理1000左右的并发,通过nginx负载均衡分流使用集群支撑项目
在tomcat能够支撑兵法流量后,如果让tomcat直接访问Mysql,企业级服务器上大概就是4000-7000左右,最多上万的并发就会让mysql服务器的CPU和硬盘拉满,所以在高并发的场景下除了使用mysql集群降低压力外,还会引入redis集群减少对mysql访问
整个项目的框架图:
![[Pasted image 20250710163337.png]]

基于Session实现登陆流程

1.发送验证码:

用在提交手机号后,首先校验是否合法,合法的话后台产生对应的验证码,同时将验证码进行保存,通过短信的方式将验证码发送给用户

2.短信验证码登陆、注册:

用户将验证码和手机号进行输入,在后台从session中拿到当前验证码,然后进行校验
如果一直,先根据手机号查询用户,如果用户不存在就创建,保存到数据库,无论是否存在都将用户信息保存到session中,以方便后续获得当前登录信息

3.校验登陆状态:

用户请求时,会从cookie中携带sessionId到后台,如果没有session信息会进行拦截,有的话将用户信息保存到threadLocal中,同时放行
![[Pasted image 20250710163716.png]]

发送短信验证码

@PostMapping("code")  
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {  if(RegexUtils.isPhoneInvalid(phone))  {  return Result.fail("手机号格式错误!");  }  String code = RandomUtil.randomNumbers(6);  session.setAttribute("code",code);  log.debug("验证码{}",code);  // 发送短信验证码并保存验证码  return userService.sendCode(phone, session);  
}

什么是 Session?

  • 会话跟踪:HTTP 是无状态协议,每次请求都是独立的。Session通过唯一标识符(如JSESSIONID)来关联用户的多个请求。
  • 服务器端存储Session数据保存在服务器内存或数据库中,客户端只存储一个Session ID(通常通过 Cookie 或 URL 参数传递)。
  • 典型应用场景
    • 登录状态保持(如用户登录后,后续请求可识别身份)。
    • 购物车(暂存用户添加的商品)。
    • 临时数据(如验证码、表单分步提交)。
存储流程
  1. 首次请求
    • 服务器创建Session对象,生成唯一的Session ID(如JSESSIONID=12345)。
    • Session ID通过响应头的Set-Cookie字段返回给客户端。
  2. 后续请求
    • 客户端自动在请求头的Cookie字段中携带Session ID
    • 服务器通过Session ID找到对应的Session对象,获取存储的数据。

登录

@Override  
public Result login(LoginFormDTO loginForm, HttpSession session) {  // 1.校验手机号  String phone = loginForm.getPhone();  if (RegexUtils.isPhoneInvalid(phone)) {  // 2.如果不符合,返回错误信息  return Result.fail("手机号格式错误!");  }  // 3.校验验证码  Object cacheCode = session.getAttribute("code");  String code = loginForm.getCode();  if(cacheCode == null || !cacheCode.toString().equals(code)){  //3.不一致,报错  return Result.fail("验证码错误");  }  //一致,根据手机号查询用户  User user = query().eq("phone", phone).one();  //5.判断用户是否存在  if(user == null){  //不存在,则创建  user =  createUserWithPhone(phone);  }  //7.保存用户信息到session中  session.setAttribute("user",user);  return Result.ok();  
}
User user = query().eq("phone", phone).one();  

这里调用了mybatisPlus中的ServiceImpl,在实际项目开发中,若要为某个实体创建Service雷,只需要继承ServiceImpl类,再制定对应的Mapper接口与实体类
public class UserServiceImpl extends ServiceImpl<UserMapper, User>

  • UserMapper:继承自 BaseMapper<User> 的接口,定义数据库操作方法。
  • User:实体类,对应数据库表结构。
    在User类中指明了数据库表@TableName(“tb_user”)

query()等于select * from tb_user
.eq(“phone”,phone)实际上是where phone = ?
.one就查一个
.list就会返回一个集合

登录拦截功能

tomcat运行原理
![[Pasted image 20250710165719.png]]

当用户发起请求时,会访问向tomcat注册的端口,当监听线程检测到用户想和tomcat进行连接时,会由监听线程创建socket🔗,用户通过socket传递数据,而tomcat的socket接收到数据后,会由监听线程从tomcat的工作线程池取出一个线程来执行用户的请求,当我们的服务部署到tomcat后,线程会找到用户想要访问的工程,通过转发到controller、service、dao,并访问对应的数据库,在执行完请求后统一返回,找到tomcat的socket,再将数据写会用户端的socket。
用户通过去tomcat线程池分配的一个线程在执行工作,工作完后tomcat会进行回收,所以每个线程都是相对独立的,可以用ThreadLocal做到线程隔离,使得每个线程都操作自己的数据

threadlocal

无论是put还是get方法,都是先获取当前用户的线程,从线程中取出map,只要是不同的线程,就会有不同的map,从而做到线程隔离

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){//4.不存在,拦截,返回401状态码response.setStatus(401);return false;}//5.存在,保存用户信息到ThreadlocalUserHolder.saveUser((User)user);//6.放行return true;}
}

HttpServletRequest 与 HttpSession 的区别与联系

核心区别
维度HttpServletRequestHttpSession
作用处理单次 HTTP 请求,获取请求参数、头信息等跨请求维护会话状态,存储用户专属数据(如登录信息)
生命周期单次请求(请求结束后销毁)会话级(默认 30 分钟未活动失效,可手动控制)
存储位置客户端请求数据(内存中临时存储)服务器端(内存或持久化存储)
获取方式由 Web 容器自动创建,作为 Servlet 方法的参数传入通过request.getSession()获取 / 创建
数据范围仅在当前请求内有效跨请求有效,直至会话失效
核心联系
  1. 会话管理的纽带
    • Session 的入口HttpSession必须通过HttpServletRequest获取,例如:
      HttpSession session = request.getSession(); // 自动创建或获取已有会话
      
    • 会话 ID 的传递
      • 服务器通过HttpServletRequest生成唯一的JSESSIONID,通过CookieURL 重写传递给客户端。
      • 客户端后续请求携带JSESSIONIDHttpServletRequest解析后关联到对应的HttpSession
  2. 数据交互
    • 请求写入会话:在请求处理中,可将数据存入 Session:
      request.getSession().setAttribute("user", username);
      
    • 会话数据共享:同一客户端的后续请求可通过HttpServletRequest读取 Session 数据:
      String username = (String) request.getSession().getAttribute("user");
      
  3. 生命周期关联
    • Session 的创建:首次调用request.getSession()时触发创建。
    • Session 的失效:可通过request.getSession().invalidate()手动销毁,或通过web.xml配置超时时间。

这里拦截器先获取request中的sessionid,查询session中是否有这个用户,有的话存到threadlocal中并放行

     // 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);// token刷新的拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
  • LoginInterceptor:拦截除白名单外的所有请求,白名单包含静态资源、商品信息、验证码等无需登录即可访问的接口,顺序为 1(后执行)。
  • RefreshTokenInterceptor:刷新 Token 的拦截器,拦截所有请求,顺序为 0(先执行),确保每次请求都检查 Token 有效性。

session共享问题

每个tomcat都有一份属于自己的session,如何解决对多台tomcat的session进行同步呢?
可以当任意一台服务器的session进行修改时,都会同步给其他的Tomcat服务器的session,但这样服务器压力过大,且session拷贝数据的时候会出现延迟的问题
所以我们采用基于redis的方式来完成,把session换成redis,而redis数据本身就是共享的,可以避免session共享的问题

用Redis代替session的业务流程

通过Hash结构可以将对象中的每个字段独立存储,同时可以针对单个字段做CRUD,并且内存占用更少
![[Pasted image 20250710174141.png]]

整体访问流程

在登陆阶段查询用户信息时,将用户数据保存到redis,并生成token作为Redis的key,当校验用户是否登陆时,会带着token去访问,从redis取出token对应的value,判断是否存在,存在就保存到threadLocal中并放行
![[Pasted image 20250710174515.png]]

   String token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3.存储String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);

将user对象先转化为userDTO
然后通过userDTo转化为hashMap
beanTomap第一个参数为待转换的UserDTO对象
第二个是指定转换后的目标Map类型(此处为HashMap)
第三个是配置项,如这里省略属性为null的字段以及将所有属性值强转为String类型保证Map中值的类型统一
![[Pasted image 20250710175647.png]]

解决状态登录刷新的问题

在项目中我们需要实现刷新token令牌的存活时间,而在当前方案中,当用户访问不需要拦截的路径时,拦截器不会生效
![[Pasted image 20250710175846.png]]

因此我们可以添加一个拦截器拦截所有路径刷新令牌,同时第一个拦截器存储了threadlocal的数据,所以第二个拦截器只需要判断user对象是否存在即可
![[Pasted image 20250710175935.png]]

   @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;}

fiilBeanWithMap方法可以将map集合中的键值对填充到JavaBean对象中

总结

1.高并发场景下为何用 Redis 替代 Session?

因 Session 存储于单服务器本地,多 Tomcat 集群时存在共享问题,同步 Session 会增加服务器压力和延迟;Redis 为分布式共享存储,支持集群,可解决 Session 共享问题,适配高并发场景。

2.基于 Session 的登录流程核心步骤?

①发送验证码:校验手机号,生成并保存验证码至 Session,发送短信;
②登录验证:校验验证码,查询 / 创建用户,将用户信息存入 Session;
③状态校验:通过 Cookie 中的 SessionID 获取 Session,存在则存入 ThreadLocal 放行。

3.ThreadLocal 在登录拦截中的作用?

实现线程隔离,使 Tomcat 工作线程池中的每个线程独立存储用户信息,避免多线程数据冲突,确保请求处理过程中用户信息可安全访问。

4.两个拦截器(LoginInterceptor 与 RefreshTokenInterceptor)的分工?

RefreshTokenInterceptor:拦截所有请求,通过 Token 从 Redis 获取用户信息并存入 ThreadLocal,同时刷新 Token 有效期;
LoginInterceptor:拦截需登录的请求,校验 ThreadLocal 中是否有用户信息,未登录则拦截,确保权限控制。

5.MyBatis-Plus 中 ServiceImpl 的作用?

提供基础 CRUD 实现,继承后可直接使用query()等方法操作数据库,简化 Service 层开发,需指定对应 Mapper 接口和实体类。

6.Redis 存储登录信息为何采用 Hash 结构?

支持用户对象字段独立存储,可针对单个字段进行 CRUD 操作,内存占用更低,便于灵活管理用户信息。

7.Nginx 在高并发架构中的核心作用?

实现负载均衡分流,通过集群支撑高并发;可基于 Lua 脚本直接访问 Redis,绕开 Tomcat 减轻压力,提升系统吞吐量。

http://www.lryc.cn/news/584693.html

相关文章:

  • MatrixOne Intelligence v3.3 正式发布:结构化、自动化、可视化三重进化
  • 告别繁琐:API全生命周期管理的新范式——apiSQL
  • Android 网络开发核心知识点
  • 鸿蒙智行6月交付新车52747辆 单日交付量3651辆
  • android studio 运行,偶然会导致死机,设置Memory Settings尝试解决
  • OneFileLLM:一键聚合多源信息流
  • Logback日志框架配置实战指南
  • 浏览器 实时监听音量 实时语音识别 vue js
  • [特殊字符] ROM 和 RAM 知识点系统总结
  • C++中的左值、右值与std::move()
  • selenium中find_element()用法进行元素定位
  • 实时风险监控系统工具设计原理:2025异常检测算法与自动化响应机制
  • QT解析文本框数据——详解
  • 重新配置电脑中的环境变量
  • 安装VMware详细步骤
  • CIEDE2000 色差公式C++及MATLAB实现
  • Ansible:强大的自动部署工具
  • 国内如何考取Oracle大师
  • 解决问题的“测地线”:关于第一性原理与其他系统思考框架
  • HTTP 错误 500.19 - 打开 IIS 网页时出现内部服务器错误
  • 学习软件测试的第十四天(移动端)
  • 数据库操作核心知识点整理
  • 网安系列【15】之Docker未授权访问漏洞
  • 需求不稳定对项目进度影响大,如何进行变更控制
  • 显卡GPU的架构和工作原理
  • Rail开发日志_2
  • EasyCVR视频汇聚平台国标接入设备TCP主动播放失败排查指南
  • 【2025/07/10】GitHub 今日热门项目
  • 学习笔记(32):matplotlib绘制简单图表-数据分布图
  • STM32中DMA(直接存储器访问)详解