本指南是Spring Security的入门,它提供了对该框架的设计和基本模块的洞察。仅介绍了应用程序安全性的最基本知识,但是这样做可以清除开发人员在使用Spring Security所遇到的一些困惑。为此,来看看使用过滤器(通常使用方法注释)在Web应用程序中应用安全性的方式。当需要从高层次了解安全应用程序的工作方式,如何自定义它,或者仅需要学习如何考虑应用程序安全性时,请使用本指南。
本指南不是解决最基本的问题(还有其他来源)的手册,但对于初学者和专家都可能有用。在本文中Spring Boot之所以被广泛引用,是因为它为安全的应用程序提供了一些默认行为,并且有助于理解它与整个体系结构之间的关系。所有这些原则同样适用于不使用Spring Boot的应用程序。
身份验证和访问控制
应用程序安全性可以归结为两个独立问题:身份验证(你是谁)和授权(你可以做什么?)。有时人们会说“访问控制”而不是“授权”。Spring Security的体系结构旨在将身份验证与授权分开,并具有策略和扩展点。
认证方式
身份验证的主要策略接口AuthenticationManager只有一个方法:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager接口在它的authenticate()方法中可以执行以下其中之一种情况:
1.如果可以验证输入代表有效的主体,则返回一个Authentication(通常为authenticated=true)。
2.如果认为输入代表无效的主体,则抛出一个AuthenticationException。
3.如果无法决定,则返回null。
AuthenticationException是运行时异常。它通常由应用程序以通用方式处理,具体取决于应用程序的样式或目的。换句话说,通常不希望用户代码捕获并处理它。例如,返回一个页面显示身份验证失败,而后端HTTP服务将发送401响应,有没有WWW认证头取决于上下文。
最常用的实现AuthenticationManager是ProviderManager,它委托一系列AuthenticationProvider实例。一个 AuthenticationProvider有点像一个AuthenticationManager,但是它有一个额外的方法,允许调用者查询是否支持给定Authentication类型:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication);
}
supports()方法中的参数Class<?>实际上是Class<? extends Authentication>(仅询问它是否支持将传递到authenticate()方法中的参数)。一个ProviderManager在同一个应用程序通过委托给AuthenticationProviders程序链可支持多个不同的认证机制。如果一个ProviderManager不能识别特定的身份验证实例类型,该类型将被跳过。
一个ProviderManager具有可选的父级,它能查询是否所有providers都返回null。如果父级不可用,则空身份验证将导致AuthenticationException。有时,一个应用程序具有受保护资源的逻辑组(例如,与路径模式匹配的所有Web资源/api/**),并且每个组可以具有自己专用的AuthenticationManager。通常,每个都是一个ProviderManager,并且它们共享一个父级。因此,父级是一种“全局”资源,充当所有provider的后备。
自定义Authentication Managers
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"); }
}
(使用@Override注解)AuthenticationManagerBuilder仅用于构建“ local” AuthenticationManager,它是全局变量的子级。在Spring Boot应用程序中,你可以@Autowired一个全局变量转换为另一个Bean,但是除非你显式公开它,否则不能对本地的Bean进行转换。
Spring Boot提供了一个默认的全局AuthenticationManager(只有一个用户),可以自定义全局AuthenticationManager,但默认的足够安全,除非需要,可以不必过多考虑。如果你执行任何构建AuthenticationManager的配置,则通常可以在本地对要保护的资源执行该配置,而不必担心全局默认值。
授权或访问控制
认证成功后,我们可以继续进行授权。这里的核心策略是AccessDecisionManager。框架提供了三种实现,所有这三种实现都委托给AccessDecisionVoter链。有点像ProviderManager委托给AuthenticationProviders。
AccessDecisionToverter考虑使用ConfigAttributes装饰的身份验证(表示主体)和安全对象:
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
对象在AccessDecisionManager和AccessDecisionVoter的签名中是完全通用的-它表示用户可能想要访问的任何内容(web资源或Java类中的方法是最常见的两种情况)。ConfigAttributes也是相当通用的,它表示带有一些元数据的安全对象的装饰,这些元数据决定了访问该对象所需的权限级别。ConfigAttribute是一个接口,但它只有一个非常通用的方法,并返回一个字符串,因此这些字符串以某种方式编码资源所有者的意图,表示允许谁访问它的规则。典型的ConfigAttribute是用户角色的名称(如ROLE_ADMIN或ROLE_AUDIT),它们通常具有特殊格式(如ROLE_前缀)或表示需要求值的表达式。
大多数人只使用默认的AccessDecisionManager,它是基于确认的(如果任何voters确认,则授予访问权)。任何自定义都会在voters中发生,要么添加新的,要么修改现有的工作方式。使用作为Spring表达式语言(SpEL)表达式的ConfigAttributes非常常见,例如isFullyAuthenticated() && hasRole('FOO')。AccessDecisionVoter支持此功能,它可以处理表达式并为表达式创建上下文。要扩展可以处理的表达式的范围,需要SecurityExpressionRoot的自定义实现,有时还需要SecurityExpressionHandler。
网络安全
Web层(用于UI和HTTP后端)中的Spring安全性是基于Servlet过滤器的,因此通常先看看过滤器的角色是有帮助的。下图显示了单个HTTP请求的处理程序的典型分层。
客户端向应用程序发送请求,然后容器根据请求URI的路径确定对它应用哪些过滤器和哪个servlet。一个servlet最多只能处理一个请求,但是过滤器形成一个链,因此它们是有序的,实际上,如果过滤器要处理请求本身,则可以否决链的其余部分。过滤器还可以修改下游过滤器和Servlet中使用的请求和/或响应。过滤器链的顺序非常重要,Spring Boot通过两种机制对其进行管理:一种是@Beans类型Filter可以具有@Order或实现Ordered,另一种是它们可以是FilterRegistrationBean的一部分,FilterRegistrationBean本身有一个作为其API一部分的命令。一些现成的过滤器定义了自己的常量,以帮助显示它们的相对顺序(例如.Spring会话中的SessionRepositoryFilter有一个 Integer.MIN_VALUE + 50的DEFAULT_ORDER,这告诉我们,它在链的早期,但不排除其他过滤器之前)。Spring Security作为一个单一过滤器安装在链条上,其具体类型是FilterChainProxy,原因很快就会显现出来。在Spring Boot应用程序中,安全过滤器是ApplicationContext中的一个@Bean,默认情况下会安装它,以便将其应用于每个请求。它安装在SecurityProperties.DEFAULT_FILTER_ORDER所定义的位置,该位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER锚定(Spring Boot应用程序在包装请求并修改其行为时希望筛选器具有的最大顺序)。不过,还有更多的内容:从容器的角度来看,Spring Security是一个单一的过滤器,但是在它内部有额外的过滤器,每个过滤器都扮演着特殊的角色。这是一张图:
Spring Security是一个单独的物理过滤器,但是将处理委托给一系列内部过滤器.实际上,在安全过滤器中还有一个间接层:它通常作为DelegatingFilterProxy安装在容器中,而不必是Spring@Bean。代理委托给FilterChainProxy,它始终是一个@Bean,通常固定名称为springSecurityFilterChain。它是FilterChainProxy,它包含作为过滤器链(或链)在内部排列的所有安全逻辑。所有的过滤器都有相同的API(它们都实现了来自Servlet规范的过滤器接口),并且它们都有机会否决链的其余部分。
这里可以有多个过滤器链,所有这些都由Spring Security在同一顶级FilterChainProxy中管理,并且容器未知。Spring Security过滤器包含一个过滤器链列表,并将请求发送到与之匹配的第一个链。下图显示了基于匹配请求路径的分派(/foo/**matches before/**)。这很常见,但不是匹配请求的唯一方法。这个分派过程最重要的特性是只有一个链处理一个请求。
图3。Spring Security FilterChainProxy将请求发送到匹配的第一个链。
没有自定义安全配置的普通Spring启动应用程序有几个(称为n)过滤器链,通常n=6。第一个(n-1)链只是用来忽略静态资源模式,如/css/**和/images/**,以及错误视图/错误(路径可以由用户通过安全性。忽略从SecurityProperties配置bean)。最后一个链与catch all path/**匹配,并且更为活跃,包含用于身份验证、授权、异常处理、会话处理、头写入等的逻辑。默认情况下,此链中总共有11个筛选器,但通常用户不必关心使用的筛选器和使用的时间。
对于容器来说,Spring安全性内部的所有过滤器都是未知的,这一点很重要,特别是在Spring引导应用程序中,默认情况下,所有类型为Filter的@bean都会自动注册到容器中。因此,如果要将自定义筛选器添加到安全链中,则不需要将其设为@Bean,也不需要将其包装在显式禁用容器注册的FilterRegistrationBean中。
创建和定制过滤器链
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/**") ...;
}
}
这个bean将导致Spring Security添加一个新的过滤器链,并在回退之前对其进行排序。
许多应用程序对一组资源的访问规则与另一组完全不同。例如,承载UI和后台API的应用程序可能支持基于cookie的身份验证,并重定向到UI部分的登录页,以及基于令牌的身份验证,并对API部分的未经身份验证的请求作出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执行器用于管理端点,您可能希望它们是安全的,并且默认情况下它们是安全的。事实上,只要将执行器添加到安全应用程序中,就会得到一个只应用于执行器端点的附加过滤器链。它由一个只匹配执行器端点的请求匹配器定义,其顺序为ManagementServerProperties.BASIC验证顺序它比默认的SecurityProperties回退筛选器少5个,因此在回退之前会先查询它。
如果希望应用程序安全规则应用于执行器终结点,可以添加一个比执行器早排序的筛选器链,并使用包含所有执行器终结点的请求匹配器。如果您更喜欢执行器端点的默认安全设置,那么最简单的事情是在执行器端点之后添加自己的过滤器,但要早于回退(例如. ManagementServerProperties.BASIC验证顺序+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的应用程序
方法安全
除了支持保护web应用程序之外,Spring Security还支持将访问规则应用于Java方法执行。对于Spring安全来说,这只是一种不同类型的“受保护资源”。对于用户,这意味着访问规则是使用相同格式的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安全和方法安全结合起来并不少见。过滤器链提供了用户体验特性,如身份验证和重定向到登录页面等,方法安全性提供了更细粒度的保护。
使用线程
Spring Security从根本上说是线程绑定的,因为它需要使当前经过身份验证的主体对各种下游消费者可用。基本构建块是SecurityContext,它可能包含一个身份验证(当用户登录时,它将是一个经过显式身份验证的身份验证)。您始终可以通过SecurityContextHolder中的静态便利方法访问和操作SecurityContext,而SecurityContextHolder中的静态便利方法又只是操作一个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中提取当前身份验证,并对其调用getPrincipal()方法以生成方法参数。身份验证中主体的类型取决于用于验证身份验证的AuthenticationManager,因此这是一个有用的小技巧,可用于获取对用户数据的类型安全引用。
如果使用的是Spring Security,HttpServletRequest中的主体将是身份验证类型,因此您也可以直接使用它:
@RequestMapping("/foo")
public String foo(Principal principal) {
Authentication authentication = (Authentication) principal; User = (User) authentication.getPrincipal();
... // do stuff with user
}
如果需要编写在不使用Spring Security的情况下可以工作的代码(在加载身份验证类时需要更加防御性),这有时会很有用。
异步处理安全方法
由于SecurityContext是线程绑定的,如果要执行调用安全方法的任何后台处理,例如使用@Async,则需要确保传播上下文。这可以归结为用在后台执行的任务(Runnable、Callable等)包装SecurityContext。Spring Security提供了一些帮助程序来简化这个过程,比如用于Runnable和Callable的包装器。要将SecurityContext传播到@Async方法,需要提供AsyncConfigurer并确保执行器的类型正确:
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}