Spring Security3源码分析-SessionManagementFilter分析-上

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】

SessionManagementFilter过滤器对应的类路径为

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>

[color=red]session-fixation-protection属性支持三种不同的选项允许你使用

none:使得session固化攻击失效(未配置其他属性)

migrateSession:当用户经过认证后分配一个新的session,它保证原session的所有属性移到新session中

newSession:当用户认证后,建立一个新的session,原(未认证时)session的属性不会进行移到新session中来

[/color]

如果使用了标签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过滤器的执行过程,具体有什么作用呢?

[color=red]简单点概括就是:从session缓存中获取当前session信息,如果发现过期了,就跳转到expired-url配置的url或者响应session失效提示信息。当前session有哪些情况会导致session失效呢?这里的失效并不是指在web容器中session的失效,而是spring security把登录成功的session封装为SessionInformation并放到注册类缓存中,如果SessionInformation的expired变量为true,则表示session已失效。

所以,ConcurrentSessionFilter过滤器主要检查SessionInformation的expired变量的值

[/color]

为了能清楚解释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();
        }

经过以上分析,可以这么理解

[color=red]

如果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).提示信息)

[/color]

由于篇幅过长,SessionManagementFilter、org.springframework.security.web.session.HttpSessionEventPublisher就放到下部分再分析了


来源:http://ddrv.cn

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » Spring Security3源码分析-SessionManagementFilter分析-上

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