Spring Security架构
本指南是Spring Security的入门文档,提供了对Spring Security框架的设计和基本构建块的深入了解。我们只讨论了应用程序安全性的基础知识,但这样做可以消除开发人员在使用Spring security时的一些困惑。为了做到这一点,我们来看看使用过滤器和更普遍的方法注解来提供Web应用安全性的方式。当您需要在较高的层次上理解安全应用程序如何工作,以及如何对其进行自定义,或者只是需要了解如何考虑应用程序安全性时,请使用本指南。
本指南并不打算作为解决最基本问题的手册(有其他来源),但它可能对初学者和专家都很有用。Spring Boot也被大量引用,因为它为安全应用程序提供了一些默认行为,了解它如何与整个体系结构相适应是很有用的。所有这些原则都同样适用于不使用Spring Boot的应用程序。
身份认证和访问控制
应用程序安全性归结为两个独立的问题:身份验证(您是谁?)和授权(允许您做什么?)有时人们会说“访问控制”而不是“授权”,这可能会让人感到困惑,但是这样想是有帮助的,因为“授权”在其他地方是重载的。Spring Security的体系结构旨在将身份验证和授权分离开来,并为这两者提供了策略和扩展点。
身份认证
身份认证的主要策略接口是AuthenticationManager
,这个接口只有一个方法:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
在AuthenticationManager
的authenticate()
方法中可能会返回下面三种结果:
- 如果验证成功,返回
Authentication
对象 (authenticated=true
) - 如果验证失败,抛出
AuthenticationException
异常 - 否则,返回
null
AuthenticationException
是一个运行时异常。它通常由应用程序以通用的方式处理,而不是在代码捕获并处理它。例如,web UI应该呈现一个表明身份验证失败的页面,后端HTTP服务应该返回一个401响应(根据上下文选择是否带有WWW-Authenticate
头)。
AuthenticationManager
最常用的实现是ProviderManager
,它委托给AuthenticationProvider实例链。AuthenticationProvider
有点像AuthenticationManager
,但是它有一个额外的方法,允许调用者查询它是否支持给定的Authentication
类型:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
supports()
方法中的Class<?>
参数实际上是Class<? extends Authentication>
。通过委托给Authenticationprovider
链,ProviderManager
可以在同一个应用程序中支持多个不同的身份验证机制。如果ProviderManager
不能识别身份验证实例的类型,那么就会跳过它。
每个ProviderManager
都有一个可选的父管理器(AuthenticationManager
),通过父管理器检查是否所有的provider都返回null
。如果父管理器不可用,那么空Authentication
会导致AuthenticationException
异常。
有时,应用可能会对受保护资源分组(例如,所有的匹配/aip/**
的web资源),每个组都有它特定的AuthenticationManager
。通常,每个Manager都是ProviderManager
,而且它们共享一个父管理器。这个父管理器是一种“全局“资源,作为所有provider的后备资源。
图1. 使用ProviderManager
的AuthenticationManager
层次结构
自定义身份认证管理器
Spring Security提供了一些配置帮助程序来快速获得应用程序中设置的通用身份验证管理器特性。最常用的是AuthenticationManagerBuilder
,它非常适合在内存、JDBC或LDAP中设置用户详细信息,或用于添加自定义UserDetailsService
。下面是一个应用程序配置全局(父)AuthenticationManager
的例子:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
... // web stuff here
@Autowired
public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
这个例子涉及到一个web应用程序,但是AuthenticationManagerBuilder
的使用更加广泛(有关如何实现web应用程序安全性的更多细节,请参阅下面的内容)。注意AuthenticationManagerBuilder
通过@Autowired
注入到@Bean
的一个方法中——这就是它构建全局(父)AuthenticationManager
的原因。相反,如果我们这样做:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
... // web stuff here
@Override
public void configure(AuthenticationManagerBuilder builder) {
builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
(使用configurer方法的@Override
重写)AuthenticationManagerBuilder
仅用于构建“本地”AuthenticationManager
,它是全局AuthenticationManager
的一个子类。在Spring Boot应用程序中,可以将全局bean通过@Autowired
注入到另一个bean中,但是不能对本地bean这样做,除非你自己显式地公开它。
Spring Boot提供一个默认的全局AuthenticationManager
(只有一个用户),你也可以用自己的AuthenticationManager
类型的bean来覆盖它。默认的AuthenticationManager
本身就足够安全,因此您不必太担心它,除非您确实需要一个自定义全局AuthenticationManager
。如果您要对构建AuthenticationManager
的配置进行修改,通常都可以要保护的资源进行配置,而不必担心全局缺省值。
授权或访问控制
Once authentication is successful, we can move on to authorization, and the core strategy here is AccessDecisionManager. There are three implementations provided by the framework and all three delegate to a chain of AccessDecisionVoter, a bit like the ProviderManager delegates to AuthenticationProviders.
一旦身份认证成功,我们就可以继续进行授权,这里的核心策略是AccessDecisionManager
。框架提供了三种实现,这三种实现都委派给了AccessDecisionVoter
调用链,这有点像ProviderManager
委派给`AuthenticationProviders·。
AccessDecisionVoter
对Authentication
(代表一个主体)和ConfigAttributes
修饰的安全对象
进行验证:
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
AccessDecisionManager
和AccessDecisionVoter
的方法签名中的Object
是完全通用的——它表示用户可能要访问的任何内容(web资源或Java类中的方法是最常见的两种情况)。ConfigAttribute
也是相当通用的,它用一些元数据来修饰安全对象
,这些元数据决定了访问它所需的权限级别。ConfigAttribute
是一个只有一个方法的接口,这个方法返回一个字符串,以某种方式编码了资源拥有者的意图,即关于谁可以访问的规则。典型的ConfigAttribute
是用户角色的名称(如ROLE_ADMIN
或ROLE_AUDIT
),它们通常具有特殊的格式(带有如ROLE_
前缀)或表示需要计算的表达式。
大多数人只是使用默认的AccessDecisionManager
,它是基于肯定的
(如果有任何Voter的回答是肯定的,那么访问就被授予了)。任何定制都可能发生在Voter中,要么添加新的Voter,要么修改现有Voter的工作方式。
ConfigAttributes
的通常用法是Spring Expression Language (SpEL)表达式,例如isFullyAuthenticated() && hasRole('FOO')
。AccessDecisionVoter
支持这种方法,它可以处理这些表达式并为表达式创建上下文。要扩展可以处理的表达式的范围,需要自己实现SecurityExpressionRoot
,有时还需要SecurityExpressionHandler
。
Web Security
Spring Security in the web tier (for UIs and HTTP back ends) is based on Servlet Filters
, so it is helpful to look at the role of Filters
generally first. The picture below shows the typical layering of the handlers for a single HTTP request.
Web层中的Spring Security(用于UI和HTTP的后端)是基于Servlet 过滤器
的,因此通常首先了解过滤器
的作用是有帮助的。下图显示了单个HTTP请求的处理程序的典型层次。
客户端向应用程序发送一个请求,容器根据请求URI的路径决定应用哪些过滤器和哪个servlet。一个servlet最多可以处理一个请求,但是过滤器形成了一个链,因此它们是有序的。实际上,如果过滤器希望自己处理请求,它可以否决链上的其余部分。过滤器还可以修改下游过滤器和servlet中使用的请求和/或响应。过滤器链的顺序是非常重要的。Spring Boot通过2机制管理过滤器的顺序:一是@Bean
类型的过滤器通过@Order
注解或实现Ordered
接口,另一种是通过FilterRegistrationBean
设置。一些现成的过滤器定义了它们自己的常量,以帮助确定它们之间的顺序(例如,Spring Session中的SessionRepositoryFilter
有一个DEFAULT_ORDER
值为 Integer.MIN_VALUE + 50
,这标识它排在过滤器链的前面,但是它不排除在它之前的其他过滤器)。
Spring Security是一个单独的过滤器
,它的相关类型是FilterChainProxy
,原因很快就会说明。在Spring启动应用程序中,安全过滤器是ApplicationContext
中的@Bean
,它是默认的,以便应用于每个请求。它的顺序由SecurityProperties.DEFAULT_FILTER_ORDER
设置,其值由FilterRegistrationBean .REQUEST_WRAPPER_FILTER_MAX_ORDER
定义(Spring Boot应用程序期望的最大顺序)。但是,它还有更多的功能:从容器的角度来看,Spring Security是一个单独的过滤器,但是在它内部有其他的过滤器,每个过滤器都扮演一个特殊的角色。这里有一个图片:
图2. Spring Security是一个单独的过滤器
,但将处理委托给内部过滤器链
事实上,在安全过滤器中甚至还有一个间接层:它通常作为DelegatingFilterProxy
运行在容器中,它不必是一个Spring @Bean
。它将委托给FilterChainProxy
,而FilterChainProxy
是一个@Bean
,通常有一个固定的名称springSecurityFilterChain
。FilterChainProxy
作为内部过滤器链(或多个过滤器链)包含了所有安全逻辑。所有过滤器都具有相同的API(它们都实现了Servlet规范中的Filter
接口),并且它们都有机会否决其余部分。
在顶部FilterChainProxy
中可以有多个由Spring Security管理的过滤器链,并且容器不知道所有这些过滤器链。Spring Security过滤器包含了一个过滤器链的列表,并将请求分派给与之匹配的第一个链。下图显示了基于匹配请求路径的分派(/foo/**
在/**
之前匹配)。这很常见,但并不是匹配请求的唯一方法。这个调度过程最重要的特性是只有一个链处理请求。
图3. Spring Security的FilterChainProxy
将请求分派到第一个匹配的过滤器链中。
没有自定义安全配置的普通Spring Boot应用有几个(n)过滤器链,通常n=6。前(n-1)个链只是为了忽略静态资源,如/css/**
和/images/**
,以及错误视图/error
(路径可以通过SecurityProperties
中的security.ignored
进行配置)。最后一个链与 /**
匹配,包含用于身份验证、授权、异常处理、会话处理、头文件写入等的逻辑。默认情况下,这个链中总共有11个过滤器,但通常用户不必关心使用哪个过滤器以及何时使用过滤器。
注意
Spring Security内部的所有过滤器都不为容器所知,这一点很重要,尤其是在Spring Boot应用程序中,默认情况下,所有类型为Filter
的@Bean
都会自动注册到容器中。因此,如果您想要向安全链添加自定义过滤器,则需要不将其设置为@Bean
,或者将其包装在FilterRegistrationBean
中,该bean显式地禁用容器注册。
创建和自定义过滤器链
Spring Boot应用(带有/**请求匹配器)中的默认后备过滤器链具有预定义的由SecurityProperties.BASIC_AUTH_ORDER
指定的顺序。你可以通过设置security.basicenabled=false
来完全关闭它,或者您可以使用它作为后退,并以较低的顺序定义其他规则。为此,只需添加一个WebSecurityConfigurerAdapter
(或WebSecurityConfigurer
)类型的@Bean
,并用@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将为Spring Security添加一个新的过滤器链并在后备过滤器之前对其进行排序。
很多应用对于不同的资源集合有完全不同的访问规则。例如,一个有UI和后端API的应用,对于UI部分,支持基于cookie的身份认证,会重定向到登录页面;对于API部分,支持基于token的身份认证,对未经过认证的请求会返回401响应。每一组资源都有自己的带有唯一序号和请求匹配器的WebSecurityConfigurerAdapter
。如果匹配规则重叠,则会选择顺序好较小的过滤器链。
请求匹配
安全过滤器链(即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/**")
.authorizeRequests()
.antMatchers("/foo/bar").hasRole("BAR")
.antMatchers("/foo/spam").hasRole("SPAM")
.anyRequest().isAuthenticated();
}
}
配置Spring Security时最容易犯的错误之一是忘记这些匹配器应用于不同的过程:一个是整个过滤器链的请求匹配器,另一个是只选择要应用的访问规则。
合并应用安全规则和执行器规则
如果您将Spring Boot Actuator用于管理端点,则可能希望它们是安全的,并且默认情况下确实是安全的。 实际上,将执行器添加到安全应用程序后,就会获得一条仅适用于执行器端点的附加过滤器链。 它由仅匹配执行器端点的请求匹配器定义,并且其顺序为ManagementServerProperties.BASIC_AUTH_ORDER
,该顺序比默认的SecurityProperties
后备过滤器少5,因此会在进行后备过滤器之前执行。
如果你希望将应用程序的安全规则应用于执行器端点,则可以添加一个比执行器顺序更早的过滤器链,并带有一个包括所有执行器端点的请求匹配器。 如果你更倾向于执行器端点的默认安全设置,那么最简单的方法是在执行器端点之后但在后备过滤器之前(例如ManagementServerProperties.BASIC_AUTH_ORDER + 1
)添加自己的过滤器。 例如:
@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/foo/**")
...;
}
}
注意
Web层中的Spring Security当前是与Servlet API绑定的,因此它仅在以嵌入式或其他方式在Servlet容器中运行应用程序时才真正适用。 但是,它不依赖于Spring MVC或Spring Web堆栈的其余部分,因此可以在任何servlet应用程序中使用,例如使用JAX-RS的servlet应用程序。
方法安全
As well as support for securing web applications, Spring Security offers support for applying access rules to Java method executions. For Spring Security this is just a different type of “protected resource”. For users it means the access rules are declared using the same format of ConfigAttribute strings (e.g. roles or expressions), but in a different place in your code. The first step is to enable method security, for example in the top level configuration for our app:
除了支持Web安全应用程序外,Spring Security还支持将访问规则应用于Java方法。 对于Spring Security,这只是“受保护资源”的另一种类型。 对于用户来说,意味着在代码中的不同位置使用跟Web安全相同的ConfigAttribute
字符串格式(例如角色或表达式)来声明访问规则。 第一步是启用方法安全性,例如在应用程序的最上层配置中:
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}
然后我们可以直接修饰方法的资源,例如:
@Service
public class MyService {
@Secured("ROLE_USER")
public String secure() {
return "Hello Security";
}
}
这个例子是一个使用了安全方法的Service。如果Spring创建了这种类型的@Bean
,那么它会被代理,并且在实际执行该方法之前,调用者必须通过安全拦截器。 如果访问被拒绝,则调用者将得到AccessDeniedException
异常,而不是实际的方法执行结果。
方法上还可以使用其他注解来强制执行安全性约束,特别是@PreAuthorize
和@PostAuthorize
,它们可以使您分别编写包含对方法参数和返回值的引用的表达式。
提示
Web安全和方法安全结合使用的情况并不少见。过滤器链提供了用户体验功能,例如身份验证和重定向到登录页面等,而方法安全在更精细的级别上提供了保护。
使用线程
Spring Security is fundamentally thread bound because it needs to make the current authenticated principal available to a wide variety of downstream consumers. The basic building block is the SecurityContext which may contain an Authentication (and when a user is logged in it will be an Authentication that is explicitly authenticated). You can always access and manipulate the SecurityContext via static convenience methods in SecurityContextHolder which in turn simply manipulate a TheadLocal, e.g.
Spring Security从根本上讲是线程绑定的,因为它需要使当前经过身份验证的主体可供各种后续的使用者使用。其基本构件是SecurityContext
,它可以包含一个Authentication
(当用户登录时,它将是一个经过显式身份验证的Authentication
)。 您始终可以通过SecurityContextHolder
中的静态方法来访问和操作SecurityContext
,该方法又可以简单地操作TheadLocal
,例如:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);
用户应用程序代码执行此操作并不常见,但是例如在您需要编写自定义身份验证过滤器时可能会很有用(尽管即使如此,Spring Security中也可以使用基类来避免需要使用SecurityContextHolder
)。
如果需要在Web端点中访问当前已认证的用户,则可以在@RequestMapping
中使用方法参数。 例如:
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
... // do stuff with user
}
这个注解从SecurityContext
中获取当前的Authentication
,并调用getPrincipal()
方法来产生方法参数。Authentication
中的Principal
类型取决于用于验证身份验证的AuthenticationManager
,因此这对于获得对用户数据的类型安全引用是一个有用的小技巧。
如果使用Spring Security,则HttpServletRequest
中的Principal
的类型将为Authentication
,因此也可以直接使用它:
@RequestMapping("/foo")
public String foo(Principal principal) {
Authentication authentication = (Authentication) principal;
User = (User) authentication.getPrincipal();
... // do stuff with user
}
如果你希望编写的代码在不使用Spring Security时也可以工作,那么这会很有用(需要在装入Authentication
类时更具防御性)。
异步处理安全方法
由于SecurityContext
是线程绑定的,因此,如果要执行任何调用安全方法的后台处理,例如使用@Async
,您需要确保传播上下文。 归结为将SecurityContext
与在后台执行的任务(Runnable
,Callable
等)包装在一起。 Spring Security提供了一些帮助程序,例如Runnable
和Callable
的包装器。 要将SecurityContext
传播到@Async
方法,您需要提供AsyncConfigurer
并确保Executor
具有正确的类型:
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}