Spring Security最难的地方就是这个了

Posted 码农小胖哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security最难的地方就是这个了相关的知识,希望对你有一定的参考价值。

本篇摘自胖哥最新的基于Spring Security 5.6.x的《Spring Security干货》教程。旧版的教程将在2022年1月1日下线,请需要的同学尽快通过本公众号回复“2021开工福利”下载。原创不易,请多多关注、点赞、转发。

Spring Security最难的地方就是HttpSecurity的顶层设计。不信你看看HttpSecurity的定义。

public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
        implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> 
    // 省略

感觉不到的话,再给你看看UML图:

为什么要这么复杂?我第一次看到HttpSecurity的结构时我怀疑我自己是不是Java开发。多年以后,当我深入学习了之后才理解了这种设计。作为一个框架,尤其是安全框架,配置必须足够灵活才能适用于更多的业务场景。Spring Security采取了配置与构建分离的架构设计来保证这一点。

配置与构建分离

配置只需要去收集配置项,构建只需要把所有的配置构建成目标对象。各干各的,分离职责,这种做法能够提高代码的可维护性和可读写性。Spring Security利用接口隔离把配置和构建进行高度抽象,提高灵活度,降低复杂度。不过这个体系依然非常庞大。为了降低学习难度需要把大问题拆解成小问题,各个击破,这种学习方法在学习一些复杂的抽象理论时很凑效。

SecurityBuilder

SecurityBuilder就是对构建的抽象。你看上面的类图过于复杂,而看SecurityBuilder就非常的简单了。

public interface SecurityBuilder<O> 
    // 构建
 O build() throws Exception;

就一个动作,构建泛化的目标对象O。通过下面这一组抽象和具体的定义我想你应该明白SecurityBuilder了吧。

// 抽象
 SecurityBuilder -> O
 // 具体
 HttpSecurity->DefaultSecurityFilterChain

一句话,构建的活都是我来干。

AbstractSecurityBuilder

AbstractSecurityBuilder是对SecurityBuilder的实现。源码如下:

public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> 

 private AtomicBoolean building = new AtomicBoolean();

 private O object;

 @Override
 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");
 
     // 获取构建目标对象
 public final O getObject() 
  if (!this.building.get()) 
   throw new IllegalStateException("This object has not been built");
  
  return this.object;
 

 /**
  *  钩子方法
  */
 protected abstract O doBuild() throws Exception;

它通过原子类AtomicBoolean对构建方法build()进行了调用限制:每个目标对象只能被构建一次,避免安全策略发生不一致的情况。构建方法还加了final关键字,不可覆写!构建的核心逻辑通过预留的钩子方法doBuild()来扩展,钩子方法是很常见的一种继承策略。另外AbstractSecurityBuilder还提供了获取已构建目标对象的方法getObject

一句话,构建的活我只干一次。

HttpSecurityBuilder

public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
  extends SecurityBuilder<DefaultSecurityFilterChain> 

     // 根据类名获取配置  
 <C extends SecurityConfigurer<DefaultSecurityFilterChain, H>> C getConfigurer(Class<C> clazz);
    // 根据类名移除配置 
 <C extends SecurityConfigurer<DefaultSecurityFilterChain, H>> C removeConfigurer(Class<C> clazz);
    // 把某个对象设置为共享,以便于在多个SecurityConfigurer中使用
 <C> void setSharedObject(Class<C> sharedType, C object);
    // 获取某个共享对象
 <C> C getSharedObject(Class<C> sharedType);
    //  添加额外的 AuthenticationProvider
 H authenticationProvider(AuthenticationProvider authenticationProvider);
    //  添加额外的 UserDetailsService
 H userDetailsService(UserDetailsService userDetailsService) throws Exception;
    // 在过滤器链已有的afterFilter类后面注册一个过滤器
 H addFilterAfter(Filter filter, Class<? extends Filter> afterFilter);
    // 在过滤器链已有的beforeFilter类前面注册一个过滤器
 H addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter);
    // 在过滤器链注册一个过滤器,该过滤器必须在内置注册表 FilterOrderRegistration 中
 H addFilter(Filter filter);

HttpSecurityBuilderDefaultSecurityFilterChain的构建进行了增强,为其构建器增加了一些额外的获取配置或管理配置的入口,参见上面的注释。补充一点这个接口最大的功能就是打通了构建和配置的关系,可以操作下面要讲的SecurityConfigurer

一句话,我只构建DefaultSecurityFilterChain

SecurityConfigurer

SecurityConfigurer是对配置的抽象。配置只是手段,构建才是目的。因此配置是对构建的配置。

public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> 
   // 构建器初始化需要注入的配置,用来后续的信息共享
 void init(B builder) throws Exception;
   // 其它的一些必要配置  
 void configure(B builder) throws Exception;

SecurityConfigurer有两个方法,都非常重要。一个是init方法,这个方法你可以认为是SecurityBuilder构造函数的逻辑。如果你想在SecurityBuilder初始化的时候执行一些逻辑或者在后续配置中共享一些变量的话就可以在init方法中去实现;第二个方法是configure,为SecurityBuilder配置一些必要的属性。到这里还没完?这两个方法有着明确的先后执行顺序。在一次构建内可能有多个SecurityConfigurer,只有全部的init逐个执行完毕后才会逐个执行configure方法。相关的源码在AbstractConfiguredSecurityBuilder中的标记部分:

