public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
Spring Security 架构
本指南是 Spring Security 的入门指南,提供了对框架的设计和基本构建块的见解。我们仅介绍了应用程序安全的基础知识。但是,通过这样做,我们可以消除使用 Spring Security 的开发人员遇到的一些困惑。为此,我们通过使用过滤器,更一般地通过使用方法注释,了解了在 Web 应用程序中应用安全的方式。当你需要对安全应用程序的工作原理、如何对其进行自定义或需要了解如何考虑应用程序安全时,请使用本指南。
本指南并非旨在作为解决最基本问题的手册或秘籍(有其他来源可以提供这些信息),但它对初学者和专家都很有用。Spring Boot 也经常被提及,因为它为安全应用程序提供了一些默认行为,了解它如何与整体架构相适应很有用。
注意
|
所有原则同样适用于不使用 Spring Boot 的应用程序。 |
身份验证和访问控制
应用程序安全归结为两个或多或少独立的问题:身份验证(你是谁?)和授权(你能做什么?)。有时人们会用“访问控制”代替“授权”,这可能会令人困惑,但以这种方式考虑它可能会有所帮助,因为“授权”在其他地方被过载。Spring Security 具有旨在将身份验证与授权分开的架构,并具有针对两者都适用的策略和扩展点。
身份验证
用于身份验证的主要策略接口是 AuthenticationManager
,它只有一种方法
AuthenticationManager
可以在其 authenticate()
方法中执行以下 3 项操作之一
-
如果它可以验证输入表示有效的委托人,则返回
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>
(它只会被询问是否支持传递给 authenticate()
方法的内容)。ProviderManager
可以通过委托给 AuthenticationProviders
链在同一个应用程序中支持多种不同的身份验证机制。如果 ProviderManager
不识别特定的 Authentication
实例类型,则会跳过它。
ProviderManager
有一个可选的父级,如果所有提供程序都返回 null
,它可以咨询父级。如果父级不可用,则 null
Authentication
将导致 AuthenticationException
。
有时,应用程序具有受保护资源的逻辑组(例如,与路径模式匹配的所有 Web 资源,例如 /api/**
),并且每个组都可以有自己专用的 AuthenticationManager
。通常,其中每个都是 ProviderManager
,并且它们共享一个父级。然后,父级是一种“全局”资源,充当所有提供程序的备用。

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 应用程序安全如何实现的更多详细信息,请参见 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
将仅用于构建“本地”AuthenticationManager
,它是全局 AuthenticationManager
的子级。在 Spring Boot 应用程序中,您可以将全局 AuthenticationManager
@Autowired
到另一个 bean,但除非您自己显式公开它,否则您无法使用本地 AuthenticationManager
执行此操作。
除非您通过提供自己的 AuthenticationManager
类型 bean 来抢先,否则 Spring Boot 会提供一个默认的全局 AuthenticationManager
(仅有一个用户)。默认情况下,它本身就足够安全,您不必太担心它,除非您确实需要一个自定义的全局 AuthenticationManager
。如果您执行任何构建 AuthenticationManager
的配置,您通常可以在您要保护的资源本地执行此操作,而不用担心全局默认值。
授权或访问控制
一旦身份验证成功,我们就可以继续进行授权,此处的核心策略是 AccessDecisionManager
。该框架提供了三个实现,所有三个实现都委派给 AccessDecisionVoter
实例的链,有点像 ProviderManager
委派给 AuthenticationProviders
。
AccessDecisionVoter
考虑一个 Authentication
(代表主体)和一个安全的 Object
,该 Object
已用 ConfigAttributes
修饰
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
Object
在 AccessDecisionManager
和 AccessDecisionVoter
的签名中是完全通用的。它表示用户可能想要访问的任何内容(Web 资源或 Java 类中的方法是两种最常见的情况)。ConfigAttributes
也相当通用,表示使用一些元数据修饰安全 Object
,这些元数据决定访问它所需的权限级别。ConfigAttribute
是一个接口。它只有一个方法(非常通用且返回 String
),因此这些字符串以某种方式对资源所有者的意图进行编码,表达有关谁可以访问它的规则。典型的 ConfigAttribute
是用户角色的名称(如 ROLE_ADMIN
或 ROLE_AUDIT
),它们通常具有特殊格式(如 ROLE_
前缀)或表示需要评估的表达式。
大多数人使用默认的 AccessDecisionManager
,它是 AffirmativeBased
(如果任何投票者返回肯定,则授予访问权限)。任何自定义都倾向于在投票者中发生,要么通过添加新的投票者,要么通过修改现有投票者的工作方式。
使用 Spring 表达式语言 (SpEL) 表达式的 ConfigAttributes
非常常见,例如 isFullyAuthenticated() && hasRole('user')
。这得到了 AccessDecisionVoter
的支持,它可以处理表达式并为它们创建上下文。要扩展可以处理的表达式的范围,需要自定义实现 SecurityExpressionRoot
,有时还需要 SecurityExpressionHandler
。
Web 安全性
Web 层中的 Spring Security(对于 UI 和 HTTP 后端)基于 Servlet Filters
,因此首先了解 Filters
的作用很有帮助。下图显示了单个 HTTP 请求的处理程序的典型分层。

客户端向应用程序发送请求,容器根据请求 URI 的路径决定哪些过滤器和哪些 servlet 适用于它。最多,一个 servlet 可以处理一个请求,但过滤器形成一个链,因此它们是有序的。事实上,如果过滤器想要自己处理请求,它可以否决链的其余部分。过滤器还可以修改下游过滤器和 servlet 中使用的请求或响应。过滤器链的顺序非常重要,Spring Boot 通过两种机制对其进行管理:类型为 Filter
的 @Beans
可以具有 @Order
或实现 Ordered
,它们可以是 FilterRegistrationBean
的一部分,该 FilterRegistrationBean
本身在其 API 中具有一个顺序。一些现成的过滤器定义了自己的常量,以帮助表明它们希望相对于彼此处于什么顺序(例如,Spring Session 中的 SessionRepositoryFilter
具有 DEFAULT_ORDER
为 Integer.MIN_VALUE + 50
,这告诉我们它希望在链中处于早期,但它并不排除其他过滤器在它之前)。
Spring Security 安装为链中的单个 Filter
,其具体类型为 FilterChainProxy
,原因我们很快就会介绍。在 Spring Boot 应用程序中,安全过滤器是 ApplicationContext
中的 @Bean
,并且默认安装,以便将其应用于每个请求。它安装在由 SecurityProperties.DEFAULT_FILTER_ORDER
定义的位置,而 SecurityProperties.DEFAULT_FILTER_ORDER
又由 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER
锚定(如果 Spring Boot 应用程序期望过滤器包装请求并修改其行为,则为过滤器提供的最大顺序)。但它还有更多内容:从容器的角度来看,Spring Security 是一个单一过滤器,但它内部还有其他过滤器,每个过滤器都发挥着特殊作用。下图显示了这种关系

Filter
,但将处理委托给内部过滤器链
事实上,安全过滤器中甚至还有一层间接:它通常作为 DelegatingFilterProxy
安装在容器中,它不必是 Spring @Bean
。代理委托给 FilterChainProxy
,它始终是 @Bean
,通常具有固定的名称 springSecurityFilterChain
。正是 FilterChainProxy
包含所有安全逻辑,在内部按过滤器链(或链)排列。所有过滤器具有相同的 API(它们都实现了 Servlet 规范中的 Filter
接口),并且它们都有机会否决链的其余部分。
在同一个顶级 FilterChainProxy
中,Spring Security 可以管理多个过滤器链,并且容器不知道所有这些过滤器链。Spring Security 过滤器包含过滤器链列表,并将请求分派到与之匹配的第一个链。下图显示了基于匹配请求路径(/foo/**
在 /**
之前匹配)的分派过程。这是非常常见的,但不是匹配请求的唯一方法。此分派过程最重要的特性是,只有一个链可以处理请求。

FilterChainProxy
将请求分派到与之匹配的第一个链。
没有自定义安全配置的普通 Spring Boot 应用程序有几个(称之为 n)过滤器链,其中通常 n=6。前 (n-1) 个链只是为了忽略静态资源模式,如 /css/**
和 /images/**
,以及错误视图:/error
。(用户可以使用 SecurityProperties
配置 bean 中的 security.ignored
控制路径。)最后一个链与 catch-all 路径(/**
)匹配,并且更加活跃,包含用于身份验证、授权、异常处理、会话处理、标头写入等的逻辑。默认情况下,此链中总共有 11 个过滤器,但通常用户不必关心使用哪些过滤器以及何时使用。
注意
|
所有内部于 Spring Security 的过滤器对容器都是未知的,这一点很重要,尤其是在 Spring Boot 应用程序中,在 Spring Boot 应用程序中,默认情况下,所有类型为 Filter 的 @Beans 都会自动向容器注册。因此,如果你想向安全链添加自定义过滤器,则需要使其不成为 @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("/match1/**")
...;
}
}
此 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("/match1/**")
.authorizeRequests()
.antMatchers("/match1/user").hasRole("USER")
.antMatchers("/match1/spam").hasRole("SPAM")
.anyRequest().isAuthenticated();
}
}
在配置 Spring Security 时最容易犯的一个错误是忘记这些匹配器适用于不同的进程。一个是整个过滤器链的请求匹配器,另一个仅用于选择要应用的访问规则。
将应用程序安全规则与执行器规则相结合
如果你将 Spring Boot 执行器用于管理端点,你可能希望它们是安全的,并且默认情况下它们是安全的。事实上,一旦你将执行器添加到安全应用程序,你就会获得一个仅适用于执行器端点的附加过滤器链。它使用仅匹配执行器端点的请求匹配器进行定义,并且具有 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 的应用程序。 |
方法安全性
除了支持保护 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
,则会对其进行代理,并且在实际执行方法之前,调用者必须通过安全拦截器。如果访问被拒绝,调用者将获得 AccessDeniedException
,而不是实际的方法结果。
还有其他注释可以用于方法来强制执行安全约束,特别是 @PreAuthorize
和 @PostAuthorize
,它们允许你编写包含对方法参数和返回值的引用的表达式。
提示
|
将 Web 安全性和方法安全性结合起来并不少见。过滤器链提供用户体验功能,例如身份验证和重定向到登录页面等,而方法安全性提供更精细级别的保护。 |
使用线程
Spring Security 从根本上来说是线程绑定的,因为它需要向各种下游使用者提供当前经过身份验证的主体。基本构建模块是 SecurityContext
,它可能包含一个 Authentication
(当用户登录时,它是一个明确 authenticated
的 Authentication
)。你始终可以通过 SecurityContextHolder
中的静态便捷方法来访问和操作 SecurityContext
,而 SecurityContextHolder
又会操作一个 ThreadLocal
。以下示例显示了这样的安排
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
)的任何后台处理,则需要确保传播上下文。这归结为使用在后台执行的任务(Runnable
、Callable
等)包装 SecurityContext
。Spring Security 提供了一些帮助程序来简化此操作,例如 Runnable
和 Callable
的包装器。要将 SecurityContext
传播到 @Async
方法,你需要提供一个 AsyncConfigurer
并确保 Executor
为正确类型
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}