Spring Security

自动配置

注释查找文件的原理

在启动类上有注解@SpringBootApplication,
该注解内嵌了两个值得注意的注解
在这里插入图片描述

一是@SpringBootConfiguration,这个注解标识该类是@Configuration,这样后期会将启动类转化为bean
在这里插入图片描述

二是@EnableAutoConfiguration,这个注解内嵌了@Import(AutoConfigurationImportSelector.class),后继会在容器中自动调AutoConfigurationImportSelector.selectImports(AnnotationMetadata),
在这里插入图片描述

selectImports这个方法根据以下路径执行SpringBoot的SPI机制
AutoConfigurationImportSelector.getAutoConfigurationEntry()
->AutoConfigurationImportSelector.getCandidateConfigurations()
->ImportCandidates.load(AutoConfiguration.class,getBeanClassLoader())

最终指定目录匹配名和文件匹配名
在这里插入图片描述

文件位置与内容

内部路径为:
/META/INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Spring Security提供的Configuration路径就放在spring-boot-autoconfigurer的jar包中。事实上,这三个类也不只是security提供的,只要存在spring-boot-autoconfigure,或者它的上级spring-boot-starter以及spring-boot-starter-web,那就已经提供了这三个类,如果我们没有注入spring-boot-starter-security的依赖,那么spring-boot会通过@ConditionalOnClass完成配置类的停止解析

在该文件中,security指定了三个Configuration

org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration

初始化Configuration

三个自动配置类的规划

SecurityAutoConfiguration是主类,它起了初始化校验通知器和拦截器的作用。
UserDetailsServiceAutoConfiguration是实现了UserDetailsService,这个接口是拦截器鉴权的关键方法。
SecurityFilterAutoConfiguration用于联通容器和servlet的过滤链。它完成了DelegatingFilterProxy的初始化,并将这个Proxy插入了servlet的过滤链。然后创建了DelegatingFilterProxyRegistrationBean便于spring容器管理。

SecurityAutoConfiguration

这个类对于三部分做了初始化。
第一部分是初始化DefaultAuthenticationEventPublisher,如果不存在AuthenticationEventPublisher接口的实现Bean,那么就会完成默认的初始化。

除此之外,该类通过@Import({ SpringBootWebSecurityConfiguration.class, SecurityDataConfiguration.class })同时对于两个类进行初始化,其中SecurityDataConfiguration用于安全评估,非核心类,所以继续看向SpringBootWebSecurityConfiguration。

第二部分是SecurityFilterChain的创建,@ConditionalOnDefaultWebSecurity保证在SecurityFilterChain和HttpSecurity类都存在,如果不存在SecurityFilterChain的bean,那么会创建一个。

第三部分是@EnableSecurity注解的识别和解析。只有在名为springSecurityFilterChain的bean不存在的时候,这个注解才会正常解析。该注解主要做两件事:
一,创建HttpSecurity,HttpSecurity是一个用于配置和创建SecurityFilterChain的builder类,这个bean也被上面第二部分用于完成SecurityFilterChain的bean的创建。
二,完成名为springSecurityFilterChain的bean的创建。这里需要引入一个新的概念,WebSecurity,这个类与HttpSecurity恰好是一对多,它对应的就是Servlet中的filter。当调用WebSecurity的build时候,它会根据所有SecurityFilterChain最终整合创建出一个Filter。这样就实现了一个Filter根据不同路由映射到不同SecurityFilterChain,完成security的路由区分过滤了。

SecurityFilterAutoConfiguration

这个类有两点值得注意:
一,
@AutoConfiguration(after = SecurityAutoConfiguration.class),这个注解保证了它的所有bean生成不会影响SecurityAutoConfiguration的bean的生成。

二,
生成了一个DelegatingFilterProxyRegistrationBean类型的bean,并设置targetBeanName为"springSecurityFilterChain"

DelegatingFilterProxyRegistrationBean实现了ServletContextInitializer的接口中的onStartUp方法,此方法会在Web启动时被调用,它生成了一个DelegatingFilterProxy,并将其加入了过滤器,让我们看一下最本质的代理伪代码

