0. 概述

经过基于注解的Spring Security原理解析分析,Spring Security本身所做的事情就是在Spring容器中注册了一系列的Filter,这些Filters在检测到满足条件的URL请求时,会执行其定义的处理过程; Security本身默认提供了一些Filter来完成其各种功能; 本文主要分析以下问题:

  1. 默认Filter的作用及配置
  2. 默认Filter的配置及生效示例分析

由于水平受限,如有分析不正确处敬指出。

1. 默认Filter分析

1.1 概述

Security默认的Filter入口在HttpSecurity对象中;关于该对象的加载过程,请参考基于注解的Spring Security原理解析;

在HttpSecurity对象中,实际提供的是各默认Filter的配置类,通过配置类来控制对应Filter的各个属性配置;在配置完成将Filter加载到HttpSecurity中的FilterChain中去。

在HttpSecurity中提供了以下默认Filter及其配置类: 

Configurer Filter 功能说明
OpenIDLoginConfigurer OpenIDAuthenticationFilter 处理OpenID授权请求
HeaderWriterFilter HeadersConfigurer 在返回报文头中添加Security相关信息
CorsConfigurer CorsFilter 提供跨域访问配置支持的Filter
SessionManagementConfigurer SessionManagementFilter 会话管理Filter
PortMapperConfigurer 用于在Http及Https请求之间重定向时的端口判定
JeeConfigurer J2eePreAuthenticatedProcessingFilter 添加J2EE预授权处理机制支持
X509Configurer X509AuthenticationFilter 添加X509预授权处理机制支持
RememberMeConfigurer RememberMeAuthenticationFilter 记住用户名及密码功能支持
ExpressionUrlAuthorizationConfigurer FilterSecurityInterceptor Security的主要Filter,通过调用权限管理器等进行Http访问的权限判断
RequestCacheConfigurer RequestCacheAwareFilter 缓存请求并在必要的时候使用缓存的请求
ExceptionHandlingConfigurer ExceptionTranslationFilter 处理AccessDeniedException及AuthenticationException异常
SecurityContextConfigurer SecurityContextPersistenceFilter SecurityContext对象持久化Filter,用于在请求开始阶段初始化并持久化该对象,在后续的Filter中可以使用该对象来获取信息
ServletApiConfigurer SecurityContextHolderAwareRequestFilter 在原始请求基础上包装一些方法供后续调用
CsrfConfigurer CsrfFilter 跨站请求伪造保护Filter;
LogoutConfigurer LogoutFilter 退出登录请求处理Filter
AnonymousConfigurer AnonymousAuthenticationFilter 匿名请求控制Filter
FormLoginConfigurer UsernamePasswordAuthenticationFilter 表单登录请求处理Filter
OAuth2LoginConfigurer OAuth2AuthorizationRequestRedirectFilter OAuth2请求权限控制处理Filter,为其它网站提供本网站Oauth2方式登录,即其它网站通过本网站的账户密码进行登录授权
ChannelSecurityConfigurer ChannelProcessingFilter 通道选择Filter,确保请求是通过正确的通道过来的,如Http或者Https
HttpBasicConfigurer BasicAuthenticationFilter Security基础登录授权Filter,将其结果保存在SecurityContextHolder中

默认的Filter并不是在HttpSecurity对象初始化的时候就全部加载,而是根据用户定制情况进行加载,具体加载情况见后文;

1.2 默认Filter默认配置

在WebSecurityConfigurerAdapter类中,存在默认的configure方法,它会提供一些默认的权限控制配置,默认方法实现哪下:

    protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin().and()
            .httpBasic();
    }

同时,在初始化HttpSecurity的方法init中,也会提供一些默认的配置:

