Spring Security的过滤器

Spring Security文档的The Security Filter Chain一章指出Spring Security完全基于标准的servlet过滤器,本文结合笔者的看法对该文档进行一些补充说明。

DelegatingFilterProxy类

DelegatingFilterProxy用于代理其他的过滤器,位于spring-web.jar中,这使得Spring可以通过它方便地使用Spring容器管理的过滤器。DelegatingFilterProxy的部分代码如下所示,它继承自GenericFilterBean类,GenericFilterBean的作用与DispatcherServlet的初始化过程这篇文章中介绍的HttpServletBean相似。

public class DelegatingFilterProxy extends GenericFilterBean {
    private String contextAttribute;
    private WebApplicationContext webApplicationContext;
    private String targetBeanName;
    private boolean targetFilterLifecycle = false;
    private volatile Filter delegate;
    private final Object delegateMonitor = new Object();

    // 省略一些代码

    @Override
    protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // If no target bean name specified, use filter name.
                if (this.targetBeanName == null) {
                    this.targetBeanName = getFilterName();
                }
                // Fetch Spring root application context and initialize the delegate early,
                // if possible. If the root application context will be started after this
                // filter proxy, we'll have to resort to lazy initialization.
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // Lazily initialize the delegate if necessary.
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    delegateToUse = initDelegate(wac);
                }
                this.delegate = delegateToUse;
            }
        }

        // Let the delegate perform the actual doFilter operation.
        invokeDelegate(delegateToUse, request, response, filterChain);
    }

    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }
    // 省略一些代码
}
  • 在initFilterBean方法初始化DelegatingFilterProxy对象或者第一次执行doFilter方法时会调用findWebApplicationContext方法查找上下文;
  • targetBeanName表示被代理的过滤器bean的名称;
  • delegate保存被代理的过滤器实例,该实例即是initDelegate方法从上下文中查找的名为targetBeanName的过滤器bean;
  • delegateMonitor是初始化时用的锁。

doFilter方法利用invokeDelegate方法将调用委托给被代理的过滤器执行。

protected void invokeDelegate(
        Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    delegate.doFilter(request, response, filterChain);
}

FilterChainProxy类

文档提到“Spring Security的基础设施只应该委托给一个FilterChainProxy实例,如果在web.xml中配置每个Spring Security的过滤器那就会显得很笨拙”。
FilterChainProxy继承自GenericFilterBean类,可以看成一个过滤器的集合。

public class FilterChainProxy extends GenericFilterBean {
    private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
    private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
            ".APPLIED");
    private List<SecurityFilterChain> filterChains;
    private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
    private HttpFirewall firewall = new DefaultHttpFirewall();

    public FilterChainProxy() {
    }

    public FilterChainProxy(SecurityFilterChain chain) {
        this(Arrays.asList(chain));
    }

    public FilterChainProxy(List<SecurityFilterChain> filterChains) {
        this.filterChains = filterChains;
    }

    @Override
    public void afterPropertiesSet() {
        filterChainValidator.validate(this);
    }

    // 省略一些代码
}
  • filterChains字段可以看成保存了所有与Spring Security相关的过滤器,这些过滤器由SecurityFilterChain组织起来。
    public interface SecurityFilterChain {
    
        boolean matches(HttpServletRequest request);
    
        List<Filter> getFilters();
    }
    

doFilter方法则调用了doFilterInternal方法,该方法及相关代码如下:

private void doFilterInternal(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {

    FirewalledRequest fwRequest = firewall
            .getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse fwResponse = firewall
            .getFirewalledResponse((HttpServletResponse) response);

    List<Filter> filters = getFilters(fwRequest);

    if (filters == null || filters.size() == 0) {
        if (logger.isDebugEnabled()) {
            logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                    + (filters == null ? " has no matching filters"
                            : " has an empty filter list"));
        }

        fwRequest.reset();

        chain.doFilter(fwRequest, fwResponse);

        return;
    }

    VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
    vfc.doFilter(fwRequest, fwResponse);
}

private List<Filter> getFilters(HttpServletRequest request) {
    for (SecurityFilterChain chain : filterChains) {
        if (chain.matches(request)) {
            return chain.getFilters();
        }
    }

    return null;
}
  • getFilters方法根据请求查找第一个匹配的SecurityFilterChain,因此SecurityFilterChain的添加顺序非常重要;
  • VirtualFilterChain依次调用匹配的SecurityFilterChain中的每个过滤器。

Spring Security的过滤器

众所周知,Spring Security的实现基于servlet的过滤器,以下按照在SecurityFilterChain中的排列顺序列出了Spring Security中主要的过滤器:

  • SecurityContextPersistenceFilter;
  • CorsFilter;
  • LogoutFilter;
  • UsernamePasswordAuthenticationFilter、CasAuthenticationFilter和BasicAuthenticationFilter等认证过滤器;
  • RememberMeAuthenticationFilter;
  • AnonymousAuthenticationFilter;
  • ExceptionTranslationFilter;
  • FilterSecurityInterceptor。

更多过滤器可以参见Filter OrderingCore Security Filters,下面简要分析各过滤器的功能。

1. SecurityContextPersistenceFilter

SecurityContextPersistenceFilter从所配置的SecurityContextRepository中获取与该请求关联的SecurityContext,并在请求结束后清除SecurityContextHolder。一般需要将该过滤器配置在其他认证过滤器前,因为认证过滤器如Basic、CAS等要求SecurityContextHolder包含有效的SecurityContext。

2. CorsFilter

CorsFilter根据CORS配置处理预检请求、简单请求和非简单请求,CORS配置由CorsConfiguration类表示。

3. LogoutFilter

若收到注销的请求,LogoutFilter会首先清除认证信息,然后依次执行配置的所有注销处理器LogoutHandler的logout方法,最后执行配置的注销成功处理器LogoutSuccessHandler的onLogoutSuccess方法。

4. UsernamePasswordAuthenticationFilter

若收到登录的请求,UsernamePasswordAuthenticationFilter执行基于表单提交的认证,默认配置下表单的用户名和密码字段分别是username和password,但是可以通过setUsernameParameter和setPasswordParameter进行配置,用户提交的用户名和密码则是通过obtainUsername和obtainPassword方法分别得到。具体的认证过程在该Filter的超类AbstractAuthenticationProcessingFilter中执行。

5. ExceptionTranslationFilter

ExceptionTranslationFilter处理位于它之后的过滤器如FilterSecurityInterceptor抛出的AccessDeniedException或AuthenticationException异常,它只是一个Java异常和HTTP响应之间的桥梁,并不做任何实际的安全认证。

  • 处理AuthenticationException异常:该过滤器会委托给配置的AuthenticationEntryPoint处理;
  • 处理AccessDeniedException异常:如果是匿名用户则委托给配置的AuthenticationEntryPoint处理,否则委托给配置的AccessDeniedHandler。

6. FilterSecurityInterceptor

FilterSecurityInterceptor用来保护HTTP资源,若没有认证则抛出AuthenticationException异常,若权限不足则抛出AccessDeniedException异常,这些异常都会被ExceptionTranslationFilter捕获。

参考文献

https://stackoverflow.com/questions/41480102/how-spring-security-filter-chain-works
跨域资源共享 CORS 详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352