详解Spring Session

详解Spring Session请求访问 tomcat gt tomact 检查请求的 sessionId gt 根据 sessionId 可以找到 session 对象 gt 使用该 session 对象创建 HttpServetRe 对象 gt 根据 sessionId 找不到 session 对象或者请求没有带 sessionId gt 创建一个 session 对象放进 Http

1.背景

1.1 session

说起session还要从http协议说起,http协议是无状态协议。所谓无状态协议,就是http请求之间是相互独立的。比如,你用百度搜索“2020年NBA总冠军是谁”,百度服务器会给你一个响应,这一次的http请求就算完事儿了。当你再去搜索“谁会赢得2020年美国总统大选”,这个时候百度服务器并不会感知到你上一次搜索了啥,你的两次搜索是完全独立的。可以用下面的图简略表示下。

详解Spring Session

对于百度搜索这样的场景,这样的无状态协议问题不大。但是对于大多数场景,这种无状态协议是远远不够的。比如,你要查看自己淘宝的购物车,淘宝会返回一个登陆页面,这个时候你输入了自己的用户名和密码,发起了一次登陆的http请求,假设用户名和密码都正确,这个时候系统会自动跳转到登陆成功的页面;但是当你点击付款的时候,发起了一次付款的http请求,这个时候系统会校验当前的用户信息,以及用户是否登陆,由于http是无状态协议,服务器并不知道你已经通过上次请求了登陆,会再次返回登陆页面。这样每操作一次都需要登陆一次,用户体验极差。

为了解决类似的问题,session应运而生。以淘宝登陆为例,它的主要工作流程如下:

1. 用户在登陆页面输入用户名和密码,点击登陆,发送登录请求至淘宝服务器。

2. 服务器接收到请求后,校验请求头部中未携带sessionId,于是创建session对象,并将sessionId放入response的cookie对象中。

3. 服务器校验用户和密码成功后,将用户信息存放至session中。请求返回,浏览器将response中cookie缓存至本地。

4. 用户点击付款,发送付款请求,由于请求的是相同的域名,浏览器会自动在请求头部中添加sessionId。

5. 服务器接收到请求后,识别到请求头部中sessionId,直接根据sessionId去查找上一次创建的session对象,并将该对象放入request对象中。

6. 服务器从request的session对象中获取到用户信息,校验通过,付款成功。

      详解Spring Session

        

1.2 分布式应用存在的问题

1.2.1问题复现

session的出现很大程度上解决了http请求无状态的问题。但是,随着业务量增加和对服务高可用的诉求,分布式应用和微服务得到了越来越广泛的应用,这样使用传统的session就会存在问题。接下来我们就来复现一下这个问题。

1.使用springboot实现一个简单的登陆功能

访问http://localhost:8080/login,如果用户已经登陆,则直接返回主页面;如果用户未登陆,跳转至登陆页面。在登陆页面输入用户名和密码,点击登陆,跳转至登陆成功页面(此处不对用户名和密码进行校验)。