protected final HttpSecurity getHttp() throws Exception {
        if (http != null) {
            return http;
        }

        DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
                .postProcess(new DefaultAuthenticationEventPublisher());
        localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);

        AuthenticationManager authenticationManager = authenticationManager();
        authenticationBuilder.parentAuthenticationManager(authenticationManager);
        Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects();

        http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
                sharedObjects);
        if (!disableDefaults) {
            // @formatter:off
            http
                .csrf().and()
                .addFilter(new WebAsyncManagerIntegrationFilter())
                .exceptionHandling().and()
                .headers().and()
                .sessionManagement().and()
                .securityContext().and()
                .requestCache().and()
                .anonymous().and()
                .servletApi().and()
                .apply(new DefaultLoginPageConfigurer<HttpSecurity>()).and()
                .logout();
            // @formatter:on
            ClassLoader classLoader = this.context.getClassLoader();
            List<AbstractHttpConfigurer> defaultHttpConfigurers =
                    SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

            for(AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
                http.apply(configurer);
            }
        }
        configure(http);
        return http;
    }

通过分析这两个配置处,Security默认加载的Filter清单如下: 

Configurer Filter
CsrfConfigurer CsrfFilter
WebAsyncManagerIntegrationFilter
ExceptionHandlingConfigurer ExceptionTranslationFilter
HeadersConfigurer HeaderWriterFilter
SessionManagementConfigurer SessionManagementFilter
SecurityContextConfigurer SecurityContextPersistenceFilter
RequestCacheConfigurer RequestCacheAwareFilter
AnonymousConfigurer AnonymousAuthenticationFilter
ServletApiConfigurer SecurityContextHolderAwareRequestFilter
DefaultLoginPageConfigurer DefaultLoginPageGeneratingFilter
LogoutConfigurer LogoutFilter
FormLoginConfigurer UsernamePasswordAuthenticationFilter
HttpBasicConfigurer BasicAuthenticationFilter

1.3 默认Filter用户自定义加载

当开发的时候使用@EnableWebSecurity注解加载Security的配置类时,如果该类继承了WebSecurityConfigurerAdapter类,则可以覆盖其configure方法来配置权限控制参数:

@EnableWebSecurity(debug = true)
public class SercurityConfig extends WebSecurityConfigurerAdapter {
   
    @Autowired
    private TestAuthenticationProvider authenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/*", "/home", "/manager/dbManager.html", "/js/*", "/db/*", "/js/**/*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                .permitAll()
                .and()
                .sessionManagement().invalidSessionUrl("/timeout");

        http.headers().frameOptions().sameOrigin().httpStrictTransportSecurity().disable();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }

    @Bean
    public UserDetailsService getUserDetailservice() {
        return new TestUserDetailService();
    }
}

此时即可配置登录链接、退出登录链接、各个URL的访问权限配置等。

1.4 Filter执行顺序分析

Security在初始化的时候会初始化一系列的Filter,这些Filter之间实际上是有先后关系的,其先后关系是如何控制的?
通过前面的分析,Security的Filters是加载到HttpSecurity对象的Filters属性中去的,在HttpSecurity对象Build的时候生成FilterChain对象,此时会将所有添加的Filters添加到FilterChain对象中并返回。其PerformBuild方法实现如下: 

    @Override
    protected DefaultSecurityFilterChain performBuild() throws Exception {
        Collections.sort(filters, comparator);
        return new DefaultSecurityFilterChain(requestMatcher, filters);
    }

可以看到在PerformBuild方法中,第一步是对所有的Fiilters进行排序;其排序规则使用HttpSecurity的属性comparator;我们看下comparator的初始化:

private FilterComparator comparator = new FilterComparator();

再分析下该Comparator的实现:

final class FilterComparator implements Comparator<Filter>, Serializable {
    private static final int STEP = 100;
    private Map<String, Integer> filterToOrder = new HashMap<String, Integer>();

