美文网首页Spring Security
聊聊SecurityContextPersistenceFilt

聊聊SecurityContextPersistenceFilt

作者: go4it | 来源:发表于2017-12-17 12:33 被阅读188次

    本文主要研究下SecurityContextPersistenceFilter的作用。

    filter顺序

    spring security内置的各种filter:

    Alias Filter Class Namespace Element or Attribute
    CHANNEL_FILTER ChannelProcessingFilter http/intercept-url@requires-channel
    SECURITY_CONTEXT_FILTER SecurityContextPersistenceFilter http
    CONCURRENT_SESSION_FILTER ConcurrentSessionFilter session-management/concurrency-control
    HEADERS_FILTER HeaderWriterFilter http/headers
    CSRF_FILTER CsrfFilter http/csrf
    LOGOUT_FILTER LogoutFilter http/logout
    X509_FILTER X509AuthenticationFilter http/x509
    PRE_AUTH_FILTER AbstractPreAuthenticatedProcessingFilter Subclasses N/A
    CAS_FILTER CasAuthenticationFilter N/A
    FORM_LOGIN_FILTER UsernamePasswordAuthenticationFilter http/form-login
    BASIC_AUTH_FILTER BasicAuthenticationFilter http/http-basic
    SERVLET_API_SUPPORT_FILTER SecurityContextHolderAwareRequestFilter http/@servlet-api-provision
    JAAS_API_SUPPORT_FILTER JaasApiIntegrationFilter http/@jaas-api-provision
    REMEMBER_ME_FILTER RememberMeAuthenticationFilter http/remember-me
    ANONYMOUS_FILTER AnonymousAuthenticationFilter http/anonymous
    SESSION_MANAGEMENT_FILTER SessionManagementFilter session-management
    EXCEPTION_TRANSLATION_FILTER ExceptionTranslationFilter http
    FILTER_SECURITY_INTERCEPTOR FilterSecurityInterceptor http
    SWITCH_USER_FILTER SwitchUserFilter N/A

    可以看到SecurityContextPersistenceFilter优先级仅次于ChannelProcessingFilter

    SecurityContextPersistenceFilter

    spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/context/SecurityContextPersistenceFilter.java

    /**
     * Populates the {@link SecurityContextHolder} with information obtained from the
     * configured {@link SecurityContextRepository} prior to the request and stores it back in
     * the repository once the request has completed and clearing the context holder. By
     * default it uses an {@link HttpSessionSecurityContextRepository}. See this class for
     * information <tt>HttpSession</tt> related configuration options.
     * <p>
     * This filter will only execute once per request, to resolve servlet container
     * (specifically Weblogic) incompatibilities.
     * <p>
     * This filter MUST be executed BEFORE any authentication processing mechanisms.
     * Authentication processing mechanisms (e.g. BASIC, CAS processing filters etc) expect
     * the <code>SecurityContextHolder</code> to contain a valid <code>SecurityContext</code>
     * by the time they execute.
     * <p>
     * This is essentially a refactoring of the old
     * <tt>HttpSessionContextIntegrationFilter</tt> to delegate the storage issues to a
     * separate strategy, allowing for more customization in the way the security context is
     * maintained between requests.
     * <p>
     * The <tt>forceEagerSessionCreation</tt> property can be used to ensure that a session is
     * always available before the filter chain executes (the default is <code>false</code>,
     * as this is resource intensive and not recommended).
     *
     * @author Luke Taylor
     * @since 3.0
     */
    public class SecurityContextPersistenceFilter extends GenericFilterBean {
    
        static final String FILTER_APPLIED = "__spring_security_scpf_applied";
    
        private SecurityContextRepository repo;
    
        private boolean forceEagerSessionCreation = false;
    
        public SecurityContextPersistenceFilter() {
            this(new HttpSessionSecurityContextRepository());
        }
    
        public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
            this.repo = repo;
        }
    
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
    
            if (request.getAttribute(FILTER_APPLIED) != null) {
                // ensure that filter is only applied once per request
                chain.doFilter(request, response);
                return;
            }
    
            final boolean debug = logger.isDebugEnabled();
    
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
    
            if (forceEagerSessionCreation) {
                HttpSession session = request.getSession();
    
                if (debug && session.isNew()) {
                    logger.debug("Eagerly created session: " + session.getId());
                }
            }
    
            HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                    response);
            SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
    
            try {
                SecurityContextHolder.setContext(contextBeforeChainExecution);
    
                chain.doFilter(holder.getRequest(), holder.getResponse());
    
            }
            finally {
                SecurityContext contextAfterChainExecution = SecurityContextHolder
                        .getContext();
                // Crucial removal of SecurityContextHolder contents - do this before anything
                // else.
                SecurityContextHolder.clearContext();
                repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                        holder.getResponse());
                request.removeAttribute(FILTER_APPLIED);
    
                if (debug) {
                    logger.debug("SecurityContextHolder now cleared, as request processing completed");
                }
            }
        }
    
        public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
            this.forceEagerSessionCreation = forceEagerSessionCreation;
        }
    }
    

    SecurityContextPersistenceFilter是承接容器的session与spring security的重要filter,主要工作是从session中获取SecurityContext,然后放到上下文中,之后的filter大多依赖这个来获取登录态。其主要是通过HttpSessionSecurityContextRepository来存取的。

    HttpSessionSecurityContextRepository

    spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java

    /**
     * A {@code SecurityContextRepository} implementation which stores the security context in
     * the {@code HttpSession} between requests.
     * <p>
     * The {@code HttpSession} will be queried to retrieve the {@code SecurityContext} in the
     * <tt>loadContext</tt> method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY} by
     * default). If a valid {@code SecurityContext} cannot be obtained from the
     * {@code HttpSession} for whatever reason, a fresh {@code SecurityContext} will be
     * created by calling by {@link SecurityContextHolder#createEmptyContext()} and this
     * instance will be returned instead.
     * <p>
     * When <tt>saveContext</tt> is called, the context will be stored under the same key,
     * provided
     * <ol>
     * <li>The value has changed</li>
     * <li>The configured <tt>AuthenticationTrustResolver</tt> does not report that the
     * contents represent an anonymous user</li>
     * </ol>
     * <p>
     * With the standard configuration, no {@code HttpSession} will be created during
     * <tt>loadContext</tt> if one does not already exist. When <tt>saveContext</tt> is called
     * at the end of the web request, and no session exists, a new {@code HttpSession} will
     * <b>only</b> be created if the supplied {@code SecurityContext} is not equal to an empty
     * {@code SecurityContext} instance. This avoids needless <code>HttpSession</code>
     * creation, but automates the storage of changes made to the context during the request.
     * Note that if {@link SecurityContextPersistenceFilter} is configured to eagerly create
     * sessions, then the session-minimisation logic applied here will not make any
     * difference. If you are using eager session creation, then you should ensure that the
     * <tt>allowSessionCreation</tt> property of this class is set to <tt>true</tt> (the
     * default).
     * <p>
     * If for whatever reason no {@code HttpSession} should <b>ever</b> be created (for
     * example, if Basic authentication is being used or similar clients that will never
     * present the same {@code jsessionid}), then {@link #setAllowSessionCreation(boolean)
     * allowSessionCreation} should be set to <code>false</code>. Only do this if you really
     * need to conserve server memory and ensure all classes using the
     * {@code SecurityContextHolder} are designed to have no persistence of the
     * {@code SecurityContext} between web requests.
     *
     * @author Luke Taylor
     * @since 3.0
     */
    public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
        /**
         * The default key under which the security context will be stored in the session.
         */
        public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
    
        /**
         * Gets the security context for the current request (if available) and returns it.
         * <p>
         * If the session is null, the context object is null or the context object stored in
         * the session is not an instance of {@code SecurityContext}, a new context object
         * will be generated and returned.
         */
        public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
            HttpServletRequest request = requestResponseHolder.getRequest();
            HttpServletResponse response = requestResponseHolder.getResponse();
            HttpSession httpSession = request.getSession(false);
    
            SecurityContext context = readSecurityContextFromSession(httpSession);
    
            if (context == null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("No SecurityContext was available from the HttpSession: "
                            + httpSession + ". " + "A new one will be created.");
                }
                context = generateNewContext();
    
            }
    
            SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
                    response, request, httpSession != null, context);
            requestResponseHolder.setResponse(wrappedResponse);
    
            if (isServlet3) {
                requestResponseHolder.setRequest(new Servlet3SaveToSessionRequestWrapper(
                        request, wrappedResponse));
            }
    
            return context;
        }
        //......
    }
    

    readSecurityContextFromSession

    /**
         *
         * @param httpSession the session obtained from the request.
         */
        private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
            final boolean debug = logger.isDebugEnabled();
    
            if (httpSession == null) {
                if (debug) {
                    logger.debug("No HttpSession currently exists");
                }
    
                return null;
            }
    
            // Session exists, so try to obtain a context from it.
    
            Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
    
            if (contextFromSession == null) {
                if (debug) {
                    logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT");
                }
    
                return null;
            }
    
            // We now have the security context object from the session.
            if (!(contextFromSession instanceof SecurityContext)) {
                if (logger.isWarnEnabled()) {
                    logger.warn(springSecurityContextKey
                            + " did not contain a SecurityContext but contained: '"
                            + contextFromSession
                            + "'; are you improperly modifying the HttpSession directly "
                            + "(you should always use SecurityContextHolder) or using the HttpSession attribute "
                            + "reserved for this class?");
                }
    
                return null;
            }
    
            if (debug) {
                logger.debug("Obtained a valid SecurityContext from "
                        + springSecurityContextKey + ": '" + contextFromSession + "'");
            }
    
            // Everything OK. The only non-null return from this method.
    
            return (SecurityContext) contextFromSession;
        }
    

    可以看到是从session中取出名为SPRING_SECURITY_CONTEXT的attribute

    取出来是SecurityContextImpl,实例如下

    org.springframework.security.core.context.SecurityContextImpl@4198818b: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@4198818b: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@380f4: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 5B9AFCC0537B1202116A23CEC6B41142; Granted Authorities: ROLE_USER
    

    可以看到,如果是之前登录过的,则session会关联上登录用户信息,包含用户的AuthenticationT信息,比如Principal,Granted Authorities等,这些都是重要的鉴权依据。

    generateNewContext

    如果取出来没有的话,则new一个

    /**
         * By default, calls {@link SecurityContextHolder#createEmptyContext()} to obtain a
         * new context (there should be no context present in the holder when this method is
         * called). Using this approach the context creation strategy is decided by the
         * {@link SecurityContextHolderStrategy} in use. The default implementations will
         * return a new <tt>SecurityContextImpl</tt>.
         *
         * @return a new SecurityContext instance. Never null.
         */
        protected SecurityContext generateNewContext() {
            return SecurityContextHolder.createEmptyContext();
        }
    

    通常这种情况是用户没有登陆,那么通常会经过AnonymousAuthenticationFilter(spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/AnonymousAuthenticationFilter.java),其Authentication的值就是AnonymousAuthenticationToken

    登录态如何写入session

    spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java

    /**
         * Default behaviour for successful authentication.
         * <ol>
         * <li>Sets the successful <tt>Authentication</tt> object on the
         * {@link SecurityContextHolder}</li>
         * <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
         * <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
         * <tt>ApplicationEventPublisher</tt></li>
         * <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
         * </ol>
         *
         * Subclasses can override this method to continue the {@link FilterChain} after
         * successful authentication.
         * @param request
         * @param response
         * @param chain
         * @param authResult the object returned from the <tt>attemptAuthentication</tt>
         * method.
         * @throws IOException
         * @throws ServletException
         */
        protected void successfulAuthentication(HttpServletRequest request,
                HttpServletResponse response, FilterChain chain, Authentication authResult)
                throws IOException, ServletException {
    
            if (logger.isDebugEnabled()) {
                logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                        + authResult);
            }
    
            SecurityContextHolder.getContext().setAuthentication(authResult);
    
            rememberMeServices.loginSuccess(request, response, authResult);
    
            // Fire event
            if (this.eventPublisher != null) {
                eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                        authResult, this.getClass()));
            }
    
            successHandler.onAuthenticationSuccess(request, response, authResult);
        }
    

    鉴权成功之后,会将authentication写入到SecurityContextHolder的context中。

    SecurityContextPersistenceFilter#saveContext

    spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/context/SecurityContextPersistenceFilter.java

    try {
                SecurityContextHolder.setContext(contextBeforeChainExecution);
    
                chain.doFilter(holder.getRequest(), holder.getResponse());
    
            }
            finally {
                SecurityContext contextAfterChainExecution = SecurityContextHolder
                        .getContext();
                // Crucial removal of SecurityContextHolder contents - do this before anything
                // else.
                SecurityContextHolder.clearContext();
                repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                        holder.getResponse());
                request.removeAttribute(FILTER_APPLIED);
    
                if (debug) {
                    logger.debug("SecurityContextHolder now cleared, as request processing completed");
                }
            }
    

    可以看到SecurityContextPersistenceFilter在filter执行完之后,finally的时候,调用了saveContext来保存context的东西到session中。

    spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java

    /**
             * Stores the supplied security context in the session (if available) and if it
             * has changed since it was set at the start of the request. If the
             * AuthenticationTrustResolver identifies the current user as anonymous, then the
             * context will not be stored.
             *
             * @param context the context object obtained from the SecurityContextHolder after
             * the request has been processed by the filter chain.
             * SecurityContextHolder.getContext() cannot be used to obtain the context as it
             * has already been cleared by the time this method is called.
             *
             */
            @Override
            protected void saveContext(SecurityContext context) {
                final Authentication authentication = context.getAuthentication();
                HttpSession httpSession = request.getSession(false);
    
                // See SEC-776
                if (authentication == null || trustResolver.isAnonymous(authentication)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
                    }
    
                    if (httpSession != null && authBeforeExecution != null) {
                        // SEC-1587 A non-anonymous context may still be in the session
                        // SEC-1735 remove if the contextBeforeExecution was not anonymous
                        httpSession.removeAttribute(springSecurityContextKey);
                    }
                    return;
                }
    
                if (httpSession == null) {
                    httpSession = createNewSessionIfAllowed(context);
                }
    
                // If HttpSession exists, store current SecurityContext but only if it has
                // actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
                if (httpSession != null) {
                    // We may have a new session, so check also whether the context attribute
                    // is set SEC-1561
                    if (contextChanged(context)
                            || httpSession.getAttribute(springSecurityContextKey) == null) {
                        httpSession.setAttribute(springSecurityContextKey, context);
    
                        if (logger.isDebugEnabled()) {
                            logger.debug("SecurityContext '" + context
                                    + "' stored to HttpSession: '" + httpSession);
                        }
                    }
                }
            }
    

    可以看到这里把context设置进入session当中,这样就关联起来了。

    小结

    可以看到SecurityContextPersistenceFilter通过往session存取名为SPRING_SECURITY_CONTEXT,值为SecurityContext的attribute,来为后续filter建立所需的上下文,包括登录态。其中:

    • before filter取出或创建新的context
    • AbstractAuthenticationProcessingFilter鉴权完,见authentication写入context
    • after filter保存context到session

    因此它的filter顺序排在前面就不足为奇了。

    相关文章

      网友评论

        本文标题:聊聊SecurityContextPersistenceFilt

        本文链接:https://www.haomeiwen.com/subject/jtpywxtx.html