package com.example.mysession.collector; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @Controller public class LoginContoller { / * 返回登陆页面 */ @GetMapping("/login") public String login() { HttpServletRequest request = getRequest(); HttpSession session = request.getSession(); // session中包含用户信息,直接返回主页 String usernameInSession = (String) session.getAttribute("username"); if (!StringUtils.isEmpty(usernameInSession)) { return "main"; } // 重定向至登陆页面 return "loginpage"; } / * 处理登陆请求 */ @PostMapping("/doLogin") public String doLogin(@RequestParam String username, @RequestParam String password) { HttpServletRequest request = getRequest(); HttpSession session = request.getSession(); // 将用户名放入session,并通知用户登陆成功(这里不对用户名和密码进行校验) session.setAttribute("username", username); return "main"; } private HttpServletRequest getRequest() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return servletRequestAttributes.getRequest(); } } 

2.我们来分析一下这个简单的登陆流程

(1)springboot启动后,访问http://localhost:8084/login。回想1.1中的流程,由于我们是首次访问,请求头中没有带sessionId,因此tomcat会为我们创建一个session对象,并将sessionId返回,浏览器将sessionId缓存至本地。

请求前:

详解Spring Session

请求后:

详解Spring Session

详解Spring Session

(2)输入用户名和密码,点击登陆。这时请求会带上SessionId,tomcat会根据SessionId找到之前创建session,并将其放入request对象中。doLogin()方法在处理时会将用户信息存入session。

详解Spring Session

(3)再访问http://localhost:8084/login。此次请求浏览器依然会将SessionId放入请求头部,login()方法根据SessionId找到session,进而在该session中找到用户信息,判断当前用户已经登陆,直接返回登陆成功页面。

详解Spring Session

3.使用nginx反向代理,实现简单的集群

(1)上述是单机环境,session完美解决了http协议无状态的问题,使得我们不需要频繁登陆。接下来,我们启动两个相同的服务(1中简单登陆服务),他们分别监听8084和8085端口。

详解Spring Session

(2)测试两个服务是否都正常可用

8084:

详解Spring Session

8085:

详解Spring Session

(3)使用ngnix进行反向代理

nginx.conf:

 #user nobody; worker_processes 1; events { # 并发连接数量 worker_connections 1024; } http { # tomcat集群 upstream tomcat_servers{ server 127.0.0.1:8084; server 127.0.0.1:8085; } server { # 监听80端口 listen 80; server_name localhost; # 将请求交给tomcat集群处理 location / { proxy_set_header Host $host:$server_port; proxy_pass http://tomcat_servers; } } } 

4.出现问题

(1)访问http://localhost/login。

详解Spring Session

(2)输入用户名和密码,点击登陆。

详解Spring Session

(3) 再次访问http://localhost/login。按照之前单节点的处理逻辑,应该返回登陆成功的页面才对,但是现在好像有点不太对?

详解Spring Session

1.2.2 原因分析

上述的现象比较明显,就是服务器好像并没有记住我们已经登陆过了。要解释这个问题我们首先要看下session的本质,session本质是tomcat为我们创建的一个对象,它使用ConcurrentHashMap保存属性值。tomcat本质也是一个Java程序,一个tomcat容器(8084)创建的session另外一个tomcat(8085)自然是获取不到的。

详解Spring Session

好了,有了上面的知识,我们再来看看这个问题是怎么产生的。我们启动了两个tomcat服务器(分别监听8084和8085端口),使用ngnix进行反向代理,为了达到负载均衡的效果,我们在ngnix上置的策略是轮流访问8084和8085,也就是请求第一次访问8084节点,第二次访问8085节点,再是8084…以此类推。

当我们第一次访问http://localhost/login时,ngnix会为我们路由到8084节点,由于是第一次访问8084会为我们创建一个session,sessionId为32AC7EEFE4CD0486F88B23B84594A768,并返回至浏览器。

当我们输入用户名和密码,点击登陆,第二次访问时,ngnix会为我们路由到8085节点。这一次我们请求头部带了SessionId:32AC7EEFE4CD0486F88B23B84594A768,但是这个session是8084节点创建的,8085节点获取不到这个session,所以8085又会创建一个新的session,709F94B639FFD8DF34110E1F1760D84D,返回至浏览器。

至此我们已经登陆成功了。我们再次访问http://localhost/login,ngnix会为我们路由到8084节点,请求头部带上上一次请求返回的sessionId:709F94B639FFD8DF34110E1F1760D84D,而这个是8085节点创建的,8084节点获取不到这个session,所以认为我们没有登陆过。

如此循环,我们会发现每一次请求,tomcat都会为我们创建一个session对象,而一个tomcat容器创建的session对象,另一个tomcat容器获取不到,这就是问题的根源。可能有的人会说,这不简单,在ngnix上配置策略,让每次来自同一个IP的用户访问同一个tomcat容器,这样不就好了?但是这样主要有两个问题,第一,如果一个节点出问题挂掉了,那么之前一直访问这个节点的用户登陆信息就丢失了,需要重新登陆,这显然不合理第二,移动端应用越来越多,而移动端可能换一个基站IP就变了。

2.spring session

2.1 解决问题的原理

知道了上述问题的原因,我们来看看怎么解决这个问题。首先来回顾下tomcat处理请求的流程

详解Spring Session

从上面的分析以及工作流程我们可以知道,解决该问题的关键就是如何让多个tomcat共享同一个session。我们自然而然就会想到,把session存储在一个公共的地方,这样每个tomcat就都会获取到了,这个公共的地方就是数据库(本文以Redis为例)。spring session的实现原理,就是在tomcat中加入了一个优先级很高的filter,来一个偷天换日,将request中的session置换为spring session,而这个spring session就存储在数据库中(Redis),这样一来,不同的tomca就可以共享同一个session啦。具体的细节我们在源码解析中再详细介绍,先看看spring session如何使用。

详解Spring Session

2.2 spring session的使用

可以参考spring官网https://docs.spring.io/spring-session/docs/1.3.0.RELEASE/reference/html5/guides/httpsession.html

1.添加依赖

 
   
   
   
     4.0.0 
    
    
    
      org.springframework.boot 
     
    
      spring-boot-starter-parent 
     
    
      2.3.5.RELEASE 
     
     
     
    
   
     com.example 
    
   
     my-session 
    
   
     0.0.1-SNAPSHOT 
    
   
     my-session 
    
   
     Demo project for Spring Boot 
    
    
    
      1.8 
     
    
    
     
     
       org.springframework.boot 
      
     
       spring-boot-starter-web 
      
     
     
     
       org.springframework.boot 
      
     
       spring-boot-starter-thymeleaf 
      
     
     
     
     
       org.springframework.session 
      
     
       spring-session-data-redis 
      
     
     
     
       org.springframework.boot 
      
     
       spring-boot-starter-data-redis 
      
     
    
    
     
      
      
        org.springframework.boot 
       
      
        spring-boot-maven-plugin 
       
      
     
    
   

 

2.添加一个配置文件

package com.example.mysession.config; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; @EnableRedisHttpSession public class SpringSessionConfig { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } } 

3.向spring中注入一个Initializer

package com.example.mysession.initializer; import jdk.nashorn.internal.runtime.regexp.joni.Config; import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer; import org.springframework.stereotype.Component; @Component public class Initializer extends AbstractHttpSessionApplicationInitializer { public Initializer() { super(Config.class); } }

4.测试

(1)访问http://localhost/login。

详解Spring Session

(2)输入用户名和密码,点击登陆。

详解Spring Session

(3)再次请求http://localhost/login。直接跳转至登陆成功页面。

详解Spring Session

这里我们可以看到,第二次请求和第三次请求携带的都是第一次请求返回的sessionId。

2.3 spring session源码解析

详解Spring Session

详解Spring Session

spring的官网写的非常清晰,我们使用@EnableRedisHttpSession注解创建了一个springSessionRepositoryFilter。而这个filter就是用来将tomcat创建的session对象替换为spring session。

实际处理的Filter时SessionRepositoryFilter,而SessionRepositoryFilter又继承OncePerRequestFilter,所以dofilter()方法在OncePerRequestFilter类中实现。

详解Spring Session

OncePerRequestFilter中的dofilter()方法

 / * This {@code doFilter} implementation stores a request attribute for "already * filtered", proceeding without filtering again if the attribute is already there. * @param request the request * @param response the response * @param filterChain the filter chain * @throws ServletException if request is not HTTP request * @throws IOException in case of I/O operation exception */ @Override public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { throw new ServletException("OncePerRequestFilter just supports HTTP requests"); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String alreadyFilteredAttributeName = this.alreadyFilteredAttributeName; boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null; // 已经过滤过了 if (hasAlreadyFilteredAttribute) { if (DispatcherType.ERROR.equals(request.getDispatcherType())) { doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain); return; } // Proceed without invoking this filter... filterChain.doFilter(request, response); } // 还未过滤 else { // Do invoke this filter... request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { doFilterInternal(httpRequest, httpResponse, filterChain); } finally { // Remove the "already filtered" request attribute for this request. request.removeAttribute(alreadyFilteredAttributeName); } } }

我们来看这个doFilterInternal方法。(SessionRepositoryFilter类中的方法)这里采用了装饰器模式,将HttpServletRequest和HttpServletResponse封装成SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。在这两个Wrapper中对tomcat的session进行了替换。

 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response); try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { wrappedRequest.commitSession(); } } 

 

 

 

 

 

 

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/224757.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 上午11:12
下一篇 2026年3月17日 上午11:13