Filter filter = new DelegatingFilterProxy(targetBeanName, this.getWebApplicationContext()) {
      protected void initFilterBean() throws ServletException {
      }
    }
servletContext.addFilter(this.getOrDeduceName(filter), filter)

需要注意的是this.getOrDeduceName(filter)和"springSecurityFilterChain"已经毫无关系了,这是DelegatingFilterProxyRegistrationBean这个bean自身的称呼名字。

DelegatingFilterProxy与springSecurityFilterChain

DelegatingFilterProxy是Filter的实现类,它内部有一个beanName属性。它的过滤方法就是,通过beanName在ApplicationContext中查询名为"springSecurityFilterChain"的bean,将其拿出来,调用其过滤方法。

在SecurityAutoConfiguration中,我们说过了,WebSecurity最终build创建出来的是一个Filter,实际上,这个Filter就是DelegatingFilterProxy,也就是说"springSecurityFilterChain"实际上就是一个DelegatingFilterProxy的bean的名字。

让我们看一下DelegatingFilterProxy的伪代码实现

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    Filter delegateToUse = this.delegate;
    // 如果不存在,同步初始化
    if (delegateToUse == null) {
      synchronized(this.delegateMonitor) {
        delegateToUse = this.delegate;
        if (delegateToUse == null) {
          WebApplicationContext wac = this.findWebApplicationContext();
          if (wac == null) {
            throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
          }
          delegateToUse = (Filter)wac.getBean(targetBeanName, Filter.class);
        }
        this.delegate = delegateToUse;
      }
    }
    // 如果存在,触发代理
    delegate.doFilter(request, response, filterChain);
  }

全景展示

最后,让我们看看我们的猜测是否正确

我们都知道org.apache.catalina.core.ApplicationFilterChain负责filter.doFilter以及最终servlet的service方法的调用。所以让我们看看目前已有的filters,我们发现了一个name为springSecurityFilterChain,类型为org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean的过滤器。
在这里插入图片描述
点开这个过滤器,我们发现它确实delegate了一个FilterChainProxy,并且FilterChainProxy是一个过滤器链列表,列表中有很多过滤器链待匹配,如果requestMatcher通过匹配,就可以执行内部filters了。
在这里插入图片描述

鉴权篇

我们在上一章中对于security的自动配置进行了一定的剖析,但是,如果我们需要更改这些默认配置,需要怎么做呢,为什么这么做呢,这就是本章关注点

chain的配置

配置方法一

从前面我们得知,配置的本质就是更改HttpSecurity的配置,然后调用它的build方法,生成一条自定义的SecurityFilterChain。

在WebSecurity中,webSecurityConfigurers会通过@Autowired的set方法注入。set方法中,会负责找寻容器中所有WebSecurityConfigurer,所以方法一就是配置一个新的Configurer。在security中存在这WebSecurityConfigurerAdapter这个类,它实现了webSecurityConfigurer。

在WebSecurity的build被触发的时候,它会触发所有包含WebSecurityConfigurerAdapter在内所有Configurer的init方法

以下是WebSecurityConfigurerAdapter的伪代码

	public void init(WebSecurity web) throws Exception {
		HttpSecurity http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
		configure(this.http);
		web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
			FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
			web.securityInterceptor(securityInterceptor);
		});
	}

它会完成创建HttpSecurity,设置HttpSecurity,以及将其放入webSecurity。而设置HttpSecurity的configure(this.http)恰好是个protected方法,我们可以直接继承WebSecurityConfigurerAdapter,重写configure(this.http)来完成httpSecurity的配置

配置方法二

WebSecurityConfiguration会通过@Autowired的set方法注入securityFilterChains,按我们之前所说,spring-boot会自动完成WebSecurityConfiguration的装配,并且获得securityFilterChains,然后调用webSecurity.build

所以我们可以直接通过@Bean完成securityFilterChain的创建。

被遗忘的自动配置-UserDetailsServiceAutoConfiguration

