zoukankan      html  css  js  c++  java
  • 浅析HttpSession

    苏格拉底曰:我唯一知道的,就是自己一无所知

    源头

    最近在翻阅Springboot Security板块中的会话管理器过滤器SessionManagementFilter源码的时候,发现其会对单用户的多会话进行校验控制,比如其下的某个策略ConcurrentSessionControlAuthenticationStrategy,节选部分代码

    	public void onAuthentication(Authentication authentication,
    			HttpServletRequest request, HttpServletResponse response) {
    
    		// 获取单用户的多会话
    		final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
    				authentication.getPrincipal(), false);
    
    		// 一系列判断
    		int sessionCount = sessions.size();
    		int allowedSessions = getMaximumSessionsForThisUser(authentication);
    
    		....
    		....
    
    		// session超出后的操作,一般是抛异常结束filter的过滤
    		allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
    	}
    

    笔者一般的思维是认为单个校验通过的用户有单一的会话,为何会有多个会话呢?那多个会话其又是如何管理的呢?带着疑问探究下HttpSession的概念

    何为HttpSession

    通俗的理解应该是基于HTTP协议而产生的服务器级别的对象。其独立于客户端发的请求,并不是客户端每一次的请求便会创建此对象,也不是客户端关闭了就会被注销。
    故其依赖于HTTP服务器的运行,是独立于客户端的一种会话。目的也是保存公共的属性供页面间跳转的参数传递。

    如何使用HttpSession

    HttpSession主要是通过HttpServletRequest#getSession()方法来创建,且只依赖于此方法的创建。一般都是用户校验通过后,应用才会调用此方法保存一些公共的属性,方便页面间传递。

    HttpSession的实现机制

    为了理解清楚上述的疑问,那么HttpSession的实现机制必须深入的了解一下。因为其依赖于相应的HTTP服务器,就以Springboot内置的Tomcat服务器作为分析的入口吧。

    代码层

    笔者以唯一入口HttpServletRequest#getSession()方法为源头,倒推其代码实现逻辑,大致梳理了下Tomcat服务器的HTTP请求步骤

    	AbstractEndpoint作为服务的创建入口,其子类NioEndpoint则采用NIO思想创建TCP服务并运行多个Poller线程用于接收客户端(浏览器)的请求-->
    	通过Poller#processSocket()方法调用内部类SocketProcessor来间接引用AbstractProtocol内部类ConnectionHandler处理具体的请求-->
    	HTTP相关的请求则交由AbstractHttp11Protocol#createProcessor()方法创建Http11Processor对象处理---->
    	Http11Processor引用CoyoteAdapter对象来包装成org.apache.catalina.connector.Request对象来最终处理创建HttpSession-->
    	优先解析URL中的JSESSIONID参数,如果没有则尝试获取客户端Cookie中的JSESSIONID键值,最终存入至相应Session对象属性sessionId中,避免对来自同一来源的客户端重复创建HttpSession
    

    基于上述的步骤用户在获取HttpSession对象时,会调用Request#doGetSession()方法来创建,具体的笔者不分析了。

    总而言之,HttpSession的关键之处在于其对应的sessionId,每个HttpSession都会有独一无二的sessionId与之对应,至于sessionId的创建读者可自行分析,只需要知道其在应用服务期间会对每个HttpSession创建唯一的sessionId即可。

    保存方式

    上述讲解了HttpSession的获取方式是基于sessionId的,那么肯定有一个出口去保存相应的键值对,仔细一看发现其是基于cookie去实现的,附上Request#doGetSession()方法关键源码

        protected Session doGetSession(boolean create) {
    
            .....
            .....
    
            // session不为空且支持cookie机制
            if (session != null
                    && context.getServletContext()
                            .getEffectiveSessionTrackingModes()
                            .contains(SessionTrackingMode.COOKIE)) {
                // 默认创建Key为JSESSIONID的Cookie对象,并设置maxAge=-1
                Cookie cookie =
                    ApplicationSessionCookieConfig.createSessionCookie(
                            context, session.getIdInternal(), isSecure());
    
                response.addSessionCookieInternal(cookie);
            }
    
            if (session == null) {
                return null;
            }
    
            session.access();
            return session;
        }
    

    很明显,由上述的代码可知,HttpSession的流通还需要依赖Cookie机制的使用。此处谈及一下Cookie对象中的maxAge,可以看下其API说明

        /**
         * Sets the maximum age of the cookie in seconds.
         * <p>
         * A positive value indicates that the cookie will expire after that many
         * seconds have passed. Note that the value is the <i>maximum</i> age when
         * the cookie will expire, not the cookie's current age.
         * <p>
         * A negative value means that the cookie is not stored persistently and
         * will be deleted when the Web browser exits. A zero value causes the
         * cookie to be deleted.
         *
         * @param expiry
         *            an integer specifying the maximum age of the cookie in
         *            seconds; if negative, means the cookie is not stored; if zero,
         *            deletes the cookie
         * @see #getMaxAge
         */
        public void setMaxAge(int expiry) {
            maxAge = expiry;
        }
    

    默认maxAge值为-1,即当浏览器进程重开之前,此对应的JSESSIONID的cookie值都会在访问服务应用的时候被带上。
    由此处其实可以理解,如果多次重开浏览器进程并登录应用,则会出现单用户有多个session的情况。所以才有了限制Session最大可拥有量

    HttpSession的管理

    这里浅谈下Springboot Security中对Session的管理,主要是针对单个用户多session的情况。由HttpSecurity#sessionManagement()来进行相应的配置

        @Override
        protected void configure(HttpSecurity http) throws Exception {
    	    // 单用户最大session数为2
            http.sessionManagement().maximumSessions(2);
        }
    

    经过上述的配置,便会引入两个关于session管理的过滤链,笔者按照过滤顺序分开浅析

    ConcurrentSessionFilter

    主要是针对过期的session进行相应的注销以及退出操作,看下关键的处理代码

    	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    			throws IOException, ServletException {
    		HttpServletRequest request = (HttpServletRequest) req;
    		HttpServletResponse response = (HttpServletResponse) res;
    		
    		// 获取HttpSession
    		HttpSession session = request.getSession(false);
    
    		if (session != null) {
    			SessionInformation info = sessionRegistry.getSessionInformation(session
    					.getId());
    
    			if (info != null) {
    				// 如果设置为过期标志,则开始清理操作
    				if (info.isExpired()) {
    					// 默认使用SecurityContextLogoutHandler处理退出操作,内含session注销
    					doLogout(request, response);
    					
    					// 事件推送,默认是直接输出session数过多的信息
    					this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
    					return;
    				}
    				else {
    					// Non-expired - update last request date/time
    					sessionRegistry.refreshLastRequest(info.getSessionId());
    				}
    			}
    		}
    
    		chain.doFilter(request, response);
    	}
    

    前文也提及,如果服务应用期间,要注销session,只能调用相应的session.invalid()方法。直接看下SecurityContextLogoutHandler#logout()源码

    	public void logout(HttpServletRequest request, HttpServletResponse response,
    			Authentication authentication) {
    		Assert.notNull(request, "HttpServletRequest required");
    		if (invalidateHttpSession) {
    			HttpSession session = request.getSession(false);
    			if (session != null) {
    				// 注销
    				session.invalidate();
    			}
    		}
    
    		if (clearAuthentication) {
    			SecurityContext context = SecurityContextHolder.getContext();
    			context.setAuthentication(null);
    		}
    
    		// 清理上下文
    		SecurityContextHolder.clearContext();
    	}
    

    SessionManagementFilter

    笔者只展示ConcurrentSessionControlAuthenticationStrategy策略类用于展示session的最大值校验

    	public void onAuthentication(Authentication authentication,
    			HttpServletRequest request, HttpServletResponse response) {
    		// 获取当前校验通过的用户所关联的session数量
    		final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
    				authentication.getPrincipal(), false);
    
    		int sessionCount = sessions.size();
    		// 最大session支持,可配置
    		int allowedSessions = getMaximumSessionsForThisUser(authentication);
    
    		if (sessionCount < allowedSessions) {
    			// They haven't got too many login sessions running at present
    			return;
    		}
    
    		if (allowedSessions == -1) {
    			// We permit unlimited logins
    			return;
    		}
    
    		if (sessionCount == allowedSessions) {
    			HttpSession session = request.getSession(false);
    
    			if (session != null) {
    				// Only permit it though if this request is associated with one of the
    				// already registered sessions
    				for (SessionInformation si : sessions) {
    					if (si.getSessionId().equals(session.getId())) {
    						return;
    					}
    				}
    			}
    			// If the session is null, a new one will be created by the parent class,
    			// exceeding the allowed number
    		}
    		// 超出对应数的处理
    		allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
    	}
    

    继续跟踪allowableSessionsExceeded()方法

    	protected void allowableSessionsExceeded(List<SessionInformation> sessions,
    			int allowableSessions, SessionRegistry registry)
    			throws SessionAuthenticationException {
    		// 1.要么抛异常
    		if (exceptionIfMaximumExceeded || (sessions == null)) {
    			throw new SessionAuthenticationException(messages.getMessage(
    					"ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
    					new Object[] { Integer.valueOf(allowableSessions) },
    					"Maximum sessions of {0} for this principal exceeded"));
    		}
    
    		// Determine least recently used session, and mark it for invalidation
    		SessionInformation leastRecentlyUsed = null;
    
    		for (SessionInformation session : sessions) {
    			if ((leastRecentlyUsed == null)
    					|| session.getLastRequest()
    							.before(leastRecentlyUsed.getLastRequest())) {
    				leastRecentlyUsed = session;
    			}
    		}
    		// 2.要么设置对应的expired为true,最后交由上述的ConcurrentSessionFilter来处理
    		leastRecentlyUsed.expireNow();
    	}
    

    关于session的保存,大家可以关注RegisterSessionAuthenticationStrategy注册策略,其是排在上述的策略之后的,就是先判断再注册,很顺畅的逻辑。笔者此处就不分析了,读者可自行分析

    小结

    HttpSession是HTTP服务中比较常用的对象,理解它的含义以及应用逻辑可以帮助我们更好的使用它。以苏格拉底的话来说就是我唯一知道的,就是自己一无所知

  • 相关阅读:
    简单RPC框架-业务线程池
    简单RPC框架-基于Consul的服务注册与发现
    简易RPC框架-学习使用
    统一配置中心2
    dubbo+zipkin调用链监控
    Spring Cache扩展:注解失效时间+主动刷新缓存
    转:深入理解Java G1垃圾收集器
    dubbo源码分析(一)
    理解WEB API网关
    理解zookeeper选举机制
  • 原文地址:https://www.cnblogs.com/question-sky/p/10154404.html
Copyright © 2011-2022 走看看