应用程序安全性归结为或多或少的两个独立问题:身份验证(你是谁?)和授权(你可以做什么?)。有时人们会说 “访问控制” 而不是 “授权”,这可能会造成混淆,但是以这种方式思考可能会有所帮助,因为 “授权” 在其他地方超载。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>
(仅会询问它是否支持将传递到 authenticate()
方法中的内容)。通过委派给 AuthenticationProviders
链,ProviderManager
可以在同一应用中支持多种不同的身份验证机制。如果 ProviderManager
无法识别特定的身份验证实例类型,则将跳过该类型。
ProviderManager
具有可选的父级,如果所有供应程序都返回 null
,则可以咨询该父级。如果父级不可用,则 null
Authentication
将导致 AuthenticationException
。
有时,应用具有逻辑组的受保护资源(例如,与路径模式 /api/**
匹配的所有 Web 资源),并且每个组可以具有自己的专用 AuthenticationManager
。通常,每一个都是 ProviderManager
,它们共享一个父级。因此,父级是一种 “全局” 资源,充当所有供应程序的后备。
图片 1. 使用 ProviderManager
的 AuthenticationManager
层次结构
自定义身份验证管理器
Spring Security 提供了一些配置助手,可以快速获取在应用中设置的通用身份验证管理器功能。最常用的帮助程序是 AuthenticationManagerBuilder
,它非常适合设置内存中的 JDBC 或 LDAP 用户详细信息,或者用于添加自定义 UserDetailService
。这是配置全局(父)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
仅用于构建 “本地” AuthenticationManager
,它是全局控件的子级。在 Spring Boot 应用中,你可以 @Autowired
,将全局的一个 bean 倒入另一个。但是除非你自己显示公开它,否则不能对本地的 Bean 执行该操作。
Spring Boot 提供了一个默认的全局 AuthenticationManager
(只有一个用户),除非你通过提供自己的 AuthenticationManager
类型的 bean 来抢占它。除非你主动需要自定义的全局 AuthenticationManager
,否则默认值本身就足够安全,你不必担心太多。如果执行任何构建 AuthenticationManager
的配置,则通常可以在本地对要保护的资源进行配置,而不必担心全局默认值。
授权或访问控制
身份验证成功后,我们可以继续进行授权,这里的核心策略是 AccessDecisionManager
。框架提供了三种实现,所有三个实现都委托给 AccessDecisionVoter
链,有点像 ProviderManager
委托给 AuthenticationProviders
。
AccessDecisionVoter
考虑使用 ConfigAttributes
修饰的 Authentication
(代表主体)及安全 Object
:
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)表达式的 ConfigAttribute
非常常见,例如 isFullyAuthenticated() && hasRole('FOO')
。 AccessDecisionVoter
支持该功能,可以处理表达式并为其创建上下文。为了扩展可以处理的表达式的范围,需要 SecurityExpressionRoot
的自定义实现,有时还需要 SecurityExpressionHandler
。