为什么一开始说有三个基础AutoConfiguration,但是却只说了两个呢,这是因为UserDetailsServiceAutoConfiguration与基础装配关系不大。但是它在鉴权设置中却起着最关键的作用。

UserDetailsServiceAutoConfiguration默认提供了一种简单的仅存在内存的UserDetailsService,对于这个UserService我们不需要知道它任何实现,它自动完成账号密码生成和校验,使得spring security启动就可以hello world。关键在于,我们从中知道了,如果想要自定义修改校验规则,只需要重写UserService。

UserDetailsService有唯一的一个抽象方法loadUserByUsername,在loadUserByUsername我们最好实现以下任务:
1 根据参数查库获得用户
2 判断用户状态
3 封装用户返回以便后续操作
最后,不要忘了注释@Service

UserDetailsService与Authentication

Authentication的意思就是身份验证,在于判断这个人是否存在。
这恰好就是UserDetailsService擅长做到的,它会通过"Username"完成User表的查询,从而获得一条User数据。

在Security中提供了AuthenticationManager这个接口,其中authenticate方法用于完成UserDetailService的loadUserByUsername调用,而后可以将AuthenticationManager放入AuthenticationProvider,有Provider完成此方法的调用

如果想要弄清楚AuthenticationManager的建造,就需要知道一个建造类AbstractConfiguredSecurityBuilder。这个建造类用于建造Configurer,无论是Authentication用的用于生成AuthenticationProvider的Configurer,还是上文提到的用于生成新的securityFilterChain的名为WebSecurityConfigurerAdapter的Configurer,其最终都是使用了这个类的build,所以看一下build的流程是很重要的,以下是核心build源码:

  protected final O doBuild() throws Exception {
    synchronized(this.configurers) {
      this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
      this.beforeInit();
      this.init();
      this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
      this.beforeConfigure();
      this.configure();
      this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
      O result = this.performBuild();
      this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
      return result;
    }
  }

让我们浅浅地以WebSecurityConfigurerAdapter的建造为例,并假设该Adapter重写了此方法

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(myUserDetailsService);
  }

在build方法中会先执行init(),init会调用它拥有的所有configurers的init方法,然后执行build方法的configure(),其中调用了它拥有的configurers的configure方法。
而在init中有一个方法getHttp(),这个方法会调用authenticationManager(),方法中先调用configure(AuthenticationManagerBuilder auth),我们需要重写这个方法,并将重写的UserService作为参数放入DaoAuthenticationConfigurer,这个DaoAuthenticationConfigurer最终会加入AuthenticationManagerBuilder。完成了bldr初始化,接下来再执行this.localConfigureAuthenticationBldr.build(),这个localConfigureAuthenticationBldr本质上也是一个AbstractConfiguredSecurityBuilder,所以又递归回来了。先执行这个bldr的init,在bldr的configure执行中,还会将bldr自己的AuthenticationProvider放入当前builder,然后最后执行当前builder的build,生成一个包裹AuthenticationProvider的ProviderManager。

在getHttp()的最后还调用了configure(HttpSecurity http),我们可以通过重写该方法完成路由规则配置。

从中我们看出,ProviderManager用于容纳和使用AuthenticationProvider,而AuthenticationManager和AuthenticationProvider相互引用,所以AuthenticationProvider一定是AuthenticationManager的调用者,实际上AuthenticationProvider中有着两个方法,supports和authenticate,前者用于ProviderManager判断当前AuthenticationProvider是否需要进行验证,只有指定实现的Authentication才会进入验证,后者用于当前AuthenticationProvider的正式判断-即对AuthenticationManager的调用。

public interface AuthenticationProvider {
  Authentication authenticate(Authentication authentication) throws AuthenticationException;
  boolean supports(Class<?> authentication);
}

真实配置示例

