二刷 黑马点评 短信登陆功能
概括:
使用nginx可以实现基于Lua脚本直接绕开tomcat访问redis
在没有nginx的情况下一台4核8G的tomcat最多处理1000左右的并发,通过nginx负载均衡分流使用集群支撑项目
在tomcat能够支撑兵法流量后,如果让tomcat直接访问Mysql,企业级服务器上大概就是4000-7000左右,最多上万的并发就会让mysql服务器的CPU和硬盘拉满,所以在高并发的场景下除了使用mysql集群降低压力外,还会引入redis集群减少对mysql访问
整个项目的框架图:
基于Session实现登陆流程
1.发送验证码:
用在提交手机号后,首先校验是否合法,合法的话后台产生对应的验证码,同时将验证码进行保存,通过短信的方式将验证码发送给用户
2.短信验证码登陆、注册:
用户将验证码和手机号进行输入,在后台从session中拿到当前验证码,然后进行校验
如果一直,先根据手机号查询用户,如果用户不存在就创建,保存到数据库,无论是否存在都将用户信息保存到session中,以方便后续获得当前登录信息
3.校验登陆状态:
用户请求时,会从cookie中携带sessionId到后台,如果没有session信息会进行拦截,有的话将用户信息保存到threadLocal中,同时放行
发送短信验证码
@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 参数传递)。 - 典型应用场景:
- 登录状态保持(如用户登录后,后续请求可识别身份)。
- 购物车(暂存用户添加的商品)。
- 临时数据(如验证码、表单分步提交)。
存储流程
- 首次请求:
- 服务器创建
Session
对象,生成唯一的Session ID
(如JSESSIONID=12345
)。 - 将
Session ID
通过响应头的Set-Cookie
字段返回给客户端。
- 服务器创建
- 后续请求:
- 客户端自动在请求头的
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运行原理
当用户发起请求时,会访问向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 的区别与联系
核心区别
维度 | HttpServletRequest | HttpSession |
---|---|---|
作用 | 处理单次 HTTP 请求,获取请求参数、头信息等 | 跨请求维护会话状态,存储用户专属数据(如登录信息) |
生命周期 | 单次请求(请求结束后销毁) | 会话级(默认 30 分钟未活动失效,可手动控制) |
存储位置 | 客户端请求数据(内存中临时存储) | 服务器端(内存或持久化存储) |
获取方式 | 由 Web 容器自动创建,作为 Servlet 方法的参数传入 | 通过request.getSession() 获取 / 创建 |
数据范围 | 仅在当前请求内有效 | 跨请求有效,直至会话失效 |
核心联系
- 会话管理的纽带
- Session 的入口:
HttpSession
必须通过HttpServletRequest
获取,例如:HttpSession session = request.getSession(); // 自动创建或获取已有会话
- 会话 ID 的传递:
- 服务器通过
HttpServletRequest
生成唯一的JSESSIONID
,通过Cookie或URL 重写传递给客户端。 - 客户端后续请求携带
JSESSIONID
,HttpServletRequest
解析后关联到对应的HttpSession
。
- 服务器通过
- Session 的入口:
- 数据交互
- 请求写入会话:在请求处理中,可将数据存入 Session:
request.getSession().setAttribute("user", username);
- 会话数据共享:同一客户端的后续请求可通过
HttpServletRequest
读取 Session 数据:String username = (String) request.getSession().getAttribute("user");
- 请求写入会话:在请求处理中,可将数据存入 Session:
- 生命周期关联
- Session 的创建:首次调用
request.getSession()
时触发创建。 - Session 的失效:可通过
request.getSession().invalidate()
手动销毁,或通过web.xml
配置超时时间。
- Session 的创建:首次调用
这里拦截器先获取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,并且内存占用更少
整体访问流程
在登陆阶段查询用户信息时,将用户数据保存到redis,并生成token作为Redis的key,当校验用户是否登陆时,会带着token去访问,从redis取出token对应的value,判断是否存在,存在就保存到threadLocal中并放行
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中值的类型统一
解决状态登录刷新的问题
在项目中我们需要实现刷新token令牌的存活时间,而在当前方案中,当用户访问不需要拦截的路径时,拦截器不会生效
因此我们可以添加一个拦截器拦截所有路径刷新令牌,同时第一个拦截器存储了threadlocal的数据,所以第二个拦截器只需要判断user对象是否存在即可
@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 减轻压力,提升系统吞吐量。