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

  • 相关阅读:
    shell进行mysql统计
    java I/O总结
    Hbase源码分析:Hbase UI中Requests Per Second的具体含义
    ASP.NET Session State Overview
    What is an ISAPI Extension?
    innerxml and outerxml
    postman
    FileZilla文件下载的目录
    how to use webpart container in kentico
    Consider using EXISTS instead of IN
  • 原文地址:https://www.cnblogs.com/question-sky/p/10154404.html
Copyright © 2011-2022 走看看