我们在WebSecurityConfigurerAdapter的configure(HttpSecurity http)中设立通过许可

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.requestMatchers()
        .antMatchers("/api/**", "/iframe/**", "/oauth/**")
        .and()
        .csrf().disable()
        .authorizeRequests()
        .antMatchers("/api/login**").permitAll()
        .anyRequest().authenticated()
        .and()
        .httpBasic()
        .authenticationEntryPoint(new CustomizedAuthenticationEntryPoint())
        .and()
        .logout()
        .logoutUrl("/api/logout")
        .addLogoutHandler(new CustomizedLogoutHandler())
        .logoutSuccessHandler(
            (request, response, authentication) -> request
                .getRequestDispatcher(SecurityHolder.LOGOUT_SUCCESS_PATH)
                .forward(request, response))
        .and()
        .sessionManagement()
        .sessionFixation()
        .none()
        .and()
        // 注册登录过滤器.必须在SecurityContextPersistenceFilter之后完成登录,
        // 否则会被该过滤器覆盖SecurityContext导致登录失败。
        .addFilterAfter(new AFilter(configManager, authenticationService),
            BasicAuthenticationFilter.class)
        .addFilterAfter(new BFilter(configManager, authenticationService),
            AFilter.class)
        .addFilterAfter(new CFilter(), BFilter.class);
  }

如何鉴权

AccessDecisionManager=ProviderManager有三种实现
AccessDecisionVoter =AuthenticationManager具体鉴权

ConfigAttribute 是校验规则必须支持,supports(Class<?> clazz)是需要校验的类型有些AccessDecisionManager可以不支持

public interface AccessDecisionVoter<S> {
  int ACCESS_GRANTED = 1;
  int ACCESS_ABSTAIN = 0;
  int ACCESS_DENIED = -1;

  boolean supports(ConfigAttribute attribute);

  boolean supports(Class<?> clazz);

  int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}

SecurityContextHolder

SecurityContextHolder的配置

SecurityContext用于承载授权上下文,根据SecurityContextHolder的strategy属性,SecurityContext可以分为四类:
当前线程,最普通的情况,让本次请求执行响应都拥有着权限信息
父子线程,如果存在异步时可能有用
全局拥有,只有在全局只有一个账号的时候才适用
自定义规则,根据自身定制化,在配置中为spring.security.strategy设置一个新的值,SecurityContextHolder会在初始化时全局寻找类

事实上SecurityContext可以被视为一个和Security无关的三方工具,它在ThreadLocal和InheritedThreadLocal上进行了封装,从而实现了不同类型的SecurityContext,然后对外通过SecurityContextHolder暴露。所以我们在正常使用过程中,可以在chain的任意filter中随意操作SecurityContextHolder,只要保证最终的结果和需要的一致就行。

SecurityContextHolder的应用

SecurityContext其实是对Authentication完成操作的类,Authentication中有着三个参数:
principal,对应着唯一标识,比如邮箱,用户名,用户Id
credential,对应着密码,
Authorities,对应着当前人的角色数组

在filter中,我们可以先通过filter调用UserDetailsService的loadUserByUsername,然后完成调用SecurityContext的set方法装入整体Authentication便于后续操作。

Authentication自动注入分析

在Controller中,我们往往这样写代码

  @RequestMapping(path = "abc", method = RequestMethod.POST)
  @PreAuthorize("hasPermission('abc','POST')")
  public Object createDefault(@Validated(value = {DefaultValidate.class,
      PostValidate.class}) @RequestBody NeedBody body,
      BindingResult result, Authentication authentication) throws Exception

而这个Authentication是谁帮忙注入的呢
首先我们在DispatcherServlet当中通过request在handlerMappings中找到合适的handler。通过Handler我们在HandlerAdapters中找到合适的HandlerAdapter。然后使用HandlerAdapter来调用handler,处理request。

在关键方法InvocableHandlerMethod.invokeForRequest中会通过支持的resolver来完成参数的获取和注入。而Authentication是被ServletRequestMethodArgumentResolver完成解析的
以下给出resolver解析的伪代码实现

if (Principal.class.isAssignableFrom(paramType)) {
	Principal userPrincipal = SecurityContextHolder.getContext().getAuthentication();
	if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
		throw new IllegalStateException(
				"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
	}
	return userPrincipal;
}

事实上,Authentication是Principal的继承接口,所以会进入该方法。

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值