这个指南是Spring Security的入门指南,提供了一个研究框架设计和框架模块的机会。 我们只讨论应用程序安全性的基本知识,这样做可以消除开发人员使用Spring Security时遇到的一些困惑。为了做到这一点,我们来看看web应用程序中安全性的应用方式,使用过滤器Filter,更一般地说是使用方法注解(Method Annotations)。当您需要高水准的了解安全应用程序是如何工作的,以及如何对其进行定制改造,或者您需要了解如何设计应用程序安全性时,请参阅本指南。
本指南不打算作为一本手册或科普文档来解决最基本的问题或概念(有很多其他资料可以查阅),所以它对初学者或者领域专家同样的有用。SpringBoot会再后面经常被提及,因为它为一个安全的应用程序提供了一些默认行为,了解它如何与整个体系结构相适应是非常有用的。本指南讨论的原则同样适用于不使用SpringBoot的应用程序。Spring Security 是独立存在的框架.
身份验证和访问控制
应用程序安全性基本可以归结为两个左右的独立的问题:身份验证(您是谁?)授权(你可以做什么?)。有时人们说“访问控制”而不是“授权”,这可能会让人困惑,但这样理解是对的,因为“授权”在其他应用场景中承载着更广泛的意义。Spring Security的体系结构设计为将身份验证与授权分开,并为两者提供了实施策略和扩展点。
身份验证
身份验证的主要策略接口是AuthenticationManager,它只有一个方法:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager可以在其authenticate()方法中执行以下三种操作之一:
1. 如果可以验证输入是一个有效的主体,则返回身份验证(通常authenticated=true)。
2.如果认为输入的是一个无效的主体,则引发AuthenticationException。
3.如果无法决定,则返回null。
AuthenticationException是运行时异常。它通常由应用程序以通用方式处理,这取决于应用程序的类型或用途。换句话说,应用层代码通常不需要捕捉和处理它。例如,一个webui将呈现一个声明身份验证失败的页面,而后端HTTP服务将发送401响应,根据上下文的不同,带有或不带有WWW-Authenticate头。
AuthenticationManager的最常用的实现方式是ProviderManager,它代理AuthenticationProvider实例链。AuthenticationProvider有点像AuthenticationManager,但它有一个额外的方法,允许调用方查询它是否支持给定的身份验证类型:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class> authentication);
}
supports()方法中的参数Class>实际上是Class<?extend Authentication>(只有是supports方法参数支持的类型authenticate()方法才会被调用)。ProviderManager可以通过代理不同的AuthenticationProviders链来支持同一应用程序中的多个不同身份验证机制。如果ProviderManager不识别特定的身份验证实例类型,则将跳过。
ProviderManager有一个可选的父级,如果所有提供程序都返回null,它可以查询该父级。如果父级不可用,则空身份验证将导致AuthenticationException。
有时,应用程序具有受保护资源的逻辑组(例如,与路径模式/api/**)匹配的所有web资源,并且每个组都可以有自己专用的AuthenticationManager。通常,他们中的每一个都是一个ProviderManager,他们共享一个父类。父级则是一种“全局”资源,充当所有提供者的后备。
自定义身份验证管理器
Spring Security提供了一些配置助手,可以快速地在应用程序中设置常见的AuthenticationManager特性。最常用的助手是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是自动填充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");
}
}
(在方法体中用了一个@Override的配置)因此AuthenticationManagerBuilder仅能用于构建一个本地的AuthenticationManager, 属于全局AuthenticationManager的一个分支。 在一个springboot应用程序中,您可以@Autowired注入一个全局的AuthenticationManager,但是对于本地AuthenticationManager,您不能这样做,除非您自己显式地指明它。
SpringBoot提供了一个默认的全局AuthenticationManager(只有一个用户),除非您通过提供自己的AuthenticationManager类型的bean来抢占它。默认值本身足够安全,您不必太担心它,除非您主动需要一个自定义的全局AuthenticationManager。你可以构建任意的AuthenticationManager,用来保护本地资源,而不必担心全局默认的AuthenticationManager。
访问控制或者授权
一旦身份验证成功,我们就可以转到授权,这里的核心策略是AccessDecisionManager。框架提供了三个实现,并且所有三个实现都委托给一个AccessDecisionVoter链,有点像ProviderManager委托给AuthenticationProviders。
AccessDecisionVoter用到Authentication(代表主体)和用ConfigAttributes修饰的安全对象Object:
boolean supports(ConfigAttribute attribute);
boolean supports(Class> clazz);
int vote(Authentication authentication, S object,
Collection
该object在AccessDecisionManager和AccessDecision投票者的签名中是完全通用的—它表示用户可能想要访问的任何内容(web资源或Java类中的方法是两种最常见的情况)。ConfigAttributes也是相当通用的,表示使用一些元数据对安全对象进行装饰,这些元数据确定访问该对象所需的权限级别。ConfigAttribute是一个接口,但它只有一个非常通用的方法,它返回一个字符串,因此这些字符串以某种方式编码资源所有者的意图,表达关于谁可以访问它的规则。典型的ConfigAttribute是用户角色的名称(如role_ADMIN或role_AUDIT),它们通常具有特殊格式(如role_前缀)或表示需要求值的表达式。
大多数人只使用默认的AccessDecisionManager,AffirmactiveBased规则(如果任何投票者肯定地返回,那么访问被授予)。任何定制都会发生在投票者身上,要么添加新的,要么修改现有的工作方式。
使用Spring Expression Language(SpEL)表达式的ConfigAttributes是非常常见的,例如isFullyAuthenticated()&&hasRole('FOO')。AccessDecision投票者支持这一点,它可以处理表达式并为它们创建上下文。要扩展可处理的表达式范围,需要SecurityExpressionRoot的自定义实现,有时还需要SecurityExpressionHandler。
网络安全
web层中的Spring安全性(对于ui和HTTP后端)是基于Servlet过滤器的,因此首先了解过滤器的作用是很有帮助的。下图显示了单个HTTP请求的处理程序的典型分层。
客户端向应用程序发送一个请求,web容器根据请求URI的路径决定调用哪些过滤器和哪个servlet。因为最多一个servlet可以处理一个请求,所以过滤器filter形成了一个链chain,因此它们需要被排序。 事实上,如果过滤器想自己处理请求,它可以否决veto(注意vote的写法)链的后面部分。过滤器还可以修改下游过滤器和servlet中使用的请求和/或响应。过滤器链Filter chain的顺序非常重要,Spring Boot通过两种机制来管理它:一种是过给带有@bean的滤器加@order注解或实现Order接口排序,另一种是它们可以是FilterRegistrationBean的一部分,而FilterRegistrationBean本身就有一个order作为其API的一部分。过滤器在代码内部定义它们自己的常量,以帮助指示它们希望彼此的顺序(例如,Spring Session中的SessionRepositoryFilter的默认顺序是Integer.MIN_VALUE+50,这告诉我们它喜欢处于链的早期,但也不排除其他过滤器会出现在它之前)。
Spring Security整体框架作为一个过滤器filter就安装在这条链条上,它的具体类型是FilterChainProxy,原因很快就会显现出来。在一个Spring Boot应用程序中,安全过滤器security filter是ApplicationContext中的一个@Bean,它是默认安装的,以便应用于每个请求。安装的order位置由SecurityProperties.DEFAULT_FILTER_ORDER定义,然后接着被FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER锚定(如果filer封装request请求并修改其行为,则SpringBoot应用程序期望这些过滤器filter具有的最大次序)。尽管如此,还有更多的东西:从容器的角度来看(from the point of view of the container),SpringSecurity是一个单独的过滤器,但是在它里面有额外的过滤器,每个过滤器都扮演着特殊的角色。这是一张图片:
图2。springsecurity是一个单一的物理过滤器,但它将处理委托给一系列内部过滤器
实际上,在安全过滤器security filter中,不仅只有一层间接层:它经常作为一个DelegatingFilterProxy安装在容器中,不必非得作为spring的bean形式存在。 然而代理委托给FilterChainProxy的过滤器,它总是一个@Bean,通常有一个Spring SecurityFilter Chain的固定名称。FilterChainProxy,它包含所有的安全逻辑,这些逻辑在内部被安排成一个或多个过滤器链。所有过滤器都有相同的API(它们都实现Servlet规范中的Filter接口),并且它们都有机会否决链的其余部分。
在同一个顶级FilterChainProxy中可以有多个过滤器链,所有这些都是由Spring Security管理的,并且所有这些都是容器未知的。SpringSecurity过滤器包含一个过滤器链列表,并向第一个与之匹配的链发送请求。下图显示了基于匹配请求路径(/foo/**比/**先被匹配)的分派。这是非常常见的,但不是唯一匹配请求的方法。这个分派过程最重要的特性是只有一个链处理一个请求。
一个没有被定制过安全配置的普通原生的SpringBoot应用程序一般有几个(称之为n)过滤器链,其中通常n=6。开始部分的(n-1)链只是为了忽略静态资源模式,如/css/**和/images/**,以及error view/error(路径可以由用户通过security.ignored从SecurityProperties配置bean)。最后一个链与捕获到所有的路径 /** 匹配,并且更为活跃,包含身份验证、授权、异常处理、会话处理、头写入等逻辑。默认情况下,此链中共有11个过滤器,但通常用户不必关心使用哪些筛选器以及何时使用。
Spring Security内部的所有过滤器对于容器来说都是未知的这一事实很重要,尤其是在SpringBoot应用程序中,默认情况下,Filter类型的所有@Beans都会自动注册到容器中。因此,如果要向security filter中添加自定义filter,则需要将其设置为@Bean,或者将其包装在FilterRegistrationBean中,并且显式禁用容器自动注册机制。
创建和自定义过滤器链
SpringBoot应用程序(带有/**请求匹配器)中的默认的fallback过滤器链的预定义次序为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/**")
...;
}
}
这个bean将会让Spring Security安全框架在basic_auth过滤器前添加一个新的过滤链。
许多应用程序对一组资源的访问规则与另一组完全不同。例如,承载UI和后台API的应用程序可能支持基于cookie的身份验证(重定向到UI部件的登录页面),以及基于令牌的身份验证(对API部件的未经验证的请求作出401响应)。每组资源都有自己的WebSecurityConfigurerAdapter,具有唯一的顺序和自己的请求匹配器。如果匹配规则重叠,则最早排序的过滤器链将获胜。
调度和授权请求匹配
Security filter安全过滤器链(或着说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();
}
}
配置springsecurity最容易犯的错误之一是忘记了这些匹配器应用于不同的进程,一个是整个过滤器链的请求匹配器,另一个只是选择要应用的访问规则。(匹配器和应用规则要成对出现.)
结合应用程序安全规则和执行规则
如果您用Spring Boot 执行器(就是pom中的依赖包)用于管理端点,那么您可能希望它们是安全的,并且在默认情况下是安全的。事实上,一旦你把执行器添加到一个安全的应用程序中,你就会得到一个附加的过滤器链,它只适用于执行器的端点。它定义了一个只匹配执行器端点的请求匹配器,它有一个ManagementServerProperties.BASIC_AUTH_ORDER 顺序, 比默认的SecurityProperties fallback筛选器少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安全性目前与Servlet API绑定,因此只有在Servlet容器中运行应用程序时,它才真正适用,无论是嵌入的还是其他的。但是,它没有绑定到Spring mvc或spring web堆栈的其余部分,因此它可以用于任何servlet应用程序,例如使用JAX-RS的应用程序。 (意思就是说,Spring Security是一个独立的模块,高内聚,低耦合, 可以应用到任何servelet的容器应用.)
方法安全性
除了支持保护web应用程序之外,Spring Security还提供了对Java方法执行应用访问规则的支持。对于Spring Security来说,这只是一种不同类型的“受保护资源”。对于用户来说,这意味着访问规则是使用相同格式的ConfigAttribute字符串(例如角色或表达式)声明的,但在代码的不同位置。第一步是启用方法安全性,例如在应用程序的顶层配置中:
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}
然后我们可以直接对方法资源进行修饰,例如。
@Service
public class MyService {
@Secured("ROLE_USER")
public String secure() {
return "Hello Security";
}
}
此示例是具有安全方法的服务。如果Spring创建了一个这种类型的@Bean,那么它将被代理,并且调用方必须在方法实际执行之前通过一个安全拦截器。如果调用方拒绝访问,而不是实际的访问结果,则调用方将拒绝访问。
还可以在方法上使用其他注释来实施安全约束,特别是@PreAuthorize和@PostAuthorize,它们允许您编写包含对方法参数和返回值的引用的表达式。
将Web安全性和方法安全性结合起来并不少见。过滤器链提供用户体验特性,如身份验证和重定向到登录页面等,方法安全性在更细粒度的级别提供保护。
使用线程
SpringSecurity基本上是线程绑定的,因为它需要使当前经过身份验证的主体对各种下游用户可用。基本构建块是SecurityContext,它可能包含身份验证(当用户登录时,它将是一个经过显式身份验证的身份验证)。您始终可以通过SecurityContextHolder中的静态方便方法访问和操作SecurityContext,而这些方法又只是简单地操作ThreadLocal,例如。
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);
用户应用程序代码这样做并不常见,但是如果您需要编写一个自定义的身份验证过滤器(尽管在这样的情况下,SpringSecurity中也有一些基类可以用于避免直接使用SecurityContextHolder)。
如果您需要访问web端点中当前经过身份验证的用户,可以在@RequestMapping中使用方法参数。例如。
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
... // do stuff with user
}
此注释将当前身份验证从SecurityContext中拉出,并对其调用getPrincipal()方法以生成方法参数。身份验证中主体的类型取决于用于验证身份验证的AuthenticationManager,因此这是一个有用的小技巧,可以获得对用户数据的类型安全引用。
如果当前Spring Security使用的是HttpServeletRequest中的Principal,因此您也可以直接使用它:
@RequestMapping("/foo")
public String foo(Principal principal) {
Authentication authentication = (Authentication) principal;
User = (User) authentication.getPrincipal();
... // do stuff with user
}
如果您需要编写在不使用springsecurity的情况下工作的代码(对于加载身份验证类,您需要更具防御性),这有时会很有用。
异步处理安全方法
由于SecurityContext是线程绑定的,如果您想进行任何调用安全方法的后台处理,例如使用@Async,您需要确保上下文被传播。这归根结底就是用在后台执行的任务(Runnable、Callable等)包装SecurityContext。SpringSecurity提供了一些帮助程序来简化这一过程,比如Runnable和Callable的包装器。要将SecurityContext传播到@Async方法,需要提供AsyncConfigurer并确保Executor的类型正确:
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}