@Override
 protected final O doBuild() throws Exception 
  synchronized (this.configurers) 
   this.buildState = BuildState.INITIALIZING;
   beforeInit();
            // ① 执行所有的初始化方法
   init();
   this.buildState = BuildState.CONFIGURING;
   beforeConfigure();
            // ② 执行所有的configure方法
   configure();
   this.buildState = BuildState.BUILDING;
   O result = performBuild();
   this.buildState = BuildState.BUILT;
   return result;
  
 

一句话,配置SecurityBuilder的事都是我来干。

SecurityConfigurerAdapter

SecurityConfigurer在某些场景下是有局限性的,它不能获取正在配置的SecurityBuilder,因此你无法进一步操作SecurityBuilder,配置的扩展性将大打折扣。因此引入了SecurityConfigurerAdapter来扩展SecurityConfigurer

public abstract class SecurityConfigurerAdapter<O, B extends SecurityBuilder<O>> implements SecurityConfigurer<O, B> 

    private B securityBuilder;

    private CompositeObjectPostProcessor objectPostProcessor = new CompositeObjectPostProcessor();

    @Override
    public void init(B builder) throws Exception 
    

    @Override
    public void configure(B builder) throws Exception 
    
   // 获取正在配置的构建器,以暴露构建器的api
    public B and() 
        return getBuilder();
    
 
    protected final B getBuilder() 
        Assert.state(this.securityBuilder != null, "securityBuilder cannot be null");
        return this.securityBuilder;
    
    
    //  用复合对象后置处理器去处理对象,以改变一些对象的特性
    @SuppressWarnings("unchecked")
    protected <T> T postProcess(T object) 
        return (T) this.objectPostProcessor.postProcess(object);
    
    // 添加一个ObjectPostProcessor到符合构建器
    public void addObjectPostProcessor(ObjectPostProcessor<?> objectPostProcessor) 
        this.objectPostProcessor.addObjectPostProcessor(objectPostProcessor);
    
    // 设置 需要配置的构建器,这样可以让多个SecurityConfigurerAdapter去配置一个SecurityBuilder
    public void setBuilder(B builder) 
        this.securityBuilder = builder;
    
    // 其它省略

这样可以指定SecurityBuilder,而且可以把SecurityBuilder暴露出来,随时随地去调整SecurityBuilder,灵活性大大提高。具体说的话,你可以通过and()方法获取SecurityBuilder并对SecurityBuilder的其它配置项进行操作,比如上图中SecurityConfigurerAdapter之间的切换。除此之外还引入了ObjectPostProcessor来后置操作一些并不开放的内置对象。关于ObjectPostProcessor会找个合适的场景去讲解它。

一句话,配置SecurityBuilder不算什么,灵活适配才是花活。

AbstractHttpConfigurer

不是所有的配置都是有用的,有些配置我们希望有个关闭的入口功能。比如csrf功能,控制着csrf的配置的是CsrfConfigurer,如果CsrfConfigurer有一个关闭功能就好了。因此从SecurityConfigurerAdapter衍生出AbstractHttpConfigurer来满足这个需求。

public abstract class AbstractHttpConfigurer<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, B> 
    // 关闭当前配置
    @SuppressWarnings("unchecked")
    public B disable() 
        getBuilder().removeConfigurer(getClass());
        return getBuilder();
    
    //  增强了父类的新增ObjectPostProcessor方法 
    @SuppressWarnings("unchecked")
    public T withObjectPostProcessor(ObjectPostProcessor<?> objectPostProcessor) 
        addObjectPostProcessor(objectPostProcessor);
        return (T) this;
    

AbstractHttpConfigurer的实现类非常多,日常的配置项大都由AbstractHttpConfigurer的实现类来控制。这个类是做定制化配置的一个重要入口之一,如果你想精通Spring Security,这个类一定要掌握。

一句话,我能“杀”我自己。

AbstractConfiguredSecurityBuilder

我们希望有多个SecurityConfigurer配置SecurityBuilder,表单登录的、会话管理、csrf等等。用到什么配置什么,让配置基于策略。因此引入了AbstractConfiguredSecurityBuilder

public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception 
        // 把 objectPostProcessor注入到configurer
  configurer.addObjectPostProcessor(this.objectPostProcessor);
        // 为 SecurityConfigurerAdapter 设置Builder 以便于能够get到   
        // 注意区别于其它SecurityConfigurer
  configurer.setBuilder((B) this);
  add(configurer);
  return configurer;
 
 
 public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception 
  add(configurer);
  return configurer;
 

通过上面两个apply方法就可以把所有的SecurityConfigurer适配进来,然后通过doBuilder进行精细化构建生命周期。你可以在各个生命周期阶段进行一些必要的操作。

一句话,所有的配置都由我来进行适配。

总结

我们把Spring Security整个配置构建体系拆分了来看会简单的多一些。即使这样想理解这个体系也绝非靠一篇两篇文章也是不现实的。不过从中也可以看得出一个道理,如果你的代码想高度灵活,就必须把各个生命周期分层地高度抽象才行。

2021版Spring Security实战干货教程即将下线,2022版即将上线!

2021-12-15

更快的Maven来了

2021-12-23

能进这个Java组织的都是大神,现在只有三个中国人

2021-12-22

如果你是头条用户,一定要加入这个程序员组织

2021-12-22

前瞻|Spring 6.0将停止支持Freemarker和JSP

2021-12-20

以上是关于Spring Security最难的地方就是这个了的主要内容,如果未能解决你的问题,请参考以下文章

Python 最难的问题

Javascript 面向对象编程:封装

Javascript 面向对象编程:封装

Javascript 面向对象编程:封装

用数据告诉你高考最难的省份是哪里!

中国高考难度地图出炉,最难的是你们那里吗?