您的当前位置:首页Spring安全架构

Spring安全架构

2024-12-13 来源:哗拓教育

本篇是Spring安全的初级指南,主要介绍Spring安全框架的设计和基本模块。此处仅仅涉及应用安全方面非常基础的知识,但是通过本篇可以扫清使用Spring安全框架是遇到的一些困惑。为了达到此目的,我们会关注安全是如何通过过滤器和注解而被应用到Web应用中的。当你想在更高的层次理解Spring安全框架都是如何工作的,并且想自定义一些特性时可以考虑这份指南,或者你只是想了解一下应用安全的知识也是可以的。

本指南不打算作为一个手册或解决多个最基本的问题的配方(可以查看别的地方),但是对于初学者或专家都会有帮助。SpringBoot也被引用了很多次,这是因为它为一个安全的应用提供了很多默认的行为,这对于理解他们如何同整个架构整合在一起很有帮助。对于所有没有使用SpringBoot的应用这些原则也能很好的应用。

身份验证和访问控制

应用安全可以大概分为两个独立的问题:身份验证(你是谁?)和授权(你有权做什么?)。有时候把 “授权” 称作 “访问控制” ,这可能会引起困惑,但是在某些地方这么想是有好处的而“授权”却显得太重了。Spring安全框架被设计成分开身份验证和授权模块,但是对于二者都提供了各自的策略和扩展点。

身份验证

身份验证的主要策略设置接口是 AuthenticationManager ,它只有一个方法:

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

}

AuthenticationManager 会在它的 authenticate() 方法里面做下面三件事之一:

  1. 如果它验证当前用户通过,会返回一个 Authentication (正常情况下authenticated=true)
  2. 如果验证当前用户不合法,会抛出一个 AuthenticationException 异常
  3. 如果它无法决定,返回 null

AuthenticationException 是一个运行时异常,应用通常会根据自己的类型和目的采用比较通用的方法来处理。换句话说,用户的代码通常不会直接捕获并处理这个异常。比如:Web服务器会返回一个用户身份验证失败的页面,同时HTTP服务会返回一个401状态码,或许 头也会被带上。

最常用的 AuthenticationManager 接口实现是 ProviderManager, 它会把工作进一步委托给一组AuthenticationProvider集合(链)。AuthenticationProvider 是类似于 AuthenticationManager 的接口,但是它多出一个方法用来判断所支持的 Authentication 类型。

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

    boolean supports(Class<?> authentication);

}

supports() 方法中的 Class<?> 参数实际支持的类型是 Class<? extends Authentication>. 通过把工作代理给一组AuthenticationProviders实例,在同一个应用中,一个ProviderManager能够同时支持多种不同的身份验证机制。如果一个 ProviderManager 不能识别特殊的 Authentication 类型,那么它会被直接跳过。

每个ProviderManager会有一个可选的父母(parent),如果所有的 providers 都返回 null时,会调用父母的实现。如果父母不可用(null),会抛出 AuthenticationException 异常。

