苏格拉底曰:我惟一知道的,就是本身一无所知java
最近在翻阅Springboot Security板块中的会话管理器过滤器SessionManagementFilter源码的时候,发现其会对单用户的多会话进行校验控制,好比其下的某个策略ConcurrentSessionControlAuthenticationStrategy,节选部分代码apache
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的概念浏览器
通俗的理解应该是基于HTTP协议而产生的服务器级别的对象。其独立于客户端发的请求,并非客户端每一次的请求便会建立此对象,也不是客户端关闭了就会被注销。
故其依赖于HTTP服务器的运行,是独立于客户端的一种会话。目的也是保存公共的属性供页面间跳转的参数传递。服务器
HttpSession主要是经过HttpServletRequest#getSession()方法来建立,且只依赖于此方法的建立。通常都是用户校验经过后,应用才会调用此方法保存一些公共的属性,方便页面间传递。cookie
为了理解清楚上述的疑问,那么HttpSession的实现机制必须深刻的了解一下。由于其依赖于相应的HTTP服务器,就以Springboot内置的Tomcat服务器做为分析的入口吧。session
笔者以惟一入口HttpServletRequest#getSession()方法为源头,倒推其代码实现逻辑,大体梳理了下Tomcat服务器的HTTP请求步骤ide
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()方法来建立,具体的笔者不分析了。ui
总而言之,HttpSession的关键之处在于其对应的sessionId,每一个HttpSession都会有独一无二的sessionId与之对应,至于sessionId的建立读者可自行分析,只须要知道其在应用服务期间会对每一个HttpSession建立惟一的sessionId便可。this
上述讲解了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最大可拥有量
这里浅谈下Springboot Security中对Session的管理,主要是针对单个用户多session的状况。由HttpSecurity#sessionManagement()来进行相应的配置
@Override protected void configure(HttpSecurity http) throws Exception { // 单用户最大session数为2 http.sessionManagement().maximumSessions(2); }
通过上述的配置,便会引入两个关于session管理的过滤链,笔者按照过滤顺序分开浅析
主要是针对过时的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(); }
笔者只展现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服务中比较经常使用的对象,理解它的含义以及应用逻辑能够帮助咱们更好的使用它。以苏格拉底的话来讲就是我惟一知道的,就是本身一无所知