Spring Security 实现原理的理解记录

前言

最近在弄微信的OAuth认证,我们认证服务及作为客户端又作为认证服务端。因此需要对spring cloud oauth的oauth流程做出相应的变动改造。在研究spring cloud oauth的源码时候,发现对spring security 的认证流程不清不楚,改造起来比较吃力。所以对spring security 的实现原理进行了研究学习。

本文对spring security 的启动流程以及配置源码做了简单分析,希望对大家理解以及以后的使用spring security 有个很好的帮助。


一切从filter开始

spring security 的核心就是filter,通过一层层的filter后,才访问到我们的资源信息。感觉就像打魂斗罗,一个关卡一个关卡的打过去,打完最后一关,OK,结局是啥我不记得了。spring security 的filter做着一层一层的拦截,把相关的权限做一层一层的验证。成功?走下层。失败?认证失败。spring security的所有的权限校验都是这样做的,一切的认证都在filter中,业务代码完全不知情。

URI与Filter的匹配

每个请求的uri就是个路由地址。spring security中,配置的其中一个部分就是 uri与filter对应关系。换句话就是,每个uri在程序启动的时候,就已经确定了他们所对应的filter列表。这样,每个请求过来,都会经过他所有对应的filter列表,来做一层层的认证授权。如下图。指定了三个uri(支持EL表达式)。/login 的请求经过自己的filter列表,/api/* 以及 /userinfo 也是如此。通过这样的方式来达到控制具体uri权限的能力。

严谨点说不止是uri,可以自定义匹配头信息啊或者其他的,http请求中可以用来区分的都可以做为匹配条件。这里为了方便理解。

filter-uri

Spring Security filter配置与原理

我们再根据源码分析下spring security 是如何加载存储filter,以及如何处理uri与filter的映射的。

  1. 指定了@EnableWebSecurity,就会加载 WebSecurityConfiguration 的配置;
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class, ObjectPostProcessorConfiguration.class,
        SpringWebMvcImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
    boolean debug() default false;
}
  • 在WebSecurityConfiguration 中,会实例化 WebSecurity;
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
        ObjectPostProcessor<Object> objectPostProcessor,
        @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
        throws Exception {
    webSecurity = objectPostProcessor
            .postProcess(new WebSecurity(objectPostProcessor));
    if (debugEnabled != null) {
        webSecurity.debug(debugEnabled);
    }

    Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE);

    Integer previousOrder = null;
    Object previousConfig = null;
    for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
        Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
        if (previousOrder != null && previousOrder.equals(order)) {
            throw new IllegalStateException(
                    "@Order on WebSecurityConfigurers must be unique. Order of "
                            + order + " was already used on " + previousConfig + ", so it cannot be used on "
                            + config + " too.");
        }
        previousOrder = order;
        previousConfig = config;
    }
    for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
        webSecurity.apply(webSecurityConfigurer);
    }
    this.webSecurityConfigurers = webSecurityConfigurers;
}
  1. WebSecurity中可以通过addSecurityFilterChainBuilder() 方法,把各种filter的SecurityBuilder添加进来,而每个SecurityBuilder的泛型上界必须是SecurityFilterChain;
public WebSecurity addSecurityFilterChainBuilder(
            SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
    this.securityFilterChainBuilders.add(securityFilterChainBuilder);
    return this;
}
  1. WebSecurity的performBuild() 方法实例化 FilterChainProxy,FilterChainProxy存储着SecurityFilterChain列表;
@Override
protected Filter performBuild() throws Exception {
    Assert.state(
            !securityFilterChainBuilders.isEmpty(),
            "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. More advanced users can invoke "
                    + WebSecurity.class.getSimpleName()
                    + ".addSecurityFilterChainBuilder directly");
    int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
    List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>(
            chainSize);
    for (RequestMatcher ignoredRequest : ignoredRequests) {
        securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
    }
    for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
        securityFilterChains.add(securityFilterChainBuilder.build());
    }
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
    if (httpFirewall != null) {
        filterChainProxy.setFirewall(httpFirewall);
    }
    filterChainProxy.afterPropertiesSet();

    Filter result = filterChainProxy;
    if (debugEnabled) {
        logger.warn("\n\n"
                + "********************************************************************\n"
                + "**********        Security debugging is enabled.       *************\n"
                + "**********    This may include sensitive information.  *************\n"
                + "**********      Do not use in a production system!     *************\n"
                + "********************************************************************\n\n");
        result = new DebugFilter(filterChainProxy);
    }
    postBuildAction.run();
    return result;
}
  1. WebSecurity的performBuild()方法中将securityFilterChainBuilders列表里面的每个SecurityBuilder对象构建SecurityFilterChain实例,将SecurityFilterChain列表作为参数构建FilterChainProxy。

SecurityFilterChain接口定义了两个方法,一个是存储filter,一个是uri规则匹配。

public interface SecurityFilterChain {

    boolean matches(HttpServletRequest request);

    List<Filter> getFilters();
}
  1. 请求到达的时候,FilterChainProxy的dofilter()方法,会遍历所有的SecurityFilterChain,对匹配到的url,则一一调用SecurityFilterChain中的filter做认证授权。FilterChainProxy的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);
    // 获取请求对应的filter列表
    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);
    // 执行每个filter
    vfc.doFilter(fwRequest, fwResponse);
}

// 通过遍历filterChains,调用SecurityFilterChain的matches方法,判断当前的请求对应哪些filter,返回匹配的filter列表
private List<Filter> getFilters(HttpServletRequest request) {
    for (SecurityFilterChain chain : filterChains) {
        if (chain.matches(request)) {
            return chain.getFilters();
        }
    }

    return null;
}

下图是简单的类图,不过结合文字大体上spring security中是可以大体勾勒出uri与filter的对应关系。具体方法在何时调用,后面会讲到。通过这里我们就可以知道了几个关键类拥有哪些能力,最后在启动的时候把他们串起来,整个流程就清晰了。


HttpSecurity

httpSecurity终极作用,就是注册实例化filter

上一部分中,提到WebSecurity中可以通过addSecurityFilterChainBuilder() 方法,把各种filter的SecurityBuilder添加进来,而这里的SecurityBuilder就是HttpSecurity。

我们在使用spring security 的时候,经常的做法就是写个类,继承WebSecurityConfigurerAdapter,并且加上@Configuration注解。比如:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .permitAll();
    }
    
}

在自定义认证的时候,对HttpSecurity 的操作是非常核心的动作。这里的http所调用的方法,最终的结果就产生了我们先去提到的uri-filter 的关系映射。

authorizeRequests(),formLogin()方法分别返回ExpressionUrlAuthorizationConfigurer和FormLoginConfigurer,他们都是SecurityConfigurer接口的实现类,分别代表的是不同类型的安全配置器。而这些安全配置器分别对应一个或多个filter。

  • formLogin对应UsernamePasswordAuthenticationFilter
  • authorizeRequests对应FilterSecurityInterceptor

authorizeRequests(),formLogin()方法源码

public final class HttpSecurity extends
        AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
        implements SecurityBuilder<DefaultSecurityFilterChain>,
        HttpSecurityBuilder<HttpSecurity>{
       ...
    public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()
            throws Exception {
        return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>())
                .getRegistry();
    }
    
    public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
        return getOrApply(new FormLoginConfigurer<HttpSecurity>());
    }
    ...
}

都是调用了getOrApply()方法

private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
            C configurer) throws Exception {
        C existingConfig = (C) getConfigurer(configurer.getClass());
        if (existingConfig != null) {
            return existingConfig;
        }
        return apply(configurer);
    }

再进去 apply(),到了父类AbstractConfiguredSecurityBuilder中

// 存储了Class与configurer的Map
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>>();

public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer)
            throws Exception {
        configurer.addObjectPostProcessor(objectPostProcessor);
        configurer.setBuilder((B) this);
        add(configurer);
        return configurer;
    }

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);
            }
        }
    }

所以最后是将configurer放入一个LinkedHashMap的属性configurers中。每个configurer又对应到filter。
所以我们平常使用 http.xxx 的操作,就是将configurer添加到这个LinkedHashMap中。


接口SecurityBuilder 与 接口SecurityConfigurer

以上提到的都是具体的类,而带入的参数又是接口名,然后搞得云里雾里。WebSecurity,HttpSecurity,UsernamePasswordAuthenticationFilter这些关系怎么感觉还是有点模糊不清。

现在,我们从更高的层面来说,从两个核心接口以及其实现类的类图来理解下。
这两个接口就是SecurityBuilder 与 SecurityConfigurer

public interface SecurityBuilder<O> {

    O build() throws Exception;
}
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
    
    void init(B builder) throws Exception;

    void configure(B builder) throws Exception;
}
SecurityBuilder

SecurityConfigurer

这里描述下:
HttpSecurity是一个securityBuilder,HttpSecuirty内部维护了一个Filter的List集合,我们添加的各种安全配置器对应的Filter最终都会被加入到这个List集合中。
WebSecurity是一个securityBuilder,内部维护着securityBuilder的列表,存储securityBuilder,这里主要是存储HttpSecurity。
很多官方类是XXXConfigurer,这些都是SecurityConfigurer。这些SecurityConfigurer的configure()方法,都会把对应filter添加到HttpSecurity。

层级的抽样,再结合前面两块的内容,应该能对spring security的整体结构,会有个更好的理解。


WebSecurity

WebSecurity 是spring security 中非常重要的一个类!三个重要作用:

  1. 创建Spring Security 过滤链 的 FilterChainProxy
  2. 存储着SecurityBuilder的列表,这里主要是存储各个httpSecurity
  3. 对SecurityConfigurer进行构建

从这三个作用来看,WebSecurity 是spring security启动的核心。构建各种SecurityConfigurer,存在各种SecurityBuilder,创建好SS的过滤链。是不是把SS的核心功能都初始化完成了。。。

WebSecurityConfiguration这个配置会在使用@EnableWebSecurity后导入,WebSecurityConfiguration会初始化WebSecurity ,并且调用WebSecurity 的apply()方法。

WebSecurityConfiguration类中,构建springSecurityFilterChain()方法的时候,调用WebSecurity 的build()方法。

build()方法是在其父类AbstractSecurityBuilder中实现

public final O build() throws Exception {
    if (this.building.compareAndSet(false, true)) {
        this.object = doBuild();
        return this.object;
    }
    throw new AlreadyBuiltException("This object has already been built");
}

我们再看看核心的doBuild()

@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;
    }
}

这个方法非常的重要,SecurityConfigurer的初始化,构建;FilterChainProxy的初始化等等都在这个方法里面完成。这个方法以后有时间我再进一步的分析分析。


总结

上述中更多的描述是SS的启动的流程,以及filter的相关配置存储等等。因为我觉得,filter与uri映射关系成立之后,具体的拦截、认证授权等等,是到了每个filter中。我们把这块流程理通了,需要分析具体的认证原理时候,只需要找到对应的SecurityConfigurer做分析,就可以搞定的。

流程总结

再用文字总结下:

  1. spring security 启动过程中 WebSecurityConfiguration 实例化 WebSecurity 。

WebSecurity 中的 securityFilterChainBuilders 列表存储了各个 SecurityBuilder,这里是HttpSecurity 这个SecurityBuilder。

每个 httpSecurity.and()... 这样的操作就是把对应的 SecurityConfigurer 加入 HttpSecurity 的 configurers 中存储。

  1. 在构建WebSecurity 的时候,会调用WebSecurity 的doBuild(),这个方法是一个核心操作。
  • 2-1. doBuild() 方法中调用init()方法,会将所有的configurers调用init()方法。
  • 2-2. doBuild() 方法中调用 configure() 方法,调用每个 configurer 的 configure() 方法,把自己加入到 对应 httpSecurity 的 filter list中。
  • 2-3. doBuild() 方法中调用performBuild() 方法,构建FilterChainProxy实例,把 uri的匹配与 httpSecurity 做关联,进入了spring security的过滤链
  1. 就形成了 WebSecurity 1:n HttpSecurity HttpSecurity 1:n configurers HttpSecurity 1:n filters

  2. 每个 HttpSecurity 实例都有个 RequestMatcher 用来匹配请求uri,有个 List<Filter> 记录需要经过的 filter,有个 FilterComparator 记录了 filter 的 order

  3. 每个请求到达,FilterChainProxy匹配该请求对应的List<Filter>,进行拦截。

以上

如果有存在理解错误,请及时指出。

我的spring cloud demo的git地址: spring cloud demo

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

推荐阅读更多精彩内容