    FilterComparator() {
        int order = 100;
        put(ChannelProcessingFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        put(WebAsyncManagerIntegrationFilter.class, order);
        order += STEP;
        put(SecurityContextPersistenceFilter.class, order);
        order += STEP;
        put(HeaderWriterFilter.class, order);
        order += STEP;
        put(CorsFilter.class, order);
        order += STEP;
        put(CsrfFilter.class, order);
        order += STEP;
        put(LogoutFilter.class, order);
        order += STEP;
        put(X509AuthenticationFilter.class, order);
        order += STEP;
        put(AbstractPreAuthenticatedProcessingFilter.class, order);
        order += STEP;
        filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
                order);
        order += STEP;
        put(UsernamePasswordAuthenticationFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        filterToOrder.put(
                "org.springframework.security.openid.OpenIDAuthenticationFilter", order);
        order += STEP;
        put(DefaultLoginPageGeneratingFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        put(DigestAuthenticationFilter.class, order);
        order += STEP;
        put(BasicAuthenticationFilter.class, order);
        order += STEP;
        put(RequestCacheAwareFilter.class, order);
        order += STEP;
        put(SecurityContextHolderAwareRequestFilter.class, order);
        order += STEP;
        put(JaasApiIntegrationFilter.class, order);
        order += STEP;
        put(RememberMeAuthenticationFilter.class, order);
        order += STEP;
        put(AnonymousAuthenticationFilter.class, order);
        order += STEP;
        put(SessionManagementFilter.class, order);
        order += STEP;
        put(ExceptionTranslationFilter.class, order);
        order += STEP;
        put(FilterSecurityInterceptor.class, order);
        order += STEP;
        put(SwitchUserFilter.class, order);
    }

很明显执行顺序是在这个地方控制的。

2. 默认Filter配置分析举例

2.1 FormLoginConfigurer

2.1.1 概述

表单登录Filter配置类;用于基于表单的权限控制配置;所有配置项均存在默认值,因此所有参数均可使用默认值; 需要特别说明的是,如果没有指定loginPage配置项,Security将会提供一个默认的登录页面。 该配置项配置的实际上是UsernamePasswordAuthenticationFilter这个Filter。

2.1.2 配置详解

如要配置FormLoginConfigurer,在继承自WebSecurityConfigurerAdapter的类的Configure方法中进行:

http.formLogin()
    .loginPage("/login")
    .permitAll();

该配置类所包含的配置项清单及其作用见下表:

配置项 配置项说明
loginPage 登录页面,如果用户未指定,Security将提供默认的登录页面; 默认登录页面请求链接:/login
usernameParameter 用户名属性的名称;默认是username
passwordParameter 密码属性名称,默认是password
failureForwardUrl 授权失败时的跳转链接
failureUrl 登录失败时的跳转链接,默认是/login?error
failureHandler 登录失败后的处理器
successForwardUrl 授权成功时的跳转链接
successHandler 登录成功时的处理器
defaultSuccessUrl 指定如果用户登录前未访问需要授权访问的页面,登录成功后的跳转链接
loginProcessingUrl 登录请求处理链接
authenticationDetailsSource 保存登录请求地址及其SessionID的对象

2.1.3 Filter配置过程分析

上方已经提到过,Configurer是用来配置Filter的各个属性的;实际上最终添加到HttpSecurity对象的Filters属性中去的还是各个Filter;那Configurer与Filter分别是如何发挥作用的?
在此我们将以FormLoginConfigurer为例来分析其生效过程。

先来看下配置加载过程:
FilterConfigurer加载过程

其中HttpSecurity继承自AbstractConfiguredSecurityBuilder类,其配置入口是其方法formLogin;实际最终调用的是其父类的add方法,将FormLoginConfigurer对象添加进去。
add方法源码如下:

private <C extends SecurityConfigurer<O, B>> void add(C configurer) throws Exception {
        Assert.notNull(configurer, "configurer cannot be null");

        Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
                .getClass();
        synchronized (configurers) {
            if (buildState.isConfigured()) {
                throw new IllegalStateException("Cannot apply " + configurer
                        + " to already built object");
            }
            List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
                    .get(clazz) : null;
            if (configs == null) {
                configs = new ArrayList<SecurityConfigurer<O, B>>(1);
            }
            configs.add(configurer);
            this.configurers.put(clazz, configs);
            if (buildState.isInitializing()) {
                this.configurersAddedInInitializing.add(configurer);
            }
        }
    }

该段代码实际所做的事情就是: 将FormLoginConfigurer对象添加到HttpSecurity的configurers属性中去;

基于注解的Spring Security原理解析一文中已经说明,HttpSecurity最终会被加载到WebSecurity的securityFilterChainBuilders属性中去; 添加进去后,在WebSecurity对象的build方法中,会调用HttpSecurity的build方法生成FilterChain对象。
HttpSecurity对象的其Build方法实现如下(AbstractConfiguredSecurityBuilder父类中实现):

@Override
    protected final O doBuild() throws Exception {
        synchronized (configurers) {
            buildState = BuildState.INITIALIZING;

            beforeInit();
            init();

            buildState = BuildState.CONFIGURING;

            beforeConfigure();
            configure();

            buildState = BuildState.BUILDING;

            O result = performBuild();

            buildState = BuildState.BUILT;

            return result;
        }
    }

可以看到在performBuild前会调用configure方法; 跟踪HttpSecurity的该方法,其实现如下: 

    private void configure() throws Exception {
        Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

        for (SecurityConfigurer<O, B> configurer : configurers) {
            configurer.configure((B) this);
        }
    }

可以看到实际调用的是每个FilterConfigurer的configure方法,此处也就是FormLoginConfigurer的configure方法(实现在其父类中):

@Override
    public void configure(B http) throws Exception {
        PortMapper portMapper = http.getSharedObject(PortMapper.class);
        if (portMapper != null) {
            authenticationEntryPoint.setPortMapper(portMapper);
        }

        authFilter.setAuthenticationManager(http
                .getSharedObject(AuthenticationManager.class));
        authFilter.setAuthenticationSuccessHandler(successHandler);
        authFilter.setAuthenticationFailureHandler(failureHandler);
        if (authenticationDetailsSource != null) {
            authFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
        }
        SessionAuthenticationStrategy sessionAuthenticationStrategy = http
                .getSharedObject(SessionAuthenticationStrategy.class);
        if (sessionAuthenticationStrategy != null) {
            authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
        }
        RememberMeServices rememberMeServices = http
                .getSharedObject(RememberMeServices.class);
        if (rememberMeServices != null) {
            authFilter.setRememberMeServices(rememberMeServices);
        }
        F filter = postProcess(authFilter);
        http.addFilter(filter);
    }

该方法是重点了,可以看到执行过程实际就是根据Configurer对象中的属性更新authFilter对象的属性,最后调用HttpSecurity的addFilter方法将生成的Filter添加到了HttpSecurity对象中去。authFilter对象实现类型为: DefaultLoginPageGeneratingFilter。

在Configure方法完成后,HttpSecurity的build方法最终调用的是其performBuild方法,该方法实现如下: 

    @Override
    protected DefaultSecurityFilterChain performBuild() throws Exception {
        Collections.sort(filters, comparator);
        return new DefaultSecurityFilterChain(requestMatcher, filters);
    }

实际上就是组装了一个包含已配置的Filter的一个FilterChain对象。此时
至此,Filter的配置加载过程已经分析完成。

2.1.4 Filter执行过程分析

经过2.1.3中分析,FormLoginConfigurer最终是向HttpSecurity中添加了DefaultLoginPageGeneratingFilter这样一个Filter。其doFilter方法如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        boolean loginError = isErrorPage(request);
        boolean logoutSuccess = isLogoutSuccess(request);
        if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
            String loginPageHtml = generateLoginPageHtml(request, loginError,
                    logoutSuccess);
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.length());
            response.getWriter().write(loginPageHtml);

            return;
        }

        chain.doFilter(request, response);
    }

实际也就是将判断是否是登录、登录失败、退出登录请求,如果是的话则跳转到登录页面;此时将不会再执行后续的Filters了。如果不是上述请求,则继续执行FilterChain中的其它Filters。

参考资料:

  1. http://www.mossle.com/docs/auth/html/ch101-filters.html