Spring Security3源码分析-SessionManagementFilter分析-上
程序员文章站
2022-04-25 08:22:19
...
SessionManagementFilter过滤器对应的类路径为
org.springframework.security.web.session.SessionManagementFilter
这个过滤器看名字就知道是管理session的了,http标签是自动配置时,默认是添加SessionManagementFilter过滤器到filterChainProxy中的,如果不想使用这个过滤器,需要做如下配置
其实在之前的过滤器中有使用到session策略了,但是没有细说。
SessionManagementFilter提供两大类功能:
1.session固化保护-通过session-fixation-protection配置
2.session并发控制-通过concurrency-control配置
下面看SessionManagementFilter的bean是如何创建的
接着看SessionManagementFilter过滤器执行过程
如果项目需要使用session的并发控制,需要做如下的配置
session-fixation-protection属性支持三种不同的选项允许你使用
none:使得session固化攻击失效(未配置其他属性)
migrateSession:当用户经过认证后分配一个新的session,它保证原session的所有属性移到新session中
newSession:当用户认证后,建立一个新的session,原(未认证时)session的属性不会进行移到新session中来
如果使用了标签concurrency-control,那么filterchainProxy中会添加新的过滤器
ConcurrentSessionFilter。这个过滤器的顺序在SecurityContextPersistenceFilter之前。说明未创建空的认证实体时就需要对session进行并发控制了
看ConcurrentSessionFilter执行过程
那么分析完ConcurrentSessionFilter过滤器的执行过程,具体有什么作用呢?
简单点概括就是:从session缓存中获取当前session信息,如果发现过期了,就跳转到expired-url配置的url或者响应session失效提示信息。当前session有哪些情况会导致session失效呢?这里的失效并不是指在web容器中session的失效,而是spring security把登录成功的session封装为SessionInformation并放到注册类缓存中,如果SessionInformation的expired变量为true,则表示session已失效。
所以,ConcurrentSessionFilter过滤器主要检查SessionInformation的expired变量的值
为了能清楚解释session 并发控制的过程,现在引入UsernamePasswordAuthenticationFilter过滤器,因为该过滤器就是对登录账号进行认证的,并且在分析UsernamePasswordAuthenticationFilter过滤器时,也没有详细讲解session的处理。
UsernamePasswordAuthenticationFilter的doFilter是由父类AbstractAuthenticationProcessingFilter完成的,截取部分重要代码
session处理的方法就是这一语句
如果是采用了并发控制session,则sessionStrategy为ConcurrentSessionControlStrategy类,具体源码:
经过以上分析,可以这么理解
如果concurrency-control标签配置了error-if-maximum-exceeded="true",max-sessions="1",那么第二次登录时,是登录不了的。如果error-if-maximum-exceeded="false",那么第二次是能够登录到系统的,但是第一个登录的账号再次发起请求时,会跳转到expired-url配置的url中(如果没有配置,则显示This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).提示信息)
由于篇幅过长,SessionManagementFilter、org.springframework.security.web.session.HttpSessionEventPublisher就放到下部分再分析了
org.springframework.security.web.session.SessionManagementFilter
这个过滤器看名字就知道是管理session的了,http标签是自动配置时,默认是添加SessionManagementFilter过滤器到filterChainProxy中的,如果不想使用这个过滤器,需要做如下配置
<security:http auto-config="true"> <security:session-management session-fixation-protection="none"/> </security:http>
其实在之前的过滤器中有使用到session策略了,但是没有细说。
SessionManagementFilter提供两大类功能:
1.session固化保护-通过session-fixation-protection配置
2.session并发控制-通过concurrency-control配置
下面看SessionManagementFilter的bean是如何创建的
void createSessionManagementFilters() { Element sessionMgmtElt = DomUtils.getChildElementByTagName(httpElt, Elements.SESSION_MANAGEMENT); Element sessionCtrlElt = null; String sessionFixationAttribute = null; String invalidSessionUrl = null; String sessionAuthStratRef = null; String errorUrl = null; //如果配置了标签,解析标签的属性、子标签 if (sessionMgmtElt != null) { sessionFixationAttribute = sessionMgmtElt.getAttribute(ATT_SESSION_FIXATION_PROTECTION); invalidSessionUrl = sessionMgmtElt.getAttribute(ATT_INVALID_SESSION_URL); sessionAuthStratRef = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_STRATEGY_REF); errorUrl = sessionMgmtElt.getAttribute(ATT_SESSION_AUTH_ERROR_URL); sessionCtrlElt = DomUtils.getChildElementByTagName(sessionMgmtElt, Elements.CONCURRENT_SESSIONS); //判断是否配置了concurrency-control子标签 if (sessionCtrlElt != null) { //配置了并发控制标签则创建并发控制过滤器和session注册的bean定义 createConcurrencyControlFilterAndSessionRegistry(sessionCtrlElt); } } if (!StringUtils.hasText(sessionFixationAttribute)) { sessionFixationAttribute = OPT_SESSION_FIXATION_MIGRATE_SESSION; } else if (StringUtils.hasText(sessionAuthStratRef)) { pc.getReaderContext().error(ATT_SESSION_FIXATION_PROTECTION + " attribute cannot be used" + " in combination with " + ATT_SESSION_AUTH_STRATEGY_REF, pc.extractSource(sessionCtrlElt)); } boolean sessionFixationProtectionRequired = !sessionFixationAttribute.equals(OPT_SESSION_FIXATION_NO_PROTECTION); BeanDefinitionBuilder sessionStrategy; //如果配置了concurrency-control子标签 if (sessionCtrlElt != null) { assert sessionRegistryRef != null; //session控制策略为ConcurrentSessionControlStrategy sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionControlStrategy.class); sessionStrategy.addConstructorArgValue(sessionRegistryRef); String maxSessions = sessionCtrlElt.getAttribute("max-sessions"); //添加最大session数 if (StringUtils.hasText(maxSessions)) { sessionStrategy.addPropertyValue("maximumSessions", maxSessions); } String exceptionIfMaximumExceeded = sessionCtrlElt.getAttribute("error-if-maximum-exceeded"); if (StringUtils.hasText(exceptionIfMaximumExceeded)) { sessionStrategy.addPropertyValue("exceptionIfMaximumExceeded", exceptionIfMaximumExceeded); } } else if (sessionFixationProtectionRequired || StringUtils.hasText(invalidSessionUrl) || StringUtils.hasText(sessionAuthStratRef)) { //如果没有配置concurrency-control子标签 //session控制策略是SessionFixationProtectionStrategy sessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(SessionFixationProtectionStrategy.class); } else { //<session-management session-fixation-protection="none"/> sfpf = null; return; } //创建SessionManagementFilter,并设置依赖的bean、property BeanDefinitionBuilder sessionMgmtFilter = BeanDefinitionBuilder.rootBeanDefinition(SessionManagementFilter.class); RootBeanDefinition failureHandler = new RootBeanDefinition(SimpleUrlAuthenticationFailureHandler.class); if (StringUtils.hasText(errorUrl)) { failureHandler.getPropertyValues().addPropertyValue("defaultFailureUrl", errorUrl); } sessionMgmtFilter.addPropertyValue("authenticationFailureHandler", failureHandler); sessionMgmtFilter.addConstructorArgValue(contextRepoRef); if (!StringUtils.hasText(sessionAuthStratRef)) { BeanDefinition strategyBean = sessionStrategy.getBeanDefinition(); if (sessionFixationProtectionRequired) { sessionStrategy.addPropertyValue("migrateSessionAttributes", Boolean.valueOf(sessionFixationAttribute.equals(OPT_SESSION_FIXATION_MIGRATE_SESSION))); } sessionAuthStratRef = pc.getReaderContext().generateBeanName(strategyBean); pc.registerBeanComponent(new BeanComponentDefinition(strategyBean, sessionAuthStratRef)); } if (StringUtils.hasText(invalidSessionUrl)) { sessionMgmtFilter.addPropertyValue("invalidSessionUrl", invalidSessionUrl); } sessionMgmtFilter.addPropertyReference("sessionAuthenticationStrategy", sessionAuthStratRef); sfpf = (RootBeanDefinition) sessionMgmtFilter.getBeanDefinition(); sessionStrategyRef = new RuntimeBeanReference(sessionAuthStratRef); } //创建并发控制Filter和session注册的bean private void createConcurrencyControlFilterAndSessionRegistry(Element element) { final String ATT_EXPIRY_URL = "expired-url"; final String ATT_SESSION_REGISTRY_ALIAS = "session-registry-alias"; final String ATT_SESSION_REGISTRY_REF = "session-registry-ref"; CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), pc.extractSource(element)); pc.pushContainingComponent(compositeDef); BeanDefinitionRegistry beanRegistry = pc.getRegistry(); String sessionRegistryId = element.getAttribute(ATT_SESSION_REGISTRY_REF); //判断是否配置了session-registry-ref属性,用于扩展 //默认情况下使用SessionRegistryImpl类管理session的注册 if (!StringUtils.hasText(sessionRegistryId)) { // Register an internal SessionRegistryImpl if no external reference supplied. RootBeanDefinition sessionRegistry = new RootBeanDefinition(SessionRegistryImpl.class); sessionRegistryId = pc.getReaderContext().registerWithGeneratedName(sessionRegistry); pc.registerComponent(new BeanComponentDefinition(sessionRegistry, sessionRegistryId)); } String registryAlias = element.getAttribute(ATT_SESSION_REGISTRY_ALIAS); if (StringUtils.hasText(registryAlias)) { beanRegistry.registerAlias(sessionRegistryId, registryAlias); } //创建并发session控制的Filter BeanDefinitionBuilder filterBuilder = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionFilter.class); //注入session的注册实现类 filterBuilder.addPropertyReference("sessionRegistry", sessionRegistryId); Object source = pc.extractSource(element); filterBuilder.getRawBeanDefinition().setSource(source); filterBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); String expiryUrl = element.getAttribute(ATT_EXPIRY_URL); if (StringUtils.hasText(expiryUrl)) { WebConfigUtils.validateHttpRedirect(expiryUrl, pc, source); filterBuilder.addPropertyValue("expiredUrl", expiryUrl); } pc.popAndRegisterContainingComponent(); concurrentSessionFilter = filterBuilder.getBeanDefinition(); sessionRegistryRef = new RuntimeBeanReference(sessionRegistryId); }
接着看SessionManagementFilter过滤器执行过程
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //省略…… //判断当前session中是否有SPRING_SECURITY_CONTEXT属性 if (!securityContextRepository.containsContext(request)) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && !authenticationTrustResolver.isAnonymous(authentication)) { try { //再通过sessionStrategy执行session固化、并发处理 //与UsernamePasswordAuthenticationFilter时处理一样,后面会仔细分析。 sessionStrategy.onAuthentication(authentication, request, response); } catch (SessionAuthenticationException e) { SecurityContextHolder.clearContext(); failureHandler.onAuthenticationFailure(request, response, e); return; } //把SecurityContext设置到当前session中 securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response); } else { if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) { if (invalidSessionUrl != null) { request.getSession(); redirectStrategy.sendRedirect(request, response, invalidSessionUrl); return; } } } } chain.doFilter(request, response); }
如果项目需要使用session的并发控制,需要做如下的配置
<session-management invalid-session-url="/login.jsp"> <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" expired-url="/login.jsp"/> </session-management>
session-fixation-protection属性支持三种不同的选项允许你使用
none:使得session固化攻击失效(未配置其他属性)
migrateSession:当用户经过认证后分配一个新的session,它保证原session的所有属性移到新session中
newSession:当用户认证后,建立一个新的session,原(未认证时)session的属性不会进行移到新session中来
如果使用了标签concurrency-control,那么filterchainProxy中会添加新的过滤器
ConcurrentSessionFilter。这个过滤器的顺序在SecurityContextPersistenceFilter之前。说明未创建空的认证实体时就需要对session进行并发控制了
看ConcurrentSessionFilter执行过程
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; HttpSession session = request.getSession(false); if (session != null) { //这个SessionInformation是在执行SessionManagementFilter时通过sessionRegistry构造的并且放置在map集合中的 SessionInformation info = sessionRegistry.getSessionInformation(session.getId()); //如果当前session已经注册了 if (info != null) { //如果当前session失效了 if (info.isExpired()) { // Expired - abort processing //强制退出 doLogout(request, response); //目标url为expired-url标签配置的属性值 String targetUrl = determineExpiredUrl(request, info); //跳转到指定url if (targetUrl != null) { redirectStrategy.sendRedirect(request, response, targetUrl); return; } else { response.getWriter().print("This session has been expired (possibly due to multiple concurrent " + "logins being attempted as the same user)."); response.flushBuffer(); } return; } else { // Non-expired - update last request date/time //session未失效,刷新时间 info.refreshLastRequest(); } } } chain.doFilter(request, response); }
那么分析完ConcurrentSessionFilter过滤器的执行过程,具体有什么作用呢?
简单点概括就是:从session缓存中获取当前session信息,如果发现过期了,就跳转到expired-url配置的url或者响应session失效提示信息。当前session有哪些情况会导致session失效呢?这里的失效并不是指在web容器中session的失效,而是spring security把登录成功的session封装为SessionInformation并放到注册类缓存中,如果SessionInformation的expired变量为true,则表示session已失效。
所以,ConcurrentSessionFilter过滤器主要检查SessionInformation的expired变量的值
为了能清楚解释session 并发控制的过程,现在引入UsernamePasswordAuthenticationFilter过滤器,因为该过滤器就是对登录账号进行认证的,并且在分析UsernamePasswordAuthenticationFilter过滤器时,也没有详细讲解session的处理。
UsernamePasswordAuthenticationFilter的doFilter是由父类AbstractAuthenticationProcessingFilter完成的,截取部分重要代码
try { //由子类UsernamePasswordAuthenticationFilter认证 //之前已经详细分析 authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed authentication return; } //由session策略类完成session固化处理、并发控制处理 //如果当前认证实体的已注册session数超出最大并发的session数 //这里会抛出AuthenticationException sessionStrategy.onAuthentication(authResult, request, response); } catch (AuthenticationException failed) { // Authentication failed //捕获到异常,直接跳转到失败页面或做其他处理 unsuccessfulAuthentication(request, response, failed); return; }
session处理的方法就是这一语句
sessionStrategy.onAuthentication(authResult, request, response);
如果是采用了并发控制session,则sessionStrategy为ConcurrentSessionControlStrategy类,具体源码:
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { //检查是否允许认证 checkAuthenticationAllowed(authentication, request); // Allow the parent to create a new session if necessary //执行父类SessionFixationProtectionStrategy的onAuthentication,完成session固化工作。其实就是重新建立一个session,并且把之前的session失效掉。 super.onAuthentication(authentication, request, response); //向session注册类SessionRegistryImpl注册当前session、认证实体 //实际上SessionRegistryImpl维护两个缓存列表,分别是 //1.sessionIds(Map):key=sessionid,value=SessionInformation //2.principals(Map):key=principal,value=HashSet sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal()); } //检查是否允许认证通过,如果通过直接返回,不通过,抛出AuthenticationException private void checkAuthenticationAllowed(Authentication authentication, HttpServletRequest request) throws AuthenticationException { //获取当前认证实体的session集合 final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false); int sessionCount = sessions.size(); //获取的并发session数(由max-sessions属性配置) int allowedSessions = getMaximumSessionsForThisUser(authentication); //如果当前认证实体的已注册session数小于max-sessions,允许通过 if (sessionCount < allowedSessions) { // They haven't got too many login sessions running at present return; } //如果allowedSessions配置为-1,说明未限制并发session数,允许通过 if (allowedSessions == -1) { // We permit unlimited logins return; } //如果当前认证实体的已注册session数等于max-sessions //判断当前的session是否已经注册过了,如果注册过了,允许通过 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); } protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException { //判断配置的error-if-maximum-exceeded属性,如果为true,抛出异常 if (exceptionIfMaximumExceeded || (sessions == null)) { throw new SessionAuthenticationException(messages.getMessage("ConcurrentSessionControllerImpl.exceededAllowed", new Object[] {new Integer(allowableSessions)}, "Maximum sessions of {0} for this principal exceeded")); } //如果配置的error-if-maximum-exceeded为false,接下来就是取出最先注册的session信息(这里是封装到SessionInformation),然后让最先认证成功的session过期。当ConcurrentSessionFilter过滤器检查到这个过期的session,就执行session失效的处理。 // Determine least recently used session, and mark it for invalidation SessionInformation leastRecentlyUsed = null; for (int i = 0; i < sessions.size(); i++) { if ((leastRecentlyUsed == null) || sessions.get(i).getLastRequest().before(leastRecentlyUsed.getLastRequest())) { leastRecentlyUsed = sessions.get(i); } } leastRecentlyUsed.expireNow(); }
经过以上分析,可以这么理解
如果concurrency-control标签配置了error-if-maximum-exceeded="true",max-sessions="1",那么第二次登录时,是登录不了的。如果error-if-maximum-exceeded="false",那么第二次是能够登录到系统的,但是第一个登录的账号再次发起请求时,会跳转到expired-url配置的url中(如果没有配置,则显示This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).提示信息)
由于篇幅过长,SessionManagementFilter、org.springframework.security.web.session.HttpSessionEventPublisher就放到下部分再分析了