1. Introduction(简介)
本篇是关于Spring安全框架的入门指导,主要讲解Spring 安全框架的体系结构,设计思路和组成模块。虽然本文只涵盖了最为基本的应用安全知识,但这些足以帮助开发者消除在使用Spring 安全框架进行开发时遇到的一些困惑。为了完成这些工作,我们来瞧一瞧如何通过Filters(Servlet规范中一种组件)以及更为常用的方法注解来在Web应用中使用安全组件。
如果你需要在更高的层次上理解一个安全的应用是如何工作的,或者你想知道如何定制化应用的安全组件,或者你仅仅只是想要了解一下应用安全方面的知识,那么,都可以通过阅读本篇指导获取你想要的。但是本篇指导并没有打算去说明或者解决超出基本安全范围的问题或者需求(这些工作由其他的指导来完成),但对于一个关于应用安全的初学者来说,这篇指导是非常有用的。这篇指导有很大的篇幅涉及到了Spring Boot,这是因为Spring Boot默认为应用的安全提供了一些支持,这对于我们理解Spring安全框架是如何适配整个Spring体系结构是有帮助的。所有这些适用于Spring Boot应用的方式或者方法,同样适用于那些使用了Spring框架的其他形式的Web应用程序。
2. Authentication(认证) & Access Control(访问控制)
应用程序的安全问题或多或少可以归纳为两个相互独立的基本问题:认证(Authentication,解决身份识别问题,即识别用户身份是否合法)和授权(Authorization ,解决访问权限问题,即允许用户做什么)。有些人使用"access control(访问控制)"来代替"authorization(授权)",虽然这两种说法会给用户带来一些困惑,但由于"authorization(授权)"这个词在有些地方被过度的解释了,这导致"access control(访问控制)"这种说法更有助于我们理解这种控制用户的访问权限的方式。Spring安全框架的体系结构在设计的时候就将认证(authentication)从授权(authorization)中分离出来,并且设计了一些策略能够对这两者进行扩展。
2.1 Authentication(认证)
Spring安全框架中,为认证(Authentication)设计的主要策略接口是org.springframework.security.authentication.AuthenticationManager
,而这个接口只有一个方法:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
在AuthenticationManager
的authenticate()
方法中,用户可以做三件事情:
- 如果可以确认输入的参数
authentication
(是org.springframework.security.core.Authentication
的实例对象)代表一个合法的用户身份,那么返回另一个org.springframework.security.core.Authentication
实例对象,这个返回的对象通常会带有一个authenticated=true
的标记。 - 如果可以确认输入的参数
authentication
代表一个非法的用户身份,那么将抛出一个org.springframework.security.core.AuthenticationException
异常。 - 如果无法判断输入的参数
authentication
是否是一个合法的用户身份,可以返回null值。
org.springframework.security.core.AuthenticationException
是一个运行时异常。一般情况下,该异常会被应用程序使用专门的处理器进行处理,而如何处理取决于应用程序的形式和用途。换句话说,一般情况下,应用程序并不指望开发者编写代码去捕获和处理这些异常,而是提供默认的策略,由应用程序自身来处理这个异常,比如,应用程序会提供一个界面同时渲染一个页面来告诉用户认证失败,同时后台的HTTP服务也将会发送401状态码,也会根据应用的上下文环境来决定是否携带WWW-Authenticate
头部。
最常用的org.springframework.security.authentication.AuthenticationManager
实现类是org.springframework.security.authentication.ProviderManager
,该类维护了一个由接口org.springframework.security.authentication.AuthenticationProvider
的实现类的实例所组成的列表。而org.springframework.security.authentication.AuthenticationProvider
同org.springframework.security.authentication.AuthenticationManager
相似,区别在于org.springframework.security.authentication.AuthenticationProvider
内部包含另一个方法允许调用者测试是否支持传入的org.springframework.security.core.Authentication
实现类的类型:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
supports()
方法中的Class<?>
参数的真正类型是Class<? extens Authentication>
,主要是用来测试是否支持传入authenticate()
方法的authentication
参数的类型。由于ProviderManager
代理了一个AuthenticationProviders
链,所以可以在同一个应用中支持多种不同的认证机制。一般情况下,ProviderManager
会跳过那些自己不支持的Authentication
的实例类型。
一个ProviderManager
有一个可选的父级provider,如果所有的providers都返回的是null,也就是说所有的认证机制都无法确定当前的用户身份是合法的,最终将由这个父级(或者全局)的provider来决定,如果不存在这个父级的provider也会返回null,最终会抛出AuthenticationException
。
有时候,应用将被保护的资源按照一定的规则分成逻辑上的分组(比如,所有的web资源都通过路径来分组,即将资源按照linux目录树的形式进行分组),并且每一个分组都拥有专属的AuthenticationManager
,而这些AuthenticationManager
通常都是一个ProviderManager
,这些AuthenticationManager
共享一个父级(或者全局)的AuthenticationManager
,这个父级的AuthenticationManager
作为所有的providers的替补而存在。
2.2 Customizing Authentication Managers(自定义认证管理器)
Spring安全框架提供了一些配置的辅助类,能够在应用中快速的创建常用的认证管理功能。最常用的辅助类就是org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
,该类主要用来设置获取用户信息的方式,有三种方式,分别是in-memory,JDBC
或者LDAP
,也可以添加实现了org.springframework.security.core.userdetails.UserDetailsService
接口的类对象来设置获取用户详情的方式。
下面的例子展示了如何在一个应用中配置全局的AuthenticationManager
:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
... // web stuff here
@Autowired
public initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
这是一个web应用的例子,AuthenticationManagerBuilder
的使用方式有很多种,而在这个例子中AuthenticationManagerBuilder
的一个实例被应用作为参数通过@Autowired
注解传入initialize()
方法中,在这个方法中将会创建一个全局(父级)的AuthenticationManager
。我们可以和另外一种使用方式进行对比:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
... // web stuff here
@Override
public configure(AuthenticationManagerBuilder builder) {
builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
上例中使用@Override
注解,覆盖了父类的configure()
方法,在这个方法中,AuthenticationManagerBuilder
的实例只是被该方法的调用者使用来创建一个局部的AuthenticationManager
,这个局部的AuthenticationManager
是全局AuthenticationManager
的孩子。在一个Spring Boot应用中,我们可以使用@Autowired
注解将全局的AuthenticationManager
注入到其他的bean
中,但是无法将一个局部的AuthenticationManager
注入到其他的bean
中,除非我们通过配置@Bean
的方式明确的将该局部的AuthenticationManager
的实例作为一个组件发布出去。
Spring Boot提供了一个默认的全局AuthenticationManager
,我们可以通过提供自己的AuthenticationManager
组件来替换他。这个默认的AuthenticationManager
组件是足够安全的,我们无需对他有过多的担心,除非你确实需要一个自定义的AuthenticationManager
。如果我们做了一些配置创建了一个AuthenticationManager
组件,我们可以将该组件应用到局部的受保护资源上而无需担心全局的AuthenticationManager
。
前文说的全局的provider,是
ProviderManager
组件,ProviderManager
是AuthenticationManager
最常用的一个实现类,所以全局的provider就是全局的AuthenticationManager
组件。
2.3 Authorization or Access Control(授权或访问控制)
一旦用户成功通过了认证,我们就可以开始关注授权问题,核心的授权策略由接口org.springframework.security.access.AccessDecisionManager
来提供。Spring安全框架提供这个接口的三种实现,每一种都可以委托给一个org.springframework.security.access.AccessDecisionVoter<S>
链,这与ProviderManager
委托给AuthenticationProviders
有一点类似。
一个AccessDecisionVoter
主要用来处理代表用户身份的Authentication
对象和一个由ConfigAttributes
描述的安全对象:
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
这个安全对象指的是参数object
,其类型就是AccessDecisionVoter<S>
的泛型参数S,他代表任何用户想要访问的目标(Web资源或Java类的方法是该对象的两种最常见的形式)。ConfigAttributes
指的是参数attributes
,他是org.springframework.security.access.ConfigAttribute
的集合,保存了安全对象的元数据,而这些元数据确定了访问该安全对象所要求的权限。ConfigAttribute
是只有一个方法的接口,而这个方法返回一个String
类型的值,这个值通常情况下是一串编码,表示资源所有者制定的资源访问规则。典型的ConfigAttribute
应用方式就是返回表示用户角色的字符串,比如(ROLE_ADMIN
或者ROLE_AUDIT
),这些都有统一的格式(比如以ROLE_
为前缀),而另外一种应用方式是返回能够用来执行的表达式字符串。
绝大多开发者只使用默认的AccessDecisionManager
组件,默认的AccessDecisionManager
的机制是如果得票数没有下降,那么访问就应该被允许。因此所有的定制化开发都倾向于发生在投票者那里,要么是增加新的投票者,要么改变已有投票者的行为。
最常用的ConfigAttributes
是Spring的EL表达式,例如isFullyAuthenticated() && hasRole('FOO')
。一种AccessDecisionVoter
组件就支持Spring的EL表达式,他不但能够执行这些表达式,同时还能为他们创建上下文环境。如果想要扩展表达式的语法,可以实现org.springframework.security.access.expression.SecurityExpressionRoot
抽象类或者实现org.springframework.security.access.expression.SecurityExpressionHandler<T>
接口。
3. Web Security(Web安全)
3.1 Web Security基本组件
Spring安全框架在Web层的组件都是基于Servelt规范中的Filters
,所以事先弄明白Filters
所扮演的角色对与我们理解Web安全是有帮助的。下面的图片展示了Http请求处理器的层级结构。
客户端发送请求到应用,容器决定哪些fitlers以及哪一个servlet可以用来处理这次请求。绝大数情况下,一个servlet只能处理一种请求,但是这些filters组成了一个链,他们按照一定的顺序排列,如果某一个filter想要处理这个请求,那么他可以将这个请求拦截下来,并且进行处理。一个filter也能够改变request请求或者response回复。Filter链的顺序是非常重要的,Spring Boot有两种机制在管理filter的顺序 ,一种是在使用@Bean
注解发布一个Filter
的时候,同时使用@Order
注解来指定这个filter的优先级(优先级是由一个int
类型的整数表示,数值越大,优先级越高),或者让这个Filter
直接实现org.springframework.core.Ordered
接口,通过getOrder()
方法返回优先级数值,另一种方法是使用org.springframework.boot.web.servlet.FilterRegistrationBean
注册Filter
的时候,使用他的相关API来指定要注册的Filter
的优先级。一些标准的filters
通过定义一些常量值来确定他们之间的顺序(比如Spring Session框架中的SessionRepositoryFilter
组件,默认的优先级数值由其自身定义的常量DEFAULT_ORDER
来表示,其值为Integer.MIN_VALUE + 50
,这个值只比int
型整数的最小值大一点,因此这个过滤器几乎是排在过滤器链的最下面,要到达这里,必须先通过其他过滤器)。
Spring安全的核心组件就是一个安装到这个过滤器链中的Filter
,他的具体类型是org.springframework.security.web.FilterChainProxy
,稍后我们将会详细说明这个安全过滤器。在一个Spring Boot应用中,安全过滤器(security filter)是ApplicationContext
的一个@Bean
,一旦开启了Spring安全功能,就会默认安装这个安全过滤器,并且拦截所有的请求。安全过滤器在过滤器链中的位置由org.springframework.boot.autoconfigure.security.SecurityProperties.DEFAULT_FILTER_ORDER
表示的优先级数值来决定,这个值位于锚点org.springframework.boot.web.servlet.FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER
的下方(这个值是Spring Boot应用中最大的过滤器优先级数值,因为Spring Boot希望请求在通过整个处理流程之前,先被这个过滤器包装一下,改变一下行为)。从下图我们可以看到,Spring安全框架所提供的功能由单个Filter
来提供,但是在这个Filter
中,包含着多个内部filters
,并且每一个都具有特定的功能。图片如下:
在Spring应用中,过滤器通常是安装在类型为org.springframework.web.filter.DelegatingFilterProxy
的代理容器中,这种容器并不以Spring的@Bean
的形式存在,而是作为原生的Servlet规范中的Filter
组件安装到Servlet容器中。Spring安全的过滤器组件就是安装在这种代理容器中,是一个类型为org.springframework.security.web.FilterChainProxy
且具有固定名字springSecurityFilterChain
的过滤器,这个安全过滤器是以Spring@Bean
的形式存在的。而springSecurityFilterChain
过滤器又包含了一个封装了安全逻辑的有序过滤器链,组成这个链的过滤器都有相同的API(通常是实现了Servlet规范中Filter
接口)并且每一个过滤器都可能将请求拦截到自己这一层并进行处理。所以,事实上的安全层可不止一层。
当然,springSecurityFilterChain
也可能会管理多个不同的过滤器链,也就是包含一个过滤器链的列表,并且,所有这些过滤器对容器都是透明的。并且springSecurityFilterChain
会将请求派发给第一个匹配的过滤器链。下图展示了基于请求路径的派发过程(这也是使用最多但并不唯一的方式)。这种派发过程的最重要的特点就是有且只有一个过滤器链来处理这个请求。
一个没有任何定制化配置的Spring Boot应用具有6个过滤器链,前5个过滤器链只会忽略那些指向静态资源的路径,例如/css/**
和/images/**
,以及用来展示错误信息视图的路径/error
(这些忽略的路径可以在SecurityProperties
配置bean中,使用security.ignored
来控制)。最后一个过滤器链匹配所有的路径/**
并且也是最活跃的,包含认证和授权的逻辑,错误处理,会话处理,头部信息处理等。在这些默认的过滤器链中有总共11个过滤器,通常情况下,用户无需去关心哪个过滤器被使用了以及是什么时候使用的。
注意
Spring安全的所有内部过滤器对于容器来说都是透明的,这很重要,特别是Spring Boot应用,因为所有Filter
类型的@Bean
都是由容器自动注册的。所以如果你想要添加自定义的安全过滤器到Spring安全的过滤器链中,那么你最好不要通过配置Filter
类型的@Bean
的方式来添加,因为这样Spring应用会把过滤器注册到容器中而不是添加到Spring安全的过滤器链中,你可以通过将自定义的安全过滤器封装在FilterRegistrationBean
中来达成目的。
3.2 Creating and Customizing Filter Chains(创建和定制过滤器链)
在Spring Boot应用程序中,有一个默认的后备过滤器链(该过滤器链匹配所有的请求路径/**
)有一个预定义的顺序值SecurityProperties.BASIC_AUTH_ORDER
,你可以通过设置security.basic.enabled=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/**")
...;
}
}
上例中的@Configuration
的bean
将会使Spring安全框架添加一个优先级排在后备过滤器链前面的新过滤器链。
许多的应用程序拥有访问规则互不相同的资源组。例如:一个应用程序提供的资源包括用户UI和后台API接口两个部分,对于用户UI,支持基于Cookie的认证,而未认证的请求会重定向至登陆页面;而对于后台API接口,则支持基于令牌的认证,未认证的请求会收到携带401状态码的回复。每一个资源组都有他自己的WebSecurityConfigurerAdapter
,并且具有唯一的优先级以及请求路径的匹配规则。如果匹配规则发生重叠,那么优先级更高的过滤器链将会胜出。
3.3 Request Matching for Dispatch and Authorization(针对派发和授权的请求匹配)
一个安全过滤器链(等同与一个WebSecurityConfigurerAdapter
)持有一个请求匹配器,这个匹配器被用来决定该过滤器链是否适用于当前的HTTP请求。一旦一个HTTP请求适用与一个特定的过滤器链,其他的过滤器链则不会被应用于这个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安全的时候最容易犯的一个错误就是忘记了这些匹配器将应用于不同的程序,一个是请求匹配器,将应用于整个过滤器链,而其他的匹配器仅仅是用来选择访问规则。
3.4 Combining Application Security Rules with Actuator Rules(应用程序的安全规则与监控规则的整合)
如果你在使用Spring Boot Actuator来监控应用程序的端点(即由path所指向的资源),你应该希望他们是安全的并且默认他们是安全的。实际上,当你将Spring Boot监控功能添加到一个安全的应用程序中时,同时会添加一个过滤器链,而这个过滤器链只会拦截访问Spring Boot监控端点路径的请求。这个过滤器链定义了一个请求匹配器,这个匹配器只匹配监控端点路径,并且具有一个值为ManagementServerProperties.BASIC_AUTH_ORDER
的优先级,这个优先级只比默认的SecurityProperties
替补过滤器高一点(数值小5),所以匹配监控端点路径的请求会先到达这个过滤器链。
如果你想要你的应用程序的安全规则应用到监控功能端点上,你可以添加一个优先级高于监控端点过滤器链的新的过滤器链,并且让这个新过滤器拦截所有访问监控端点的请求。如果你更倾向于对监控端点使用默认的安全配置,那么最简单的做法就是将你自己的过滤器链添加到监控接口过滤器的后面和替补过滤器的前面。示例如下:
@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/foo/**")
...;
}
}
注意
Spring安全框架在Web层与Servlet的API是绑定的,所以现阶段,Spring安全框架只能应用于基于Servlet规范,运行在Servlet容器中的应用程序,而无论容器是否是嵌入式的。当然,Spring安全框架并没有与Spring MVC框架或者Spring的Web技术栈绑定,所以他能够应用于所有基于Servlet规范的应用程序。
4. Method Security(方法安全)
Spring安全框架不仅仅支持Web应用程序,也为Java方法的执行提供安全的访问规则支持。对于Spring安全来说,Java方法只是一种其他形式的“受保护资源”。这就意味着方法的访问规则是与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安全和方法安全结合起来使用并非是不常用的。Web安全过滤器链提供用户粒度的安全功能,例如认证和重定向到登陆页面,而方法安全能够提供更细粒度的安全保证。
5. Working with Threads(工作线程)
Spring安全本身就是一个基本的线程边界,因为当前身份被认证后,仍然需要被各种下游消费者所使用。最基本的构造块是org.springframework.security.core.context.SecurityContext
的实例对象,他包含了一个Authentication
对象(并且如果用户已经登陆,那么这个Authentication
是被明确标记为authenticated
的)。你可以通过org.springframework.security.core.context.SecurityContextHolder
的静态方法方便的访问和使用保存在ThreadLocal
中的SecurityContext
实例。例如:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);
虽然对于用户应用程序来说,上面的代码并不常用,但这并不代表他没有用处,相反对于开发者来说,这段代码非常有用,比如在开发者想要定制化编写一个认证过滤器的时候(尽管认证过滤器在Spring安全中是最基本的类,开发者编写该类的时候应该尽量避免使用SecurityContextHolder
).
如果你想要在一个Web接口中访问当前的已认证用户,你可以在@RequestMapping
使用方法参数注解@AuthenticationPrincipal
来注入持有用户信息的对象。例如:
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
... // do stuff with user
}
@AuthenticationPrincipal
注解会SecurityContext
实例中抽取当前Authentication
对象并且调用其getPrincipal()
方法来获取用户身份对象,然后将用户身份对象注入到方法参数中。Authentication
持有的Principal
(用户身份)的类型取决于AuthenticationManager
验证认证时所使用的类型。
如果Spring安全从HttpServletRequest
中获取的Principal
就是Authentication
类型的,那么开发者可以用这种方式来直接使用:
@RequestMapping("/foo")
public String foo(Principal principal) {
Authentication authentication = (Authentication) principal;
User = (User) authentication.getPrincipal();
... // do stuff with user
}
5.1 Processing Secure Methods Asynchronously(以异步的方式处理安全的方法)
自从SecurityContext
成为线程边界后,如果你想要调用安全的方法做一些异步的后台操作,比如使用@Async
注解,那么你需要确保这个上下文是可传播的。说白了就是将SecurityContext
封装到task(Runnable
,Callable
等)中,然后交给后面的线程去执行。Spring安全提供了一些辅助类来帮助开发者更方便地完成这个过程。当然,为了传播SecurityContext
到@Async
方法中,开发者需要提供一个AsyncConfigurer
同时要确保Executor
的正确类型:
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}