相关推荐

  • 数论题中(杜教筛)交换求和符号

    数论题中(杜教筛)交换求和符号文章目录方阵下三角约数倍数狄利克雷卷积以及杜教筛学习笔记突然对交换求和符号有了新的理解了,用矩阵转置的思路就很好理解,外层循环相当于枚举行,内层枚举列,交换次序就是先枚举列,再枚举行方阵正常的就是∑i=1n∑j=1nf(i,j)=∑j=1n∑i=1nf(i,j)\sum_{i=1}^n\sum_{j=1}^nf(i,j)=\sum_{j=1}^n\sum_{i=1}^nf(i,j)…

    2022年10月12日
    6
  • pytorch 自定义卷积核进行卷积操作[通俗易懂]

    pytorch 自定义卷积核进行卷积操作[通俗易懂]一卷积操作:在pytorch搭建起网络时,大家通常都使用已有的框架进行训练,在网络中使用最多就是卷积操作,最熟悉不过的就是torch.nn.Conv2d(in_channels,out_channels,kernel_size,stride=1,padding=0,dilation=1,groups=1,bias=True)通过上面的输入发现想自定义自己的卷积核,比如高斯…

    2022年5月28日
    44
  • 图像特征提取总结_将劣势转化为优势的例子

    图像特征提取总结_将劣势转化为优势的例子转载地址:https://blog.csdn.net/lskyne/article/details/8654856 特征提取是计算机视觉和图像处理中的一个概念。它指的是使用计算机提取图像信息,决定每个图像的点是否属于一个图像特征。特征提取的结果是把图像上的点分为不同的子集,这些子集往往属于孤立的点、连续的曲线或者连续的区域。 特征的定义        至今为止特征没有万能和精确的定义。…

    2025年7月8日
    4
  • 什么是泛型以及在集合中泛型的使用[通俗易懂]

    什么是泛型以及在集合中泛型的使用[通俗易懂]什么是泛型?泛型最常与集合使用,因为泛型最开始开始被加入Java就是为了解决集合向下转型一类问题的。如果我们有这样一个需求:定义一个描述类圆,要求圆中的数据类型是不确定的,也就是声名属性的时候,属性类型是不确定的。比如描述类圆中有半径,要求半径可以用int,也可以用double。那么此时数据类型不确定,就使用泛型,把数据类型参数化。集合中泛型的使用List中使用泛型在我们创建集合时使用<>来声明List集合只能保存Dog类对象Listdogs=newArrayList<&gt

    2022年6月22日
    30
  • MDaemon退信之本域收发

    MDaemon退信之本域收发

    2021年8月13日
    80
  • 分页的sql语句_如何实现分页效果

    分页的sql语句_如何实现分页效果下文将为您介绍三种SQL分页语句写法,如果您也遇到过类似的问题,不妨一看,相信对您会有所启迪。SQL分页操作是经常会遇到的,下面就将为您介绍三种SQL分页语句,供您参考,希望对您学习SQL分页能够有所帮助。方法一(适用于SQLServer2000/2005)SELECTTOP页大小* FROMtable1 WHEREidNOTIN

    2026年2月9日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号