有时,应用对于一些受保护的资源会有一个逻辑分组(比如,所有的Web资源访问路径类似/api/**),每个分组都可以设置自己的AuthenticationManager。通常情况下,每一个这样的分组都会对应一个 ProviderManager,他们都会共享同一个父母。父母在某种程度上是全局的资源,给所有的其它ProviderManager提供保底方案。

Figure 1. An AuthenticationManager hierarchy using ProviderManager

自定义 AuthenticationManager

Spring安全提供了一些配置帮助类,以便快速的在应用中创建通用的身份验证功能。最常用的帮助类是 AuthenticationManagerBuilder,它对于创建基于内存的(in-memory)、JDBC或者LDAP用户详情(user details) 或者添加自定义的 UserDetailsService 支持的都非常好。下面是一个配置全局类型(parent)的 AuthenticationManager 的示例:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

  @Autowired
  public initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
    auth.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

这个例子涉及到一个Web应用,但是对于 AuthenticationManagerBuilder 的使用有更广泛的适用场景(见下面Spring安全的实现细节)。注意到 AuthenticationManagerBuilder 通过 @Autowired 注解被注入到一个 @Bean 中的方法 - 这会导致它构造的是全局的(parent)AuthenticationManager。与之相反,如果通过下面的方式:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

  @Override
  public configure(AuthenticationManagerBuilder builder) {
    auth.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

(使用 @Override 注解方法)会导致 AuthenticationManagerBuilder 构建的是一个"局部的(local)“的AuthenticationManager,他会是全局 AuthenticationManager 的一个孩子。在SpringBoot应用中,你可以通过 @Autowired 把全局的 AuthenticationManager 注入到另外一个Bean中,但是除非你主动暴露否则是不能把本地的(local)的AuthenticationManager通过类似的方式注入到另一个Bean中的。

如果你没有提供自己的AuthenticationManager, SpringBoot会提供默认的全局 AuthenticationManager(仅包含一个用户)。默认的实现已经足够的安全,除了急需一个自定义的AuthenticationManager实现,大多时候你不需要担心它的安全性。一般情况下,只要根据自己要保护的资源定义一个相关的本地AuthenticationManager实现就足够了,没有必要去修改全局的那个。

授权/访问控制

一旦身份验证成功,接下来会进入授权访问环节,它的核心策略是通过 AccessDecisionManager 接口实现的。框架提供了3种实现,它们内部都会把工作委托给一组 DecisionVoter 集合(链),这和 ProviderManager 类似,后者委托给一组 AuthenticationProviders 集合。

  //译注:这个代码片段是译者加上去的,方便对本节的理解 
  void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
        throws AccessDeniedException, InsufficientAuthenticationException;

DecisionVoter 会考虑 Authentication(当前用户) 和一个通过 ConfigAttributes 描述的普通对象。在AccessDecisionManagerDecisionVoter 的方法签名上,这个对象是一个泛化类型 - 它代表了用户想要访问的任何资源(Web资源或者某个Java类的方法)。ConfigAttributes 的设计也是泛化的,它描述了一个需要安全访问的对象,并提供一些元信息来决定访问对象的权限等级。

ConfigAttribute 是一个只有一个方法的接口,它的实现相当通用,它会返回一个 String 类型,可以把资源所有者的意图编码在字符串里面以控制资源访问的规则。典型的 ConfigAttribute 是一些用户角色字符串(比如:ROLE_ADMINROLE_AUDIT),他们通常会有特殊的格式(比如ROLE_开头的字符串)或者是可以推导的表达式。

大部分人会使用默认的AccessDecisionManager,它是AffirmativeBased(如果没人投票拒绝,那么会允许访问)。对于自定义AccessDecisionManager一般会通过添加一个新的Voter或者修改一个现成的来实现。

ConfigAttributes中,使用Spring的表达式语言(Spring Expression Language)是很常见的做法,比如 isFullyAuthenticated() && hasRole('FOO')。它是通过一个支持解析这种语法的DecisionVoter实现的。为了扩展这种语法的处理范围,需要实现定义的 SecurityExpressionRoot 有时候还需要 SecurityExpressionHandler

Web 安全


Spring安全在Web层(UI和HTTP后台)是基于Servlet过滤器实现的,所以关注一下 Filters 在整个环节中扮演的角色很有帮助。下图展示了对一个HTTP请求处理的典型布局:

客户端发送一个请求给应用,然后容器根据请求的URI的路径来决定使用哪个过滤器或Servlet来处理请求。一个Servlet最多处理一个请求(?),但是过滤器会形成一个链,所以它们是有序的,并且事实上一个过滤器如果想自己处理请求那么可以否决链上之后的过滤器。一个过滤器也可以中途修改下游过滤器或Servlet中要处理的请求(request)或者响应(response)。过滤器链的顺序是非常重要的,SpringBoot通过两种机制来管理:一种是Filter类型的Bean可以使用@Order注解或者实现Ordered接口,另一种是把Filter添加到FilterRegistrationBean,它有提供了设置Filter顺序的方法。有一些现成的Filter定义了一些常量来标记自己的顺序(比如:SpringSession中的SessionRepositoryFilter定义了DEFAULT_ORDER = Integer.MIN_VALUE + 50),告诉我们它想待在链的前端但是又不想完全霸占最前排。

Spring安全是使用一个Filter实现的。在SpringBoot应用中,Spring安全模块只是 ApplicationContext 中的一个Bean,它默认已经被安装到系统中,所以每个请求都会经过安全模块。它的位置是通过 SecurityProperties.DEFAULT_FILTER_ORDER 定义的,而它是进一步通过 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER (SpringBoot应用期望那些封装请求/更改行为的Filter的最大顺序)来锚点。关于它还有更多需要说明的:从容器的角度来说,Spring安全就是一个Filter而已,但是在它内部包含其它的Filter,每一个都扮演了不同的角色。如下图示:

Figure 2. Spring Security is a single physical Filter but delegates processing to a chain of internal filters

事实上,在SpringSecurity的Filter实现包含更多的中间层。它通常是通过 DelegatingFilterProxy 安装到容器中的。这个代理会把工作委托给FilterChainProxy - 它本身是一个Bean通常有一个固定的名字 - springSecurityFilterChainFilterChainProxy包含了所有的安全逻辑,它内部组织成一个或多个链的形式。所有的过滤器都包含相同的API(它们都实现了来自Servlet标准的Filter接口API)而且它们都有给下游的Filter投票的机会。

SpringSecurity内部可以包含多个过滤器链,这对于容器来说都是透明的。SpringSecurity内部包含多个Filter链,它们会根据匹配的请求被应用在不同的请求的处理上。下图展示了根据匹配路径(/foo/**会在/**之前匹配)匹配过滤器的场景。这是比较常见的方式,但不是唯一的方式。在整个分发过程中,最重要的事情是每次只有一个链用来处理请求。

Figure 3. The Spring Security FilterChainProxy dispatches requests to the first chain that matches.

一个普通的没有自定义过任何安全配置的SpringBoot应用会包含多个(=n)Filter链,通常 n=6.第一个(n-1)链是用来忽略静态资源的,比如 /css/**/images/**还有错误页面 /error(这些路径可以通过SecurityProperties配置Bean中的security.ignored属性定义)。最后一个Filter链会匹配所有的请求路径 /**而且更活跃,包含了身份验证逻辑,授权逻辑,异常处理逻辑,Session处理逻辑,HEAD写入逻辑等。缺省情况下,这个链中包含11个过滤器,但是对于开发者来说,通常没有必要知道哪个Filter在什么时候被使用。

注:SpringSecurity的内部包含哪些Filter对于容器来说是无知的,这一点很重要,特别是在SpringBoot程序中,所有的Filter类型的Bean都会被默认注入到容器中。所以如果你想在安全链中添加一个自定义的Filter,那么最好不要把它注解为一个Bean而是通过FilterRegistrationBean封装进去,这样可以避免容器自动把这个Filter注册到自身。

创建并自定义过滤器链

在SpringBoot应用中,保底的过滤器链(匹配/**路径的那个)有一个提前定义好的顺序 - SecurityProperties.BASIC_AUTH_ORDER。你可以通过设置 security.basic.enabled=false 关闭它,或者你可以把它用作一个保底方案同时把其它的规则定义成更低的顺序 - 通过实现一个WebSecurityConfigurerAdapter 类型的Bean(或者WebSecurityConfigurer也可以)并通过 @Order 注解标识顺序。示例:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}

这个Bean会导致SpringSecurity在过滤链中添加一条新的链,并且处于兜底方案之前。

对于不同的资源可能有完全不同的访问控制规则。比如,一个包含UI服务和API服务的应用,对于UI部分可能会采取基于Cookie的身份验证方案并会重定向到登陆页面,但是对API部分,可能会采用基于Token的方案,并且返回一个401访问受限的状态码。它们分别会配置自己的 WebSecurityConfigurerAdapter 并设定顺序和匹配规则。如果规则重叠,那么处于前排的过滤器链会胜出。

匹配请求

SpringSecurity的过滤器链(或者等价的说是WebSecurityConfigurerAdapter)包含一个请求匹配规则用来决定是否应用于某个HTTP请求。一旦一个过滤器链被应用于某个请求,那么其它的都不会再被应用。但是在过滤器链内部,你可以通过HttpSecurity设置额外的匹配器来做更细粒度的授权访问控制。比如:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**") // 负责匹配整个FilterChain
      .authorizeRequests()     // 以下匹配访问控制的规则
        .antMatchers("/foo/bar").hasRole("BAR")
        .antMatchers("/foo/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

在配置Filter的时候最容易犯的错误是,这里不同的匹配器应用于不同的处理流程,一个是负责匹配整个FilterChain,其它的用来选择访问控制的规则。

方法安全

SpringSecurity不仅能够保护Web的安全,同样能够保证java方法的安全。对于SpringSecurity来说,这仅仅是另外一种”资源“。对于用户来说,访问控制的规则使用和 ConfigAttribute 类似格式的字符串(比如,角色或表达式),但是这需要在代码的不同地方设置。第一步需要启动方法安全的配置,比如:

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

然后就可以通过注解来装饰方法了,比如:

@Service
public class MyService {

  @Secured("ROLE_USER")
  public String secure() {
    return "Hello Security";
  }

}

例子中的服务包含一个安全的方法。Spring在创建这种类型的Bean的时候,会把它代理掉,对方法的调用会经过一个安全拦截器然后方法才能够执行。如果对方法的访问被拒绝,那么会抛出一个AccessDeniedException

还有其他的注解可以用来给方法加上安全限制,比如:@PreAuthorize@PostAuthorize, 允许使用包含方法参数和返回值的表达式。

注:同时使用Web安全和方法安全是常见的做法。FilterChain提供了用户体验方面的功能,比如身份验证并重定向到登陆页面,而方法安全提供了更细粒度的保护。

处理线程问题

Spring安全是以线程为边界的,因为它需要让当前验证的用户对接下来的一大批下游消费者可用。基础模块是 SecurityContext,它包含一个 Authentication 对象(当一个用户登陆的时候,它会返回一个验证通过的Authentication对象)。你可以随时通过 SecurityContextHolder提供的静态便捷方法操作 SecurityContext 对象,它内部其实是在操作一个 TheadLocal,比如:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

对于用户代码来说很少去直接操作它,但是如果你是在自定义自己的身份验证Filter,那么这会非常有用(也可以通过父类提供的方法来获取SecurityContext所以可能也不会用到 SecurityContextHolder)。

如果你想在Web服务器的API上访问当前身份验证通过的用户,你可以通过@RequestMapping注解的方法参数获得:

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
  ... // do stuff with user
}

这注解(@AuthenticationPrincipal)会从SecurityContext拿到当前的Authentication,并调用它的getPrincipal()方法来生成方法参数。Authentication 中的 Principal 的具体类型取决于 AuthenticationManager 中用来验证身份的对象类型,所以这是一种有用的小技巧以便来获得一个类型安全的指向用户资料的引用。

如果使用了SpringSecurity,那么从HttpServletRequest中提取的Principal对象将会是Authentication类型,你也可以直接使用:

@RequestMapping("/foo")
public String foo(Principal principal) {
  Authentication authentication = (Authentication) principal;
  User = (User) authentication.getPrincipal();
  ... // do stuff with user
}

有时候如果你想在没有使用SpringSecurity的情况下,也希望这片代码生效,那么这种写法是很有帮助的(你在获取Authentication的时候可能需要写一点防卫性的代码)。

异步的处理安全方法

因为 SecurityContext 是以线程为边界的,如果你想异步调用相关方法,比如使用@Async注解,你需要保证Context有被传递。可以使用能够后台运行的任务(RunnableCallable等)来封装SecurityContext.Spring安全提供了一些帮助方法来使这个过程更简单,比如对RunnableCallable的一些封装。为了把SecurityContext传递到@Async注解的异步方法,你需要提供一个 AsyncConfigurer 配置并且保证Executor是正确的类型:

@Confiuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

  @Override
  public Executor getAsyncExecutor() {
    return new DelegatingSecurityContextExecutorService(Executors.new FixedThreadPool(5);
  }

}
显示全文