本节讨论基于Servlet的应用程序中的Spring Security高层架构。我们在引用的“身份验证、授权、防止攻击保护”部分中构建这种高层次的理解。
Filters
的回顾Spring Security的Servlet支持基于Servlet Filter
,因此首先了解Filter
的作用是有帮助的。下图显示了单个HTTP请求的处理程序的典型分层。
客户端向应用程序发送请求,容器创建一个FilterChain
,其中包含Filter
和Servlet
,该Servlet
应该根据请求URI的路径处理HttpServletRequest
。在Spring MVC应用程序中,Servlet
是DispatcherServlet
的一个实例。一个Servlet
最多只能处理一个HttpServletRequest
和HttpServletResponse
。然而,可以使用多个Filter
:
Filter
或Servlet
被调用。在这个实例中,Filter
通常会编写HttpServletResponse
。Filter
和Servlet
使用的HttpServletRequest
或HttpServletResponse
Filter
的力量来自传递给它的FilterChain
。
FilterChain用法示例
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
由于Filter
只影响下游Filter
和Servlet
,所以调用每个Filter
的顺序非常重要。
Spring提供了一个名为DelegatingFilterProxy
的过滤器实现,它允许在Servlet容器的生命周期和Spring的ApplicationContext
之间建立桥接。Servlet容器允许使用自己的标准注册Filter
,但是它不知道Spring定义的bean。可以通过标准的Servlet容器机制注册DelegatingFilterProxy
,但将所有工作委托给实现Filter
的Spring Bean。
下面是关于DelegatingFilterProxy
如何适合Filter
和FilterChain
的图片。
DelegatingFilterProxy
从ApplicationContext
查找Bean Filter0,然后调用Bean Filter0。DelegatingFilterProxy
的伪代码如下所示。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
DelegatingFilterProxy
的另一个好处是它允许延迟查找Filter
bean实例。这一点很重要,因为容器需要在启动之前注册Filter
实例。然而,Spring通常使用ContextLoaderListener
来加载Spring bean,直到需要注册Filter
实例之后才会加载。
Spring Security的Servlet支持包含在FilterChainProxy
中。FilterChainProxy
是Spring Security提供的一个特殊Filter
,它允许通过SecurityFilterChain
委托给许多Filter
实例。因为FilterChainProxy
是一个Bean,所以它通常封装在DelegatingFilterProxy
中。
SecurityFilterChain
由FilterChainProxy
用来确定应该为这个请求调用哪个Spring安全Filter
。
SecurityFilterChain
中的安全过滤器通常是bean,但它们在FilterChainProxy
中注册,而不是在DelegatingFilterProxy
中注册。FilterChainProxy
提供了许多直接注册Servlet容器或DelegatingFilterProxy
的优点。首先,它为所有Spring Security的Servlet支持提供了一个起点。由于这个原因,如果您试图排除Spring Security的Servlet支持的故障,那么在FilterChainProxy
中添加一个调试点是一个很好的起点。
其次,由于FilterChainProxy
是Spring安全使用的核心,它可以执行非可选的任务。例如,它清除SecurityContext
以避免内存泄漏。它还应用Spring Security的HttpFirewall
来保护应用程序免受某些类型的攻击。
此外,它在确定何时应该调用SecurityFilterChain
方面提供了更大的灵活性。在Servlet容器中,只根据URL调用Filter
。然而,FilterChainProxy
可以通过利用RequestMatcher
接口,基于HttpServletRequest
中的任何内容来确定调用。
事实上,FilterChainProxy
可以用来确定应该使用哪个SecurityFilterChain
。这允许为应用程序的不同部分提供完全独立的配置。
在多个SecurityFilterChain
图中,FilterChainProxy
决定使用哪个SecurityFilterChain
。只有第一个匹配的SecurityFilterChain
才会被调用。如果请求的URL为/api/messages/
,它将首先匹配SecurityFilterChain0
的模式/api/**
,所以只有SecurityFilterChain0
会被调用,即使它也匹配SecurityFilterChainn
。如果URL/messages/
被请求,它将不匹配SecurityFilterChain0
的模式/api/**
,所以FilterChainProxy
将继续尝试每个SecurityFilterChain
。假设没有其他匹配SecurityFilterChain的实例,SecurityFilterChainn
将被调用。
注意SecurityFilterChain0
只配置了三个安全Filter
实例。但是,SecurityFilterChainn
配置了四个安全Filter
。需要注意的是,每个SecurityFilterChain
都可以是唯一的,并且可以单独配置。实际上,如果应用程序希望Spring Security忽略某些请求,SecurityFilterChain
可能没有任何安全Filter
。
安全过滤器通过SecurityFilterChain API插入到FilterChainProxy中。过滤器的顺序很重要。通常不需要知道Spring Security Filter
的顺序。然而,有时候知道顺序是有益的
以下是Spring安全过滤器排序的全面列表:
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter
ExceptionTranslationFilter允许将AccessDeniedException和AuthenticationException转换为HTTP响应。
将ExceptionTranslationFilter
作为安全过滤器之一插入到FilterChainProxy中。
首先,ExceptionTranslationFilter
调用FilterChain.doFilter(request, response)
来调用应用程序的其余部分。
如果用户未被认证,或者是AuthenticationException
,则启动认证。
SecurityContextHolder
被清除
HttpServletRequest
保存在RequestCache
中。当用户成功身份验证时,将使用RequestCache
重播原始请求。
AuthenticationEntryPoint
用于从客户端请求凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate
标头。
否则,如果是AccessDeniedException
,则拒绝访问。调用AccessDeniedHandler
来处理拒绝访问。
如果应用程序没有抛出
AccessDeniedException
或AuthenticationException
,那么ExceptionTranslationFilter
不会做任何事情。
ExceptionTranslationFilter
的伪代码是这样的:
ExceptionTranslationFilter伪代码
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
FilterChain.doFilter(request, response)
等价于调用应用程序的其余部分。FilterSecurityInterceptor
或method security)抛出AuthenticationException
或AccessDeniedException
,它将在这里被捕捉和处理。AuthenticationException
,则启动认证。Spring Security为身份验证提供了全面的支持。本节讨论:
架构组件
本节描述在Servlet身份验证中使用Spring Security的主要体系结构组件。如果您需要解释这些部分如何组合在一起的具体流,请查看身份验证机制特定部分。
SecurityContextHolder
是Spring Security存储被身份验证的详细信息的地方。SecurityContextHolder
获得,包含当前认证用户的Authentication
。AuthenticationManager
的输入,以提供用户提供的用于身份验证的凭据,也可以是SecurityContext
中的当前用户。Authentication
主体的权限(即角色、范围等)。AuthenticationManager
最常见的实现。ProviderManager
用于执行特定类型的身份验证。AuthenticationEntryPoint
请求凭证——用于从客户端请求凭证(例如,重定向到登录页面,发送一个WWW-Authenticate
响应,等等)Filter
。这也很好地了解了高层次的身份验证流程以及各个部件如何一起工作。用户认证机制
Spring Security身份验证模型的核心是SecurityContextHolder
。它包含SecurityContext
。
SecurityContextHolder
是Spring Security存储身份验证的详细信息的地方。Spring Security并不关心如何填充SecurityContextHolder
。如果它包含一个值,那么它将被用作当前经过身份验证的用户。
表示用户已经过身份验证的最简单方法是直接设置SecurityContextHolder
。
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
SecurityContext
。SecurityContext
实例,而不是使用SecurityContextHolder.getContext().setAuthentication(Authentication)
来避免多个线程之间的竞争条件。Authentication
对象。SecurityContext
上设置了什么类型的Authentication
实现。TestingAuthenticationToken
,因为它非常简单。UsernamePasswordAuthenticationToken(userDetails、password、authorities)
。SecurityContextHolder
上设置SecurityContext
。如果希望获得关于经过身份验证的主体的信息,可以通过访问SecurityContextHolder
来实现。
访问当前认证用户
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection authorities = authentication.getAuthorities();
默认情况下,SecurityContextHolder
使用ThreadLocal
来存储这些详细信息,这意味着SecurityContext
对于同一个线程中的方法总是可用的,即使SecurityContext
没有显式地作为参数传递给这些方法。以这种方式使用ThreadLocal
是相当安全的,如果在处理当前主体的请求后小心地清除线程。Spring Security的FilterChainProxy
确保SecurityContext
总是被清除。
有些应用程序并不完全适合使用ThreadLocal
,因为它们处理线程的特定方式。例如,Swing客户机可能希望Java虚拟机中的所有线程都使用相同的安全上下文。SecurityContextHolder
可以在启动时配置一个策略,以指定您希望如何存储上下文。对于独立的应用程序,您将使用SecurityContextHolder.MODE_GLOBAL
策略。其他应用程序可能希望安全线程生成的线程也采用相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
实现的。您可以更改默认的SecurityContextHolder.MODE_THREADLOCAL
模式有两种方式。第一个是设置系统属性,第二个是调用SecurityContextHolder
上的静态方法。大多数应用程序不需要改变默认设置,但如果需要,请查看SecurityContextHolder
的JavaDoc以了解更多信息。
SecurityContext
从SecurityContextHolder
中获取。SecurityContext
包含一个Authentication
对象。
在Spring Security中,Authentication
有两个主要目的:
AuthenticationManager
的输入,用于提供用户为进行身份验证而提供的凭据。在此场景中使用时,isAuthenticated()
返回false
。Authentication
可以从SecurityContext
中获取。Authentication
包含:
principal
—标识用户。当使用用户名/密码进行身份验证时,这通常是UserDetails
的一个实例。credentials
—通常是密码。在许多情况下,这将在用户身份验证后清除,以确保不会泄漏。authorities
—GrantedAuthority
为用户被授予的高级权限。一些例子是角色或作用域。GrantedAuthority
为用户被授予的高级权限。一些例子是角色或作用域。
GrantedAuthority
可以从Authentication.getAuthorities()
方法获得。此方法提供了一个GrantedAuthority
对象集合。GrantedAuthority
是授予主体的权限,这并不奇怪。这样的权限通常是“角色”,例如ROLE_ADMINISTRATOR
或ROLE_HR_SUPERVISOR
。稍后将为web授权、方法授权和域对象授权配置这些角色。Spring Security的其他部分能够解释这些权限,并期望它们存在。当使用基于用户名/密码的身份验证时,GrantedAuthority
通常由UserDetailsService加载。
通常,GrantedAuthority
对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,您不太可能拥有一个GrantedAuthority
来表示对Employee
对象编号54的权限,因为如果有数千个这样的权限,您将很快耗尽内存(或者至少导致应用程序花很长时间来验证用户)。当然,Spring Security是专门设计来处理这一常见需求的,但是您可以使用项目的域对象安全功能来实现这一目的。
AuthenticationManager
是定义Spring Security的过滤器如何执行身份验证的API。然后,调用AuthenticationManager
的控制器(即Spring Security的Filters
)在SecurityContextHolder上设置返回的Authentication。如果你没有集成Spring Security的过滤器,你可以直接设置SecurityContextHolder
,而不需要使用AuthenticationManager
。
虽然AuthenticationManager
的实现可以是任何东西,但最常见的实现是ProviderManager。
ProviderManager是AuthenticationManager最常用的实现。ProviderManager
委托给AuthenticationProvider
列表。每个AuthenticationProvider
都有机会表明身份验证应该是成功的,失败的,或者表明它不能做出决定,并允许下游的AuthenticationProvider
来做出决定。如果没有配置AuthenticationProvider
可以进行身份验证,然后用ProviderNotFoundException
身份验证失败,是一种特殊的AuthenticationException
表明ProviderManager
没有配置为支持传递给它的Authentication
类型。
实际上,每个AuthenticationProvider
都知道如何执行特定类型的身份验证。例如,一个AuthenticationProvider
可能能够验证用户名/密码,而另一个AuthenticationProvider
可能能够验证SAML断言。这允许每个AuthenticationProvider
执行特定类型的身份验证,同时支持多种类型的身份验证,并且只公开一个AuthenticationManager
bean。
ProviderManager
还允许配置一个可选的父AuthenticationManager
,当AuthenticationProvider
不能执行身份验证时,会咨询该父AuthenticationManager
。父类可以是任何类型的AuthenticationManager
,但它通常是ProviderManager
的一个实例。
事实上,多个ProviderManager
实例可能共享相同的父AuthenticationManager
。这在多个SecurityFilterChain实例具有某些共同身份验证(共享的父类AuthenticationManager
)和不同身份验证机制(不同的ProviderManager
实例)的场景中有些常见。
多个AuthenticationProvider可以被注入到ProviderManager中。每个AuthenticationProvider
执行特定类型的身份验证。例如,DaoAuthenticationProvider支持基于用户名/密码的身份验证,而JwtAuthenticationProvider
支持验证JWT令牌。
AuthenticationEntryPoint
请求凭据AuthenticationEntryPoint用于发送HTTP响应,该响应从客户端请求凭据。
有时,客户端会主动包含用户名/密码等凭据来请求资源。在这些情况下,Spring Security不需要提供从客户机请求凭据的HTTP响应,因为它们已经包含在内。
在其他情况下,客户端将向未被授权访问的资源发出未经身份验证的请求。在本例中,AuthenticationEntryPoint
的实现用于从客户端请求凭据。AuthenticationEntryPoint
实现可能执行重定向到一个登录页面,响应一个WWW-Authenticate
头,等等。
AbstractAuthenticationProcessingFilter被用作验证用户凭据的基本Filter
。在验证凭据之前,Spring Security通常使用AuthenticationEntryPoint请求凭据。
接下来,AbstractAuthenticationProcessingFilter
可以验证提交给它的任何身份验证请求。
AbstractAuthenticationProcessingFilter
从HttpServletRequest
创建一个Authentication来进行身份验证。创建的身份验证类型依赖于AbstractAuthenticationProcessingFilter
的子类。例如,UsernamePasswordAuthenticationFilter从HttpServletRequest
中提交的用户名和密码创建UsernamePasswordAuthenticationToken
。Failure
RememberMeServices.loginFail
被调用。如果记住我没配置,这是空操作。AuthenticationFailureHandler
被调用。Success
。SessionAuthenticationStrategy
会在新登录时得到通知。SecurityContextPersistenceFilter
将SecurityContext
保存到HttpSession
。RememberMeServices.loginSuccess
被调用。如果记住我没配置,这是空操作。ApplicationEventPublisher
发布一个InteractiveAuthenticationSuccessEvent
。AuthenticationSuccessHandler
被调用。验证用户身份的最常见方法之一是验证用户名和密码。因此,Spring Security提供了对使用用户名和密码进行身份验证的全面支持。
读取用户名和密码
Spring Security提供了以下内置机制来从HttpServletRequest
读取用户名和密码:
存储机制
每种受支持的读取用户名和密码的机制都可以利用任何受支持的存储机制:
Spring Security支持通过html表单提供用户名和密码。本节详细介绍基于表单的身份验证如何在Spring Security中工作。
让我们看看基于表单的登录是如何在Spring Security中工作的。首先,我们将看到如何将用户重定向到登录表单。
/private
发出未经身份验证的请求。FilterSecurityInterceptor
通过抛出AccessDeniedException
来拒绝未经身份验证的请求。ExceptionTranslationFilter
将启动Start Authentication并发送一个重定向到配置了AuthenticationEntryPoint
的登录页面。在大多数情况下,AuthenticationEntryPoint
是LoginUrlAuthenticationEntryPoint
的一个实例。提交用户名和密码后,UsernamePasswordAuthenticationFilter
将对用户名和密码进行验证。UsernamePasswordAuthenticationFilter
扩展了AbstractAuthenticationProcessingFilter
,所以这个图看起来应该很相似。
该图构建了我们的SecurityFilterChain
图。
UsernamePasswordAuthenticationFilter
通过从HttpServletRequest
中提取用户名和密码创建UsernamePasswordAuthenticationToken
,这是一种Authentication
类型。UsernamePasswordAuthenticationToken
传递到AuthenticationManager
中进行身份验证。AuthenticationManager
的详细信息取决于用户信息的存储方式。SecurityContextHolder
。RememberMeServices.loginFail
被调用。如果记住我没配置,这是空操作。AuthenticationFailureHandler
被调用。SessionAuthenticationStrategy
会在新登录时得到通知。SecurityContextPersistenceFilter
将SecurityContext
保存到HttpSession
。RememberMeServices.loginSuccess
被调用。如果记住我没配置,这是空操作。ApplicationEventPublisher
发布一个InteractiveAuthenticationSuccessEvent
。AuthenticationSuccessHandler
被调用。通常这是一个SimpleUrlAuthenticationSuccessHandler
,当我们重定向到登录页面时,它将重定向到ExceptionTranslationFilter
保存的请求。Spring Security表单登录在默认情况下是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供基于表单的登录。一个最小的、显式的Java配置可以在下面找到:
protected void configure(HttpSecurity http) {
http
// ...
.formLogin(withDefaults());
}
在这个配置中,Spring Security将呈现一个默认的登录页面。大多数生产应用程序都需要一个自定义的登录表单。
下面的配置演示了如何在表单中提供自定义日志。
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
}
当在Spring Security配置中指定登录页面时,您将负责呈现该页面。下面是一个Thymeleaf模板,它生成一个HTML登录表单,符合/login登录页面:
src/main/resources/templates/login.html
Please Log In
Please Log In
Invalid username and password.
You have been logged out.
关于默认HTML表单有几个关键点:
post
到/login
username
的参数中指定用户名password
的参数中指定密码许多用户只需要定制登录页面。但是,如果需要的话,上面的一切都可以通过额外的配置进行定制。
如果您正在使用Spring MVC,您将需要一个控制器,将GET /login
映射到我们创建的登录模板。LoginController
的最小示例如下:
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}
本节详细介绍Spring Security如何为基于servlet的应用程序提供对基本HTTP身份验证的支持。
让我们看看HTTP基本身份验证是如何在Spring Security中工作的。首先,我们看到WWW-Authenticate头被发回给一个未经过身份验证的客户端。
该图构建了我们的SecurityFilterChain图。
/private
发出未经身份验证的请求。AccessDeniedException
来拒绝未经身份验证的请求。ExceptionTranslationFilter
将启动开始身份验证。配置的AuthenticationEntryPoint
是一个BasicAuthenticationEntryPoint
的实例,它发送一个WWW-Authenticate报头。RequestCache
通常是一个不保存请求的NullRequestCache
,因为客户机能够重放它最初请求的请求。当客户端接收到WWW-Authenticate报头时,它知道应该用用户名和密码重试。下面是正在处理的用户名和密码的流程。
该图构建了我们的SecurityFilterChain
图。
BasicAuthenticationFilter
通过从HttpServletRequest
中提取用户名和密码来创建UsernamePasswordAuthenticationToken
,这是一种Authentication
类型。UsernamePasswordAuthenticationToken
传递到AuthenticationManager
中进行身份验证。AuthenticationManager
的详细信息取决于用户信息的存储方式。SecurityContextHolder
。RememberMeServices.loginFail
被调用。如果记住我没配置,这是空操作。AuthenticationEntryPoint
被调用来触发WWW-Authenticate再次发送。SessionAuthenticationStrategy
会在新登录时得到通知。RememberMeServices.loginSuccess
被调用。如果记住我没配置,这是空操作。BasicAuthenticationFilter
调用FilterChain.doFilter(request,response)
来继续应用程序逻辑的其余部分。默认情况下,Spring Security的HTTP基本身份验证支持是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供HTTP Basic。
一个最小的,显式的配置可以找到如下:
protected void configure(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
}
本节详细介绍Spring Security如何提供摘要身份验证支持,摘要身份验证是由DigestAuthenticationFilter
提供的。
您不应该在现代应用程序中使用摘要身份验证,因为它被认为不安全。最明显的问题是必须以明文、加密或MD5格式存储密码。所有这些存储格式都是不安全的。相反,您应该使用单向自适应密码散列(即bCrypt, PBKDF2, SCrypt等)存储凭证,这是摘要认证不支持的。
摘要身份验证试图解决基本身份验证的许多弱点,特别是通过确保凭证永远不会通过网络以明文发送。许多浏览器支持摘要身份验证。
管理HTTP摘要身份验证的标准由RFC 2617定义,它更新了RFC 2069规定的摘要身份验证标准的早期版本。大多数用户代理实现RFC 2617。Spring Security Digest Authentication支持与RFC 2617规定的认证质量保护(qop
)兼容,它还提供了与RFC 2069的向后兼容性。摘要身份验证被认为是一个更有吸引力的选择,如果你需要使用未加密的HTTP(即没有TLS/HTTPS),并希望最大限度地安全性的身份验证过程。然而,每个人都应该使用HTTPS。
摘要身份验证的中心是一个“nonce”。这是服务器生成的值。Spring Security的nonce采用以下格式:
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime: The date and time when the nonce expires, expressed in milliseconds
key: A private key to prevent modification of the nonce token
你需要确保你配置了不安全的明文密码存储使用NoOpPasswordEncoder
。以下是使用Java配置配置摘要认证的示例:
@Autowired
UserDetailsService userDetailsService;
DigestAuthenticationEntryPoint entryPoint() {
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
result.setRealmName("My App Relam");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}
DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
}
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
.addFilterBefore(digestFilter());
}
Spring Security的InMemoryUserDetailsManager
实现了UserDetailsService,以支持在内存中检索的基于用户名/密码的身份验证。InMemoryUserDetailsManager
通过实现UserDetailsManager
接口来提供对UserDetails
的管理。当Spring Security配置为接受用户名/密码进行身份验证时,将使用基于UserDetails
的身份验证。
在这个示例中,我们使用Spring Boot CLI对password
的密码进行编码,并获得编码后的密码{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW
。
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
上面的示例以安全的格式存储密码,但在入门经验方面还有很多不足之处。
在下面的示例中,我们利用了User.withDefaultPasswordEncoder来确保存储在内存中的密码是受保护的。但是,它不能通过反编译源代码来防止获得密码。出于这个原因,User.withDefaultPasswordEncoder
应该只用于“开始”,而不是用于生产。
@Bean
public UserDetailsService users() {
// The builder will ensure the passwords are encoded before saving in memory
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
没有简单的方法来使用User.withDefaultPasswordEncoder
基于XML的配置。对于演示或刚刚开始,您可以选择在密码前加上{noop}
,以表示不应该使用编码。
Spring Security的JdbcDaoImpl
实现了UserDetailsService
来提供对使用JDBC检索的基于用户名/密码的身份验证的支持。JdbcUserDetailsManager
扩展了JdbcDaoImpl
,通过UserDetailsManager
接口提供对UserDetails
的管理。当Spring Security配置为接受用户名/密码进行身份验证时,将使用基于UserDetails
的身份验证。
Spring Security为基于JDBC的身份验证提供默认查询。本节提供与默认查询相对应的默认模式。您将需要调整模式,以匹配与您正在使用的查询和数据库方言相匹配的定制。
JdbcDaoImpl
需要表来加载用户的密码、帐户状态(启用或禁用)和权限(角色)列表。需要的默认模式可以在下面找到。
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
如果您的应用程序利用组,您将需要提供组模式。组的默认模式可以在下面找到。
create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);
在配置JdbcUserDetailsManager
之前,必须创建一个DataSource
。在我们的示例中,我们将设置一个使用默认用户模式初始化的嵌入式数据源。
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(H2)
.addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
.build();
}
在这个示例中,我们使用Spring Boot CLI对password
的密码进行编码,并获得编码后的密码{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW
。有关如何存储密码的更多细节,请参阅PasswordEncoder一节。
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
}
UserDetails
由UserDetailsService
返回。DaoAuthenticationProvider
验证UserDetails
,然后返回一个Authentication
,该Authentication
有一个主体,该主体是由已配置的UserDetailsService
返回的UserDetails
。
DaoAuthenticationProvider
使用UserDetailsService
检索用户名、密码和其他属性,以验证用户名和密码。Spring Security提供了UserDetailsService
的内存和JDBC实现。
您可以通过将自定义UserDetailsService
公开为bean来定义自定义身份验证。例如,以下将自定义身份验证,假设CustomUserDetailsService
实现了UserDetailsService
:
只有当
AuthenticationManagerBuilder
没有被填充并且AuthenticationProviderBean
没有被定义时才会使用。
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
Spring Security的servlet通过与PasswordEncoder
集成来支持安全存储密码。定制Spring Security使用的PasswordEncoder
实现可以通过公开PasswordEncoder Bean来完成。
DaoAuthenticationProvider是一个AuthenticationProvider实现,它利用UserDetailsService和PasswordEncoder来验证用户名和密码。
让我们看看DaoAuthenticationProvider
是如何在Spring Security中工作的。图中解释了读取用户名和密码中的AuthenticationManager如何工作的细节。
Filter
将UsernamePasswordAuthenticationToken
传递给AuthenticationManager
,这是由ProviderManager
实现的。ProviderManager
被配置为使用DaoAuthenticationProvider
类型的AuthenticationProvider
。DaoAuthenticationProvider
从UserDetailsService
中查找UserDetails
。DaoAuthenticationProvider
使用PasswordEncoder
验证上一步返回的UserDetails
上的密码。UsernamePasswordAuthenticationToken
,并且具有一个主体,该主体是由已配置的UserDetailsService
返回的UserDetails
。最终,返回的UsernamePasswordAuthenticationToken
将由身份验证Filter
在SecurityContextHolder
上设置。LDAP经常被组织用作用户信息的中心存储库和身份验证服务。它还可以用于存储应用程序用户的角色信息。
当Spring Security被配置为接受用户名/密码进行身份验证时,Spring Security将使用基于LDAP的身份验证。但是,尽管利用用户名/密码进行身份验证,但它并没有使用UserDetailsService
集成,因为在绑定身份验证中,LDAP服务器没有返回密码,因此应用程序不能执行密码验证。
对于如何配置LDAP服务器,有许多不同的场景,因此Spring Security的LDAP提供者是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供可以配置为处理各种情况的缺省实现。
在尝试将LDAP与Spring Security一起使用之前,您应该熟悉LDAP。下面的链接很好地介绍了相关的概念,并提供了使用免费LDAP服务器OpenLDAP设置目录的指南:https://www.zytrax.com/books/ldap/。熟悉一些用于从Java访问LDAP的JNDI api可能也很有用。我们在LDAP提供程序中没有使用任何第三方LDAP库(Mozilla、JLDAP等),但是Spring LDAP得到了广泛的使用,所以如果您计划添加自己的自定义,对该项目有所了解可能会有所帮助。
在使用LDAP身份验证时,一定要确保正确配置LDAP连接池。如果您不熟悉如何做到这一点,可以参考Java LDAP文档。
您需要做的第一件事是确保有一个LDAP Server来指向您的配置。为简单起见,最好从嵌入式LDAP Server开始。Spring Security支持使用以下任意一种:
在下面的示例中,我们将下面的内容公开为users.ldif
作为类路径资源,以初始化嵌入的LDAP服务器,其中用户user
和admin
的密码都是password
。
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password
dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password
dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org
dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
待补充
HTTP会话相关的功能是通过SessionManagementFilter
和SessionAuthenticationStrategy
接口的组合来处理的,该接口由过滤器委托给它。典型的应用包括会话固定保护、攻击预防、会话超时检测和限制通过身份验证的用户可以同时打开的会话数量。
您可以配置Spring Security来检测无效会话ID的提交,并将用户重定向到适当的URL。这是通过session-management
元素实现的:
...
注意,如果您使用这种机制来检测会话超时,如果用户登出,然后在没有关闭浏览器的情况下重新登录,它可能会错误地报告错误。这是因为当您使会话失效时,会话cookie不会被清除,即使用户已经注销,它也会被重新提交。你可以在登出时显式地删除JSESSIONID cookie,例如在登出处理程序中使用以下语法:
不幸的是,这不能保证对每个servlet容器都适用,所以您需要在您的环境中测试它
如果您在代理服务器后运行应用程序,您还可以通过配置代理服务器来删除会话cookie。例如,使用Apache HTTPD的mod_headers,下面的指令会在登出请求的响应中使
JSESSIONID
过期,从而删除JSESSIONID
cookie(假设应用程序部署在path/tutorial
下):
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
如果您希望对单个用户登录到您的应用程序的能力进行限制,Spring Security通过以下简单的附加功能提供了开箱即用的支持。首先,你需要将以下侦听器添加到你的web.xml
文件中,以保持Spring Security关于会话生命周期事件的更新:
org.springframework.security.web.session.HttpSessionEventPublisher
然后将以下几行添加到你的应用程序上下文中:
...
这将防止用户多次登录—第二次登录将导致第一次登录无效。通常您希望防止第二次登录,在这种情况下您可以使用
...
第二次登录将被拒绝。通过“拒绝”,我们的意思是,如果使用基于表单的登录,用户将被发送到authentication-fail -url
。如果第二次身份验证是通过另一种非交互机制进行的,比如“remember-me”,一个“未经授权的”(401)错误将被发送给客户机。如果希望使用错误页面,可以将属性session-authentication-error-url
添加到session-management
元素。
如果您正在为基于表单的登录使用自定义身份验证过滤器,那么您必须显式地配置并发会话控制支持。更多的细节可以在会话管理一章中找到。
会话固定攻击是一个潜在的风险,在这种情况下,恶意攻击者可能通过访问一个站点来创建一个会话,然后说服另一个用户使用相同的会话登录(例如,通过向他们发送一个包含会话标识符作为参数的链接)。Spring Security通过在用户登录时创建新会话或更改会话ID来自动防止这种情况发生。如果您不需要这种保护,或者它与其他一些需求冲突,您可以使用
上的session-fixation-protection
属性来控制行为,该属性有四个选项
none
-不做任何事。原会议将保留。newSession
-创建一个新的“干净”会话,而不复制现有的会话数据(与Spring security相关的属性仍将被复制)。migrateSession
-创建一个新会话,并将所有现有会话属性复制到新会话。这是Servlet 3.0或旧容器的默认设置。changeSessionId
-不要创建新的会话。相反,使用Servlet容器提供的会话固定保护(HttpServletRequest#changeSessionId()
)。这个选项只在Servlet 3.1 (Java EE 7)和更新的容器中可用。在旧容器中指定它将导致异常。这是Servlet 3.1和新容器中的默认值。当会话固定保护发生时,它会导致在应用程序上下文中发布SessionFixationProtectionEvent
。如果您使用changeSessionId
,此保护也将导致任何javax.servlet.http. httpessionidlistener
被通知,所以如果您的代码侦听这两个事件,请谨慎使用。有关更多信息,请参见会话管理一章。
SessionManagementFilter
根据SecurityContextHolder
的当前内容检查SecurityContextRepository
的内容,以确定用户在当前请求期间是否已经通过身份验证,通常由非交互式验证机制,如pre-authentication或记得我[3]。如果存储库包含安全上下文,则筛选器不执行任何操作。如果没有,并且线程本地的SecurityContext
包含一个(非匿名的)Authentication
对象,那么过滤器假定它们已经通过堆栈中先前的过滤器进行了身份验证。然后它将调用已配置的SessionAuthenticationStrategy
。
如果用户当前没有经过身份验证,该过滤器将检查是否请求了一个无效的会话ID(例如,由于超时),并将调用配置的InvalidSessionStrategy
(如果设置了一个)。最常见的行为就是重定向到一个固定的URL,这被封装在标准实现SimpleRedirectInvalidSessionStrategy
中。当通过名称空间配置无效的会话URL时也会使用后者,如前所述。
SessionAuthenticationStrategy
被SessionManagementFilter
和AbstractAuthenticationProcessingFilter
使用,所以如果你使用一个定制的表单登录类,例如,你将需要将它注入到这两个类中。在这种情况下,结合命名空间和自定义bean的典型配置可能是这样的:
...
请注意,如果您将bean存储在实现HttpSessionBindingListener
的会话中(包括Spring会话范围内的bean),使用缺省的SessionFixationProtectionStrategy
可能会导致问题。有关这个类的更多信息,请参阅Javadoc。
Spring Security能够防止主体对同一应用程序的并发身份验证次数超过指定的次数。许多isv利用这一点来实施许可,而网络管理员喜欢这个特性,因为它有助于防止人们共享登录名。例如,您可以阻止用户“Batman”从两个不同的会话登录到web应用程序。您可以过期他们之前的登录,也可以在他们试图再次登录时报告错误,以防止第二次登录。注意,如果您使用第二种方法,没有显式登出的用户(例如,刚刚关闭浏览器的用户)将不能再次登录,直到原始会话过期。
命名空间支持并发控制,因此请检查前面的命名空间章节以获得最简单的配置。但有时你需要定制一些东西。
该实现使用了SessionAuthenticationStrategy
的一个特殊版本,称为ConcurrentSessionControlAuthenticationStrategy
。
以前,并发身份验证检查是由ProviderManager
进行的,它可以被注入一个ConcurrentSessionController
。后者将检查用户是否试图超过允许的会话数。但是,这种方法要求预先创建HTTP会话,这是不可取的。在Spring Security 3中,用户首先通过AuthenticationManager
进行身份验证,一旦验证成功,将创建一个会话,并检查是否允许打开另一个会话。
要使用并发会话支持,你需要在web.xml
中添加以下内容:
org.springframework.security.web.session.HttpSessionEventPublisher
此外,你需要添加ConcurrentSessionFilter
到你的FilterChainProxy
。ConcurrentSessionFilter
需要两个构造函数参数:sessionRegistry
和sessionInformationExpiredStrategy
,前者通常指向SessionRegistryImpl
的一个实例,后者定义了在会话过期时应用的策略。使用命名空间来创建FilterChainProxy
和其他默认bean的配置可能是这样的:
在web.xml
中添加侦听器会导致在每次HttpSession
开始或结束时将ApplicationEvent
发布到Spring ApplicationContext
中。这非常重要,因为它允许在会话结束时通知SessionRegistryImpl
。如果没有它,用户将永远无法在超过会话允许范围后再次登录,即使他们退出另一个会话或会话超时。
设置并发控制,通过名称空间或使用简单的bean的有用的副作用为你提供SessionRegistry
的引用,您可以直接使用在您的应用程序,所以即使你不希望限制用户的会话数量,它可能值得建立基础设施。您可以将maximumSession
属性设置为-1,以允许无限制的会话。如果使用名称空间,可以使用session-registry-alias
属性为内部创建的SessionRegistry
设置别名,提供一个可以注入到自己的bean中的引用。
getAllPrincipals()
方法为您提供了当前经过身份验证的用户列表。您可以通过调用getAllSessions(Object principal,boolean includeExpiredSessions)
方法来列出用户的会话,该方法返回一个SessionInformation
对象列表。您还可以通过在SessionInformation
实例上调用expireNow()
来终止用户的会话。当用户返回到应用程序时,他们将被阻止继续。例如,您可能会发现这些方法在管理应用程序中很有用。查看Javadoc以获得更多信息。
记住我或持久登录身份验证指的是网站能够在会话之间记住主体的身份。这通常是通过向浏览器发送cookie来完成的,在未来的会话中检测到cookie并导致自动登录发生。Spring Security为执行这些操作提供了必要的钩子,并有两个具体的remember-me实现。一个使用散列来保护基于cookie的令牌的安全性,另一个使用数据库或其他持久存储机制来存储生成的令牌。
注意,两个实现都需要一个UserDetailsService
。如果您使用的身份验证提供程序不使用UserDetailsService
(例如,LDAP提供程序),那么它将无法工作,除非您的应用程序上下文中也有UserDetailsService
bean。
这种方法使用哈希来实现一个有用的记忆策略。本质上,一个cookie在成功的交互身份验证后被发送到浏览器,cookie组成如下:
base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
username: As identifiable to the UserDetailsService
password: That matches the one in the retrieved UserDetails
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds
key: A private key to prevent modification of the remember-me token
因此,remember-me令牌仅在指定的时间段内有效,并且前提是用户名、密码和密钥不发生更改。值得注意的是,这有一个潜在的安全问题,因为捕获的remember-me令牌在令牌过期之前对任何用户代理都是可用的。这与摘要身份验证是相同的问题。如果主体意识到一个令牌已经被捕获,那么他们可以很容易地更改自己的密码,并立即使所有remember-me令牌失效。如果需要更重要的安全性,您应该使用下一节中描述的方法。或者记住我的服务根本不应该被使用。
如果您熟悉关于名称空间配置那一章讨论的主题,您可以通过添加
元素来启用remember-me身份验证:
...
UserDetailsService
通常会被自动选择。如果在您的应用程序上下文中有多个,您需要指定应该将哪个与user-service-ref
属性一起使用,其中值是您的UserDetailsService
bean的名称。
这种方法基于文章http://jaspan.com/improved_persistent_login_cookie_best_practice,并对[4]做了一些小修改。要在命名空间配置中使用这种方法,你需要提供一个数据源引用:
...
数据库应该包含一个persistent_logins
表,使用以下SQL(或等效SQL)创建:
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)
Remember-me与UsernamePasswordAuthenticationFilter
一起使用,并通过AbstractAuthenticationProcessingFilter
超类中的钩子实现。它也在BasicAuthenticationFilter
中使用。这些钩子将在适当的时候调用一个具体的RememberMeServices
。界面看起来是这样的:
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
请参考Javadoc以获得关于这些方法做什么的更全面的讨论,尽管在这个阶段注意AbstractAuthenticationProcessingFilter
只调用loginFail()
和loginSuccess()
方法。当SecurityContextHolder
不包含Authentication
时,RememberMeAuthenticationFilter
会调用autoLogin()
方法。因此,该接口为底层的remember-me实现提供了与身份验证相关的事件的充分通知,并在候选web请求可能包含cookie并希望被记住时委托给实现。这种设计允许使用任意数量的记忆实现策略。我们在上面已经看到Spring Security提供了两种实现。我们将依次讨论这些问题。
此实现支持简单哈希令牌方法中描述的更简单的方法。TokenBasedRememberMeServices
生成一个RememberMeAuthenticationToken
,该token由RememberMeAuthenticationProvider
处理。密钥在这个身份验证提供者和TokenBasedRememberMeServices
之间共享。此外,TokenBasedRememberMeServices
需要一个UserDetailsService
,它可以从中检索用户名和密码,用于签名比较,并生成RememberMeAuthenticationToken
来包含正确的GrantedAuthority
s。应用程序应该提供某种注销命令,当用户请求时使cookie失效。TokenBasedRememberMeServices
也实现了Spring Security的LogoutHandler
接口,因此可以与LogoutFilter
一起使用来自动清除cookie。
应用程序上下文中启用remember-me服务所需的bean如下所示:
别忘了把你的RememberMeServices
实现添加到UsernamePasswordAuthenticationFilter.setRememberMeServices()
属性中,在AuthenticationManager.setProviders()
列表中包括RememberMeAuthenticationProvider
,并添加RememberMeAuthenticationFilter
到你的FilterChainProxy
(通常在你的UsernamePasswordAuthenticationFilter
之后)。
这个类可以以与TokenBasedRememberMeServices
相同的方式使用,但是它还需要配置一个PersistentTokenRepository
来存储令牌。有两种标准实现。
InMemoryTokenRepositoryImpl
,它仅用于测试。JdbcTokenRepositoryImpl
,它将令牌存储在数据库中。数据库模式在上述持久令牌方法中进行了描述。
OpenID 1.0和2.0协议已经被弃用,我们鼓励用户迁移到OpenID Connect, spring-security-oauth2
支持OpenID Connect。
命名空间支持OpenID登录,而不是普通的基于表单的登录。
然后你应该注册一个OpenID提供商(如myopenid.com),并将用户信息添加到内存
:
您应该能够使用myopenid.com
站点登录进行身份验证。通过在openid-login
元素上设置user-service-ref
属性,也可以选择一个特定的UserDetailsService
bean来使用OpenID。注意,我们在上面的用户配置中省略了password属性,因为这组用户数据只用于加载用户的权限。将在内部生成一个随机密码,防止您在配置的其他地方意外地使用此用户数据作为身份验证源。
支持OpenID属性交换。例如,下面的配置将尝试从OpenID提供者检索电子邮件和全名,供应用程序使用:
每个OpenID属性的“类型”是一个URI,由特定的模式决定,在本例中为https://axschema.org/
。如果必须检索一个属性才能成功进行身份验证,则可以设置required
属性。所支持的确切模式和属性将取决于您的OpenID提供者。属性值作为身份验证过程的一部分返回,可以使用以下代码访问:
OpenIDAuthenticationToken token =
(OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
List attributes = token.getAttributes();
我们可以从SecurityContextHolder中获得OpenIDAuthenticationToken
。OpenIDAttribute
包含属性类型和检索到的值(或多值属性的值)。您可以提供多个attribute-exchange
元素,对每个元素使用identifier-matcher
属性。它包含一个正则表达式,将与用户提供的OpenID标识符进行匹配。请参阅代码库中的OpenID示例应用程序以获得示例配置,它为谷歌、Yahoo和MyOpenID提供者提供了不同的属性列表。
通常认为采用“默认拒绝”是良好的安全实践,即显式指定允许什么和不允许什么。定义未经身份验证的用户可以访问的内容也是类似的情况,特别是对于web应用程序。许多站点要求用户必须对除了一些url(例如主页和登录页面)以外的任何内容进行身份验证。在这种情况下,为这些特定url定义访问配置属性比为每个安全资源定义访问配置属性更容易。换句话说,有时候说ROLE_SOMETHING
默认情况下是必需的,并且只允许这个规则的某些例外,比如登录、注销和应用程序的主页。您也可以从过滤器链中完全忽略这些页面,从而绕过访问控制检查,但由于其他原因,这可能是不需要的,特别是如果页面对经过身份验证的用户的行为不同。
这就是我们所说的匿名身份验证。注意,在“匿名身份验证”的用户和未身份验证的用户之间没有真正的概念上的区别。Spring Security的匿名身份验证为配置访问控制属性提供了一种更方便的方式。对servlet API调用(例如getCallerPrincipal
)的调用仍然返回null,即使SecurityContextHolder
中实际上有一个匿名身份验证对象。
在其他情况下,匿名身份验证也很有用,比如审计拦截器查询SecurityContextHolder
以确定哪个主体负责给定的操作。如果知道SecurityContextHolder
总是包含一个Authentication
对象,而且永远不会为null
,则可以更加健壮地创建类。
使用HTTP配置Spring Security 3.0时自动提供了匿名身份验证支持,并且可以使用
元素进行定制(或禁用)。您不需要配置这里描述的bean,除非您正在使用传统bean配置。
三个类一起提供匿名身份验证特性。AnonymousAuthenticationToken
是Authentication
的实现,并存储应用于匿名主体的GrantedAuthority
。有一个对应的AnonymousAuthenticationProvider
,它被链接到ProviderManager
中,以便AnonymousAuthenticationToken
被接受。最后,还有一个AnonymousAuthenticationFilter
,它被链接到普通的身份验证机制之后,如果那里没有存在的身份验证,它会自动将一个AnonymousAuthenticationToken
添加到SecurityContextHolder
中。过滤器和身份验证提供程序的定义如下所示
该key
在筛选器和身份验证提供者之间共享,因此前者创建的令牌被后者接受。userAttribute
表示为usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]
。这与InMemoryDaoImpl
的userMap
属性的等号后面使用的语法相同。
如前所述,匿名身份验证的好处是所有URI模式都可以应用安全性。例如:
" +
匿名身份验证讨论的最后一部分是AuthenticationTrustResolver
接口及其相应的AuthenticationTrustResolverImpl
实现。该接口提供了一个isAnonymous(Authentication)
方法,该方法允许感兴趣的类考虑这种特殊类型的身份验证状态。ExceptionTranslationFilter
使用这个接口在处理AccessDeniedException
。如果抛出AccessDeniedException
,匿名类型的身份验证,而不是扔一个403(禁止)响应,过滤器将开始AuthenticationEntryPoint
所以主体可以正常进行身份验证。这是一个必要的区别,否则主体总是被认为是“经过身份验证的”,从来没有机会通过表单、基本的、摘要或其他一些正常的身份验证机制登录。
在上面的拦截器配置中,您经常会看到ROLE_ANONYMOUS
属性被IS_AUTHENTICATED_ANONYMOUSLY
所取代,这在定义访问控制时实际上是相同的。这是一个使用AuthenticatedVoter
的例子,我们将在授权一章中看到。它使用AuthenticationTrustResolver
来处理这个特定的配置属性,并将访问权限授予匿名用户。AuthenticatedVoter
方法更加强大,因为它允许您区分匿名、记住我和完全验证的用户。如果您不需要这个功能,那么您可以坚持使用ROLE_ANONYMOUS
,它将由Spring Security的标准RoleVoter
处理。
有些情况下,您希望使用Spring Security进行授权,但是用户在访问应用程序之前已经通过某些外部系统的可靠身份验证。我们将这些情况称为“预先身份验证”场景。示例包括X.509、Siteminder和应用程序在其中运行的Java EE容器的身份验证。当使用预认证时,Spring Security必须这样做
具体细节将取决于外部身份验证机制。对于X.509,用户可以通过证书信息来标识,对于Siteminder,则可以通过HTTP请求头来标识。如果依赖容器身份验证,将通过调用传入HTTP请求的getUserPrincipal()
方法来标识用户。在某些情况下,外部机制可能为用户提供角色/权限信息,但在其他情况下,权限必须从单独的来源获得,如UserDetailsService
。
由于大多数预身份验证机制遵循相同的模式,Spring Security有一组类,它们为实现预身份验证提供者提供了内部框架。这消除了重复,并允许以结构化的方式添加新的实现,而不必从头编写所有内容。如果您想使用X.509身份验证之类的东西,则不需要了解这些类,因为它已经有一个名称空间配置选项,使用起来更简单。如果您需要使用显式bean配置,或者正在计划编写自己的实现,那么了解所提供的实现是如何工作的将非常有用。你可以在org.springframework.security.web.authentication.preauth
下找到类。我们只是在这里提供一个大纲,因此您应该在适当的时候参考Javadoc和源代码。
这个类将检查安全上下文的当前内容,如果为空,它将尝试从HTTP请求中提取用户信息并将其提交给AuthenticationManager
。子类覆盖以下方法以获得该信息:
protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);
protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);
调用这些之后,过滤器将创建一个包含返回数据的PreAuthenticatedAuthenticationToken
,并提交它进行身份验证。这里所说的“身份验证”,实际上只是指可能加载用户权限的进一步处理,但是遵循标准Spring Security身份验证体系结构。
像其他Spring安全身份验证过滤器,pre-authentication过滤器有一个authenticationDetailsSource
属性,默认情况下将在Authentication
对象的details
属性创建一个WebAuthenticationDetails
对象来存储更多的信息,比如会话标识符和原始IP地址。如果用户角色信息可以从预认证机制获得,数据也存储在这个属性中,详细信息实现了GrantedAuthortiesContainer
接口。这使身份验证提供者能够读取外部分配给用户的权限。下面我们来看一个具体的例子。
如果过滤器配置了该类的一个实例authenticationDetailsSource
,则通过为每一个预先确定的“可映射角色”集合调用isUserInRole(String role)
方法来获得权威信息。这个类从一个配置好的MappableAttributesRetriever
检索器获得这些。可能的实现包括在应用程序上下文中硬编码一个列表,并从web.xml
文件中的
信息中读取角色信息。身份验证前的示例应用程序使用后一种方法。
还有一个附加阶段,使用配置的Attributes2GrantedAuthortiesMapper
将角色(或属性)映射到Spring Security GrantedAuthority
对象。默认情况下只会在名称中添加通常的ROLE_
前缀,但它让您完全控制行为。
预先身份验证的提供者除了为用户加载UserDetails
对象外,没有什么其他工作要做。它通过委托给AuthenticationUserDetailsService
来做到这一点。后者类似于标准的UserDetailsService
,但接受一个Authentication
对象,而不仅仅是用户名:
public interface AuthenticationUserDetailsService {
UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}
这个接口可能还有其他用途,但是使用预身份验证,它允许访问封装在Authentication
对象中的权限,正如我们在前一节中看到的那样。PreAuthenticatedGrantedAuthortiesUserDetailsService
类可以做到这一点。或者,它可以通过UserDetailsByNameServiceWrapper
实现委托给一个标准的UserDetailsService
。
AuthenticationEntryPoint
负责为未经身份验证的用户(当他们试图访问受保护的资源时)启动身份验证过程,但在预先身份验证的情况下,这并不适用。如果不使用预身份验证和其他身份验证机制结合使用,那么只能使用该类的一个实例来配置ExceptionTranslationFilter
。如果用户被AbstractPreAuthenticatedProcessingFilter
拒绝,就会调用它,从而导致验证为空。如果被调用,它总是返回一个403
禁止的响应码。
X.509身份验证将在其单独的一章中介绍。这里我们将介绍一些类,它们为其他预身份验证场景提供支持。
外部身份验证系统可以通过在HTTP请求上设置特定的头向应用程序提供信息。一个著名的例子是Siteminder,它在名为SM_USER
的头文件中传递用户名。该类RequestHeaderAuthenticationFilter
支持这种机制,它只是从头中提取用户名。它默认使用名称SM_USER
作为头名称。有关更多细节,请参阅Javadoc。
这里请注意,在使用这样的系统时,框架根本不执行身份验证检查,正确配置外部系统并保护对应用程序的所有访问是极其重要的。如果攻击者能够在他们的原始请求中伪造报头而不被检测到,那么他们可以选择任何他们想要的用户名。
使用该过滤器的典型配置如下所示:
这里我们假设安全名称空间用于配置。它还假设您已经添加了一个UserDetailsService
(称为“userDetailsService”)到您的配置中,以加载用户的角色。
类J2eePreAuthenticatedProcessingFilter
将从HttpServletRequest
的userPrincipal
属性中提取用户名。该过滤器的使用通常会与Java EE角色的使用相结合,如上所述的J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource。
在代码库中有一个使用这种方法的示例应用程序,所以从github获得代码,如果你感兴趣,可以看看应用程序上下文文件。代码位于samples/xml/preauth
目录中。
Spring Security提供了一个能够将身份验证请求委托给Java身份验证和授权服务(JAAS)的包。这个包将在下面详细讨论。
AbstractJaasAuthenticationProvider
是所提供的JAAS AuthenticationProvider
实现的基础。子类必须实现一个创建LoginContext
的方法。AbstractJaasAuthenticationProvider
有许多可以注入其中的依赖项,下面将讨论这些依赖项。
TBD
TBD
TBD
TBD
TBD
TBD
TBD
JA-SIG在系统上产生一个企业范围的单点登录,称为CAS。与其他计划不同的是,JA-SIG的中央认证服务是开源的、被广泛使用、易于理解、平台独立并支持代理功能。Spring Security完全支持CAS,并提供了从Spring Security的单一应用程序部署到由企业范围的CAS服务器保护的多应用程序部署的简单迁移路径。
您可以在https://www.apereo.org了解更多关于CAS的信息。你也需要访问这个网站来下载CAS服务器文件。
虽然CAS网站包含详细说明CAS体系结构的文档,但我们在Spring Security上下文中再次给出一般概述。Spring Security 3.x支持CAS 3。在编写本文时,CAS服务器的版本是3.4。
在您的企业中,您需要设置CAS服务器。CAS服务器只是一个标准的WAR文件,因此设置服务器并不困难。在WAR文件中,您将定制显示给用户的页面上的登录和其他单点登录。
当部署CAS 3.4服务器时,您还需要在CAS包含的deployerConfigContext.xml
中指定AuthenticationHandler
。AuthenticationHandler
有一个简单的方法,返回一组给定的凭证是否有效的布尔值。AuthenticationHandler
实现将需要链接到某种类型的后端身份验证存储库,比如LDAP服务器或数据库。CAS本身包括许多开箱即用的AuthenticationHandler
来帮助实现这一点。当您下载和部署服务器war文件时,它被设置为成功地对输入与用户名匹配的密码的用户进行身份验证,这对于测试是很有用的。
除了CAS服务器本身,其他关键角色当然是部署在整个企业中的安全web应用程序。这些web应用程序被称为“服务”。有三种类型的服务。验证服务票据的、可以获得代理票据的和验证代理票据的。对代理票据的身份验证不同,因为必须验证代理列表,而且通常可以重用代理票据。
web浏览器、CAS服务器和Spring Security-secure服务之间的基本交互如下:
ExceptionTranslationFilter
将检测AccessDeniedException
或AuthenticationException
。Authentication
对象(或缺少它)导致了AuthenticationException
, ExceptionTranslationFilter
将调用配置好的AuthenticationEntryPoint
。如果使用CAS,这将是CasAuthenticationEntryPoint
类。CasAuthenticationEntryPoint
将用户的浏览器重定向到CAS服务器。它还将指示一个service
参数,该参数是Spring Security服务(您的应用程序)的回调URL。例如,浏览器被重定向到的URL可能是https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas
。PasswordHandler
(如果使用CAS 3.0,则使用AuthenticationHandler
)来决定用户名和密码是否有效。ticket
参数,这是一个表示“服务票证”的不透明字符串。继续我们前面的示例,浏览器重定向到的URL可能是https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ
。CasAuthenticationFilter
总是侦听对/login/cas
的请求(这是可配置的,但在本介绍中我们将使用默认值)。处理过滤器将构造一个UsernamePasswordAuthenticationToken
,表示服务票据。主体将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
,而凭据将是服务票据不透明值。然后将此身份验证请求传递给已配置的AuthenticationManager
。AuthenticationManager
实现将是ProviderManager
,而ProviderManager
又是用CasAuthenticationProvider
配置的。CasAuthenticationProvider
只响应UsernamePasswordAuthenticationToken
,其中包含特定于cas的主体(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
)和CasAuthenticationToken
(稍后讨论)。CasAuthenticationProvider
将使用TicketValidator
实现验证服务票据。这通常是一个Cas20ServiceTicketValidator
,它是CAS客户端库中包含的类之一。如果应用程序需要验证代理票据,则使用Cas20ProxyTicketValidator
。TicketValidator
向CAS服务器发出一个HTTPS请求,以验证服务票证。它还可能包含一个代理回调URL,它包含在这个示例中:https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor
。pgtUrl
参数中),CAS将在XML响应中包含一个pgtIou
字符串。这个pgtIou
表示代理授权票据IOU。然后CAS服务器将创建自己的HTTPS连接回pgtUrl
。这是为了相互验证CAS服务器和声明的服务URL。HTTPS连接将用于向原始web应用程序发送代理授权票据。例如,https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH
。Cas20TicketValidator
将解析从CAS服务器接收到的XML。它将向CasAuthenticationProvider
返回一个TicketResponse
,其中包括用户名(强制)、代理列表(如果涉及)和代理授权票据IOU(如果请求了代理回调)。CasAuthenticationProvider
将调用配置好的CasProxyDecider
。CasProxyDecider
指示TicketResponse
中的代理列表是否为服务所接受。Spring Security提供了几个实现:RejectProxyTickets
, AcceptAnyCasProxy
和NamedCasProxyDecider
。除了NamedCasProxyDecider
允许提供受信任的代理List
表外,这些名称基本上是不言自明的。CasAuthenticationProvider
接下来将请求AuthenticationUserDetailsService
加载应用于Assertion
中包含的用户的GrantedAuthority
对象。CasAuthenticationProvider
将构造一个CasAuthenticationToken
,其中包含TicketResponse
和GrantedAuthority
中包含的详细信息。CasAuthenticationFilter
,它将创建的CasAuthenticationToken
放入安全上下文中。AuthenticationException
的原始页面(或根据配置自定义的目的地)。你还在这里真是太好了!现在让我们看看如何配置它
由于Spring安全性,CAS的web应用端变得很容易。假定您已经知道使用Spring Security的基本知识,因此下面不再讨论这些内容。我们将假设正在使用基于名称空间的配置,并根据需要添加CAS bean。每一节都以前一节为基础。完整的CAS示例应用程序可以在Spring Security Samples中找到。
本节描述如何设置Spring Security来验证服务票据。通常这就是web应用程序所需要的。您需要将ServiceProperties
bean添加到您的应用程序上下文中。这代表你的CAS服务:
该service
必须等于CasAuthenticationFilter
将监视的URL。sendRenew
默认值为false,但如果应用程序特别敏感,则应该设置为true。该参数的作用是告诉CAS登录服务登录时的单次登录是不可接受的。相反,用户需要重新输入用户名和密码才能访问该服务。
应该配置以下bean以开始CAS身份验证过程(假设您正在使用名称空间配置):
...
为了让CAS操作,ExceptionTranslationFilter
必须将其authenticationEntryPoint
属性设置为CasAuthenticationEntryPoint
bean。这可以很容易地使用entry-point-ref
来完成,就像上面的例子一样。CasAuthenticationEntryPoint
必须引用ServiceProperties
bean(上面讨论过),ServiceProperties
bean为企业的CAS登录服务器提供URL。这是用户的浏览器将被重定向的地方。
CasAuthenticationFilter
具有与UsernamePasswordAuthenticationFilter
(用于基于表单的登录)非常相似的属性。您可以使用这些属性来定制身份验证成功和失败的行为等内容。
接下来,你需要添加CasAuthenticationProvider
和它的合作者:
...
CasAuthenticationProvider
使用UserDetailsService
实例为用户加载授权,一旦用户通过CAS进行身份验证。我们在这里展示了一个简单的内存设置。请注意,CasAuthenticationProvider
实际上并不使用密码进行身份验证,但它使用了权限。
如果您回看CAS的工作原理一节,那么这些bean都是很容易解释的。
这就完成了CAS的最基本配置。如果您没有犯任何错误,那么您的web应用程序应该能够在CAS单点登录框架内愉快地工作。Spring Security的其他部分不需要考虑CAS处理身份验证这一事实。在下面的部分中,我们将讨论一些(可选的)更高级的配置。
CAS协议支持单次登出,可以很容易地添加到Spring Security配置中。下面是对处理Single Logout的Spring Security配置的更新
...
logout
元素使用户退出本地应用程序,但不结束与CAS服务器或已经登录到的任何其他应用程序的会话。requestSingleLogoutFilter
过滤器将允许请求/spring_security_cas_logout
的URL,将应用程序重定向到配置的CAS服务器注销URL。然后CAS服务器将向所有已签名的服务发送一个Single Logout请求。singleLogoutFilter
通过在静态Map
中查找HttpSession
,然后使其失效来处理Single Logout请求。
为什么同时需要logout
元素和singleLogoutFilter
,这可能会让人感到困惑。最好的做法是先本地注销,因为SingleSignOutFilter
只是将HttpSession
存储在静态Map
中,以便在其上调用invalidate。使用上面的配置,注销流程将是:
/logout
将使用户退出本地应用程序,并将用户发送到注销成功页面。/cas-logout.jsp
应该指示用户单击指向/logout/cas
的链接以注销所有应用程序。https://localhost:9443/cas/logout
)。SingleSignOutFilter
通过使原始会话失效来处理注销请求。下一步是将以下内容添加到web.xml中
characterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
characterEncodingFilter
/*
org.jasig.cas.client.session.SingleSignOutHttpSessionListener
在使用SingleSignOutFilter时,您可能会遇到一些编码问题。因此,建议添加CharacterEncodingFilter
,以确保在使用SingleSignOutFilter
时字符编码是正确的。同样,请参考JASIG的文档了解详细信息。SingleSignOutHttpSessionListener
确保当HttpSession
过期时,用于单个注销的映射将被删除。
本节介绍如何使用CAS对服务进行身份验证。换句话说,本节讨论如何设置使用使用CAS进行身份验证的服务的客户机。下一节将描述如何使用CAS将无状态服务设置为Authenticate。
为了对无状态服务进行身份验证,应用程序需要获得代理授权票证(PGT)。本节介绍如何配置Spring Security,以获得在thencas-st[服务票证验证]配置上的PGT构建。
第一步是在Spring Security配置中包含一个ProxyGrantingTicketStorage
。它用于存储由CasAuthenticationFilter
获得的PGT,以便它们可以用于获取代理票据。下面显示了一个配置示例
下一步是更新CasAuthenticationProvider
,使其能够获得代理票据。为此,将Cas20ServiceTicketValidator
替换为Cas20ProxyTicketValidator
。proxyCallbackUrl
应该设置为应用程序将接收PGT所在的URL。最后,配置还应该引用ProxyGrantingTicketStorage
,这样它就可以使用PGT来获得代理票据。您可以在下面找到一个配置更改的示例。
...
最后一步是更新CasAuthenticationFilter
以接受PGT,并将它们存储在ProxyGrantingTicketStorage
中。proxyReceptorUrl
匹配Cas20ProxyTicketValidator
的proxyCallbackUrl
是很重要的。下面显示了一个配置示例。
...
现在Spring Security获得了PGT,您可以使用它们来创建代理票据,代理票据可以用于对无状态服务进行身份验证。CAS示例应用程序在ProxyTicketSampleServlet
中包含一个工作示例。示例代码如下:
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal();
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);
// Make a remote call using the proxy ticket
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
...
}
CasAuthenticationProvider
区分有状态和无状态客户端。有状态客户端被认为是任何提交到CasAuthenticationFilter
的filterProcessUrl
的客户端。无状态客户端是指在URL(而不是filterProcessUrl
)上向CasAuthenticationFilter
提出身份验证请求的任何客户端。
因为远程协议没有办法在HttpSession
的上下文中显示自己,所以不可能依赖于在请求之间的会话中存储安全上下文的默认实践。此外,由于CAS服务器将在TicketValidator
验证后使票据失效,因此在后续请求中呈现相同的代理票据将无法工作。
一个明显的选择是对远程协议客户机根本不使用CAS。然而,这将消除CAS的许多理想特性。作为一个中间地带,CasAuthenticationProvider
使用一个StatelessTicketCache
。这仅用于使用等同于CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
主体的无状态客户端。CasAuthenticationProvider
将结果CasAuthenticationToken
存储在StatelessTicketCache
中,并在代理票据上键入键。因此,远程协议客户端可以提供相同的代理票据,CasAuthenticationProvider
将不需要联系CAS服务器进行验证(除了第一个请求)。一旦经过身份验证,代理票证就可以用于原始目标服务之外的其他url。
本节构建在前面的基础上,以适应代理票证身份验证。第一步是指定对所有工件进行身份验证,如下所示。
...
下一步是为CasAuthenticationFilter
指定serviceProperties
和authenticationDetailsSource
。serviceProperties
属性指示CasAuthenticationFilter
尝试验证所有工件,而不是仅验证filterProcessUrl
上的工件。ServiceAuthenticationDetailsSource
创建一个ServiceAuthenticationDetails
,以确保在验证票据时使用基于HttpServletRequest
的当前URL作为服务URL。生成服务URL的方法可以通过注入一个返回自定义ServiceAuthenticationDetails
的自定义AuthenticationDetailsSource
来定制。
...
您还需要更新CasAuthenticationProvider
来处理代理票据。为此,将Cas20ServiceTicketValidator
替换为Cas20ProxyTicketValidator
。您需要配置statelessTicketCache
以及您想要接受的代理。您可以在下面找到一个接受所有代理所需的更新示例。
...
X.509证书身份验证最常用的用法是在使用SSL时验证服务器的身份,最常用的用法是在浏览器中使用HTTPS时。浏览器将自动检查服务器提供的证书是否已由它所维护的可信证书颁发机构之一颁发(即数字签名)。
你也可以使用SSL进行“双向认证”;然后,服务器将从客户机请求一个有效的证书,作为SSL握手的一部分。服务器将通过检查客户端证书是否由可接受的权威机构签名来验证客户端。如果提供了有效的证书,可以通过应用程序中的servlet API获得它。Spring Security X.509模块使用过滤器提取证书。它将证书映射到应用程序用户,并加载该用户所授予的权限集,以便与标准Spring Security基础设施一起使用。
在尝试将它与Spring Security一起使用之前,您应该熟悉如何使用证书并为您的servlet容器设置客户机身份验证。大部分工作都是创建和安装合适的证书和密钥。例如,如果您正在使用Tomcat,请阅读这里的说明https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html。在使用Spring Security试用它之前,让它工作是很重要的
启用X.509客户机身份验证非常简单。只需将
元素添加到http安全名称空间配置中。
...
;
元素有两个可选属性:
subject-principal-regex
。用于从证书的主题名称中提取用户名的正则表达式。默认值如上所示。这是将被传递给UserDetailsService
以加载用户权限的用户名。user-service-ref
。这是将与X.509一起使用的UserDetailsService
的bean Id。如果您的应用程序上下文中只定义了一个,则不需要它。subject-principal-regex
应该包含单个组。例如,默认表达式“CN=(.?)”匹配公共名称字段。因此,如果证书中的主题名是“CN=Jimi Hendrix, OU=”,则会得到一个用户名“Jimi Hendrix”。匹配不区分大小写。所以“emailAddress=(.?),”将匹配“emailAddress= [email protected],CN=”,给出一个用户名“[email protected]”。如果客户端提供了一个证书,并且成功提取了有效的用户名,那么在安全上下文中应该有一个有效的Authentication
对象。如果没有找到证书,或者没有找到对应的用户,则安全上下文将保持为空。这意味着您可以轻松地将X.509身份验证与其他选项(如基于表单的登录)一起使用。
在Spring Security项目的samples/certificate
目录中有一些预生成的证书。如果不希望生成自己的SSL,可以使用它们来启用SSL进行测试。文件server.jks
包含服务器证书、私钥和颁发证书的权威证书。还有一些用于示例应用程序的用户的客户机证书文件。您可以在浏览器中安装这些组件,以启用SSL客户机身份验证。
要运行具有SSL支持的tomcat,请导入server.jks
文件到tomcat conf
目录中,并将以下连接器添加到server.xml
文件中
如果您仍然希望SSL连接成功,即使客户端不提供证书,也可以将clientAuth
设置为want
。不提供证书的客户端将不能访问任何受Spring Security保护的对象,除非您使用非x.509认证机制,如表单认证。
AbstractSecurityInterceptor
能够在安全对象回调阶段临时替换SecurityContext
和SecurityContextHolder
中的Authentication
对象。只有AuthenticationManager
和AccessDecisionManager
成功处理了原始的Authentication
对象时才会发生这种情况。RunAsManager
将指示应该在SecurityInterceptorCallback
期间使用的替换Authentication
对象(如果有的话)。
通过在安全对象回调阶段临时替换Authentication
对象,安全调用将能够调用其他需要不同身份验证和授权凭证的对象。它还能够对特定的GrantedAuthority
对象执行任何内部安全检查。因为Spring Security提供了许多帮助器类,它们基于SecurityContextHolder
的内容自动配置远程协议,所以这些run-as替换在调用远程web服务时特别有用
Spring Security提供了一个RunAsManager
接口:
Authentication buildRunAs(Authentication authentication, Object object,
List config);
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
第一个方法返回Authentication
对象,该对象在方法调用期间应该替换现有的Authentication
对象。如果该方法返回null,则表示不应进行替换。AbstractSecurityInterceptor
使用第二种方法作为配置属性的启动验证的一部分。安全拦截器实现调用supports(Class)
方法,以确保配置的RunAsManager
支持安全拦截器将提供的安全对象类型。
Spring Security提供了RunAsManager
的一个具体实现。如果任何ConfigAttribute
以RUN_AS_
开头,RunAsManagerImpl
类返回一个替换的RunAsUserToken
。如果找到任何这样的ConfigAttribute
,则替换的RunAsUserToken
将包含与原始Authentication
对象相同的主体、凭据和已授予的权限,以及每个RUN_AS_ ConfigAttribute
的新的SimpleGrantedAuthority
。每个新的SimpleGrantedAuthority
都将加上ROLE_
前缀,然后是RUN_AS
ConfigAttribute
。例如,RUN_AS_SERVER
将导致替换RunAsUserToken
,其中包含授予的ROLE_RUN_AS_SERVER
权限。
替换的RunAsUserToken
就像任何其他Authentication
对象一样。它需要由AuthenticationManager
进行身份验证,可能通过委托给合适的AuthenticationProvider
。RunAsImplAuthenticationProvider
执行这样的身份验证。它只是接受任何提供的RunAsUserToken
为有效。
为了确保恶意代码不会创建RunAsUserToken
并将其提供给RunAsImplAuthenticationProvider
以保证接受,密钥的哈希值存储在所有生成的令牌中。RunAsManagerImpl
和RunAsImplAuthenticationProvider
是在bean上下文中使用相同的键创建的:
通过使用相同的密钥,每个RunAsUserToken
都可以验证它是由经过批准的RunAsManagerImpl
创建的。出于安全原因,RunAsUserToken
在创建之后是不可变的
当使用WebSecurityConfigurerAdapter
时,会自动应用注销功能。默认情况下,访问URL/logout
将通过以下方式注销用户:
SecurityContextHolder
/login?logout
然而,与配置登录功能类似,您也有各种选项来进一步定制您的注销需求:
protected void configure(HttpSecurity http) throws Exception {
http
.logout(logout -> logout
.logoutUrl("/my/logout")
.logoutSuccessUrl("/my/index")
.logoutSuccessHandler(logoutSuccessHandler)
.invalidateHttpSession(true)
.addLogoutHandler(logoutHandler)
.deleteCookies(cookieNamesToClear)
)
...
}
WebSecurityConfigurerAdapter
时自动应用。/logout
)。/login?logout
。LogoutSuccesssHandler
。logoutSuccessUrl()
将被忽略。HttpSession
失效。SecurityContextLogoutHandler
。LogoutHandler
。SecurityContextLogoutHandler
被添加为最后一个LogoutHandler
。CookieClearingLogoutHandler
的快捷方式。当然,注销也可以使用XML Namespace表示法进行配置。请参阅Spring Security XML Namespace部分中登出元素的文档以获得更多详细信息。
通常,为了定制注销功能,您可以添加LogoutHandler
和/或LogoutSuccessHandler
实现。对于许多常见的场景,这些处理程序是在使用流畅API时在底层应用的。
logout
元素添加了通过导航到特定URL来登出的支持。默认的注销URL是/logout
,但是您可以使用logout-url
属性将其设置为其他内容。关于其他可用属性的更多信息可以在名称空间附录中找到。
通常,LogoutHandler
实现指出能够参与注销处理的类。他们将被要求进行必要的清理工作。因此,它们不应该抛出异常。提供了各种实现:
请参阅Remember-Me接口和实现的详细信息。
流畅的API并没有直接提供LogoutHandler
实现,而是提供了在幕后提供各自LogoutHandler
实现的快捷方式。例如deleteCookies()
允许指定登出成功时要删除的一个或多个cookie的名称。与添加一个CookieClearingLogoutHandler
相比,这是一个快捷方式。
LogoutFilter
成功注销后调用LogoutSuccessHandler
,以处理例如重定向或转发到适当的目的地。注意,该接口几乎与LogoutHandler
相同,但可能引发异常。
提供了以下实现:
如上所述,您不需要直接指定SimpleUrlLogoutSuccessHandler
。相反,流畅API通过设置logoutSuccessUrl()
提供了一种快捷方式。这将在幕后设置SimpleUrlLogoutSuccessHandler
。所提供的URL将在登出后重定向到。默认为/login?logout
。
HttpStatusReturningLogoutSuccessHandler
在REST API类型的场景中可能很有趣。此LogoutSuccessHandler
允许您提供一个要返回的普通HTTP状态代码,而不是在成功登出时重定向到一个URL。如果没有配置,则默认返回状态码200。
对于每一个成功或失败的身份验证,将分别触发一个AuthenticationSuccessEvent
或AuthenticationFailureEvent
。
要侦听这些事件,您必须首先发布一个AuthenticationEventPublisher
。Spring Security的DefaultAuthenticationEventPublisher
可能会做得很好:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
然后,你可以使用Spring的@EventListener
支持:
@Component
public class AuthenticationEvents {
@EventListener
public void onSuccess(AuthenticationSuccessEvent success) {
// ...
}
@EventListener
public void onFailure(AuthenticationFailureEvent failures) {
// ...
}
}
虽然类似于AuthenticationSuccessHandler
和AuthenticationFailureHandler
,但它们很好,因为它们可以独立于servlet API使用。
默认情况下,DefaultAuthenticationEventPublisher
将为以下事件发布一个AuthenticationFailureEvent
:
异常 | 事件 |
---|---|
BadCredentialsException | AuthenticationFailureBadCredentialsEvent |
UsernameNotFoundException | AuthenticationFailureBadCredentialsEvent |
AccountExpiredException | AuthenticationFailureExpiredEvent |
ProviderNotFoundException | AuthenticationFailureProviderNotFoundEvent |
DisabledException | AuthenticationFailureDisabledEvent |
LockedException | AuthenticationFailureLockedEvent |
AuthenticationServiceException | AuthenticationFailureServiceExceptionEvent |
CredentialsExpiredException | AuthenticationFailureCredentialsExpiredEvent |
InvalidBearerTokenException | AuthenticationFailureBadCredentialsEvent |
发布者执行一个精确的Exception匹配,这意味着这些异常的子类也不会产生事件。
为此,你可能想通过setAdditionalExceptionMappings
方法为发布者提供额外的映射:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
Map,
Class> mapping =
Collections.singletonMap(FooException.class, FooEvent.class);
AuthenticationEventPublisher authenticationEventPublisher =
new DefaultAuthenticationEventPublisher(applicationEventPublisher);
authenticationEventPublisher.setAdditionalExceptionMappings(mapping);
return authenticationEventPublisher;
}
并且,在任何AuthenticationException的情况下,你可以提供一个捕获所有事件来触发:
@Bean
public AuthenticationEventPublisher authenticationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
AuthenticationEventPublisher authenticationEventPublisher =
new DefaultAuthenticationEventPublisher(applicationEventPublisher);
authenticationEventPublisher.setDefaultAuthenticationFailureEvent
(GenericAuthenticationFailureEvent.class);
return authenticationEventPublisher;
}
Spring Security中的高级授权功能是其流行的最有力的原因之一。无论选择如何进行身份验证——是使用Spring Security提供的机制和提供者,还是与容器或其他非Spring Security身份验证机构集成——您都会发现,可以以一致而简单的方式在应用程序中使用授权服务。
在本部分中,我们将研究在第1部分中介绍的不同的AbstractSecurityInterceptor
实现。然后,我们将研究如何通过使用域访问控制列表来优化授权。
Authentication,讨论了所有Authentication
实现如何存储GrantedAuthority
对象列表。这些代表了授予主体的权力。GrantedAuthority
对象被AuthenticationManager
插入到Authentication
对象中,稍后AccessDecisionManager
在作出授权决策时读取这些对象。
GrantAuthority
是一个只有一个方法的接口:
String getAuthority();
此方法允许AccessDecisionManager
获得GrantedAuthority
的精确String
表示。通过返回字符串表示,大多数AccessDecisionManager
可以轻松地“读取”一个GrantedAuthority
。如果一个GrantedAuthority
不能精确地表示为String
,则被认为是“复杂的”,getAuthority()
必须返回null
。
“复杂的”GrantedAuthority
的一个例子是存储应用于不同客户帐号的操作和权限阈值列表的实现。将这个复杂的GrantedAuthority
表示为字符串非常困难,因此getAuthority()
方法应该返回null
。这将向任何AccessDecisionManager
表明,它需要专门支持GrantedAuthority
实现,以便理解其内容。
Spring Security包括一个具体的GrantedAuthority
实现,SimpleGrantedAuthority
。这允许将任何用户指定的String
转换为GrantedAuthority
。安全体系结构中包含的所有AuthenticationProvider
都使用SimpleGrantedAuthority
来填充Authentication
对象。
Spring Security提供了拦截器来控制对安全对象(如方法调用或web请求)的访问。关于是否允许继续调用的前置调用决策是由AccessDecisionManager
作出的。
AccessDecisionManager
由AbstractSecurityInterceptor
调用,负责做出最终的访问控制决策。AccessDecisionManager
接口包含三个方法:
void decide(Authentication authentication, Object secureObject,
Collection attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
AccessDecisionManager
的decide
方法被传递了它需要的所有相关信息,以便做出授权决策。特别是,传递安全Object
可以检查实际安全对象调用中包含的那些参数。例如,让我们假设安全对象是MethodInvocation
。可以很容易地查询MethodInvocation
中的任何Customer
参数,然后在AccessDecisionManager
中实现某种安全逻辑,以确保允许主体对该客户进行操作。如果访问被拒绝,实现将抛出AccessDeniedException
异常。
AbstractSecurityInterceptor
在启动时调用supports(ConfigAttribute)
方法,以确定AccessDecisionManager
是否可以处理传递的ConfigAttribute
。安全拦截器实现调用supports(Class)
方法,以确保配置的AccessDecisionManager
支持安全拦截器将提供的安全对象类型。
虽然用户可以实现自己的AccessDecisionManager
来控制授权的所有方面,但是Spring Security包含了几个基于投票的AccessDecisionManager
实现。Voting Decision Manager演示了相关的类。
使用这种方法,将根据授权决策轮询一系列AccessDecisionVoter
实现。然后AccessDecisionManager
根据其对投票的评估决定是否抛出AccessDeniedException
。
AccessDecisionVoter
接口有三个方法:
int vote(Authentication authentication, Object object, Collection attrs);
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
具体的实现返回一个int
,可能的值反映在AccessDecisionVoter
静态字段ACCESS_ABSTAIN
、ACCESS_DENIED
和ACCESS_GRANTED
中。如果投票实现对授权决策没有意见,它将返回ACCESS_ABSTAIN
。如果它有一个意见,它必须返回ACCESS_DENIED
或ACCESS_GRANTED
。
Spring Security提供了三个具体的AccessDecisionManager
来记录投票。ConsensusBased
的实现将基于非弃权票的共识批准或拒绝访问。提供属性是为了在投票相等或所有投票弃权的情况下控制行为。如果收到一个或多个ACCESS_GRANTED
投票,AffirmativeBased
实现将授予访问权限(即,如果至少有一个grant投票,则拒绝投票将被忽略)。与ConsensusBased
实现一样,如果所有投票者都弃权,也有一个参数来控制行为。UnanimousBased
提供程序期望一致的ACCESS_GRANTED
投票来授予访问权,忽略弃权。如果存在任何ACCESS_DENIED
投票,它将拒绝访问。与其他实现一样,如果所有投票者都弃权,则有一个参数来控制行为。
可以实现一个自定义的AccessDecisionManager
以不同的方式统计投票。例如,来自特定AccessDecisionVoter
的投票可能获得额外的权重,而来自特定投票者的否决投票可能具有否决效果。
Spring Security提供的最常用的AccessDecisionVoter
是简单的RoleVoter
,它将配置属性视为简单的角色名,如果用户已被分配了该角色,则投票授予访问权。
如果任何以ROLE_
前缀开头的ConfigAttribute
,它将进行投票。如果有一个GrantedAuthority
返回的String
表示(通过getAuthority()
方法)与一个或多个以ROLE_
开头的ConfigAttributes
完全相等,它将投票授予访问权限。如果没有任何以ROLE_
开头的ConfigAttribute
的精确匹配,RoleVoter
将投票拒绝访问。如果没有以ROLE_
开头的ConfigAttribute
,投票者将弃权。
我们已经隐式看到的另一个投票者是AuthenticatedVoter
,它可以用来区分匿名、完全验证和remember-me验证的用户。许多网站允许在记忆我身份验证下进行某些有限的访问,但要求用户通过登录来确认身份以获得完全的访问权限。
当我们使用属性IS_AUTHENTICATED_ANONYMOUSLY
来授予匿名访问时,AuthenticatedVoter
正在处理这个属性。有关这个类的更多信息,请参阅Javadoc。
显然,您还可以实现一个自定义AccessDecisionVoter
,并且可以在其中放入任何您想要的访问控制逻辑。它可能特定于您的应用程序(与业务逻辑相关),也可能实现一些安全管理逻辑。例如,你会在Spring网站上找到一篇博客文章,它描述了如何使用投票人实时拒绝帐户被暂停的用户的访问。
虽然AccessDecisionManager
在继续安全对象调用之前由AbstractSecurityInterceptor
调用,但一些应用程序需要一种方法来修改安全对象调用实际返回的对象。虽然您可以轻松地实现自己的AOP关注点来实现这一点,但Spring Security提供了一个方便的钩子,它有几个具体的实现,与它的ACL功能集成在一起。
AfterInvocation Implementation演示了Spring Security的AfterInvocationManager
及其具体实现。
像Spring安全性的许多其他部分一样,AfterInvocationManager
有一个单独的具体实现,AfterInvocationProviderManager
,它对AfterInvocationProvider
的列表进行轮询。每个AfterInvocationProvider
允许修改返回对象或抛出AccessDeniedException
。实际上,多个提供程序可以修改对象,因为前一个提供程序的结果被传递给列表中的下一个提供程序。
请注意,如果您正在使用AfterInvocationManager
,您仍然需要配置属性,允许MethodSecurityInterceptor
的AccessDecisionManager
允许操作。
如果您正在使用包含AccessDecisionManager
的典型Spring Security实现,那么没有为特定的安全方法调用定义配置属性将导致每个AccessDecisionVoter
放弃投票。反过来,如果AccessDecisionManager
属性“allowIfAllAbstainDecisions”为false
,则会抛出AccessDeniedException
异常。您可以通过(i)将“allowIfAllAbstainDecisions”设置为true
(尽管通常不建议这样做)或(ii)简单地确保至少有一个配置属性AccessDecisionVoter
将投票授予访问权限。后一种(推荐的)方法通常通过ROLE_USER
或ROLE_AUTHENTICATED
配置属性来实现。
应用程序中的特定角色应该自动“include”其他角色,这是一个常见的需求。例如,在具有“admin”和“user”角色概念的应用程序中,您可能希望管理员能够做普通用户可以做的所有事情。要实现这一点,您可以确保所有管理员用户也都被分配了“user”角色。或者,您可以修改每个需要“user”角色也包括“admin”角色的访问约束。如果您的应用程序中有很多不同的角色,这可能会变得非常复杂。
角色层次结构的使用允许您配置哪些角色(或权限)应该包括其他角色。Spring Security的RoleVoter的一个扩展版本RoleHierarchyVoter
配置了一个RoleHierarchy
,从这个RoleHierarchy
中它可以获得分配给用户的所有“可到达的权限”。一个典型的配置可能是这样的:
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
在这个层次中,我们有四个角色:ROLE_ADMIN>ROLE_STAFF>ROLE_USER>ROLE_GUEST。使用ROLE_ADMIN
进行身份验证的用户,在根据使用上述RoleHierarchyVoter
配置的AccessDecisionManager
评估安全约束时,将表现得好像他们拥有所有四个角色一样。>
符号可以被认为是“包括”的意思。
角色层次结构为简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量提供了一种方便的方法。对于更复杂的需求,您可能希望在应用程序所需的特定访问权限和分配给用户的角色之间定义一个逻辑映射,并在加载用户信息时在这两者之间进行转换。
本节以Servlet体系结构和实现为基础,深入研究授权如何在基于Servlet的应用程序中工作。
FilterSecurityInterceptor为HttpServletRequests
提供了授权。它作为安全过滤器之一被插入到FilterChainProxy中。
FilterSecurityInterceptor
从SecurityContextHolder
获取一个Authentication
。FilterSecurityInterceptor
从传递到FilterSecurityInterceptor
中的HttpServletRequest
、HttpServletResponse
和FilterChain
创建一个FilterInvocation
。FilterInvocation
传递给SecurityMetadataSource
以获得ConfigAttributes
。Authentication
、FilterInvocation
和ConfigAttributes
传递给AccessDecisionManager
。AccessDeniedException
。在本例中,ExceptionTranslationFilter
处理AccessDeniedException
。FilterSecurityInterceptor
继续使用FilterChain
,这允许应用程序正常处理。默认情况下,Spring Security的授权将要求对所有请求进行身份验证。显式配置如下:
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
);
}
我们可以通过按优先级添加更多规则来配置Spring Security,使其具有不同的规则。
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.authorizeRequests(authorize -> authorize
.mvcMatchers("/resources/**", "/signup", "/about").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().denyAll()
);
}
hasRole
方法,所以不需要指定“ROLE_”前缀。hasRole
表达式,所以不需要指定“ROLE_”前缀。Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,除了之前看到的简单使用配置属性和访问决策投票者之外。基于表达式的访问控制构建在相同的架构上,但是允许将复杂的布尔逻辑封装在一个表达式中。
Spring Security使用Spring EL来支持表达式,如果您有兴趣更深入地理解这个主题,那么应该看看它是如何工作的。表达式是用“根对象”作为计算上下文的一部分来计算的。Spring Security使用特定的web和方法安全类作为根对象,以提供内置表达式和对值(如当前主体)的访问。
表达式根对象的基类是SecurityExpressionRoot
。这提供了一些在web和方法安全中都可用的通用表达式。
表达式 | 描述 |
---|---|
hasRole(String role) | 如果当前主体具有指定的角色,则返回true。例如,hasRole(admin) 默认情况下,如果提供的角色不是以’ROLE_'开头,它将被添加。这可以通过修改DefaultWebSecurityExpressionHandler 上的defaultRolePrefix 来定制。 |
… | … |
要使用表达式保护各个url,首先需要将
元素中的use-expressions
属性设置为true
。然后,Spring Security将期望
元素的access
属性包含Spring EL表达式。表达式的计算值应该是一个布尔值,定义是否允许访问。例如:
...
这里我们定义了应用程序的“管理”区域(由URL模式定义)应该只对具有授予的权限“admin”且IP地址与本地子网匹配的用户可用。我们已经在前一节中看到了内置的hasRole
表达式。hasIpAddress
表达式是一个额外的内置表达式,它特定于web安全。它是由WebSecurityExpressionRoot
类定义的,它的一个实例在计算web访问表达式时被用作表达式根对象。该对象还直接在名称request
下公开HttpServletRequest
对象,因此您可以在表达式中直接调用该请求。如果正在使用表达式,那么一个WebExpressionVoter
将被添加到命名空间使用的AccessDecisionManager
中。因此,如果您不使用名称空间而想要使用表达式,则必须将其中一个添加到您的配置中。
如果您希望扩展可用的表达式,您可以很容易地引用您公开的任何Spring Bean。例如,假设你有一个名为webSecurity
的Bean,它包含以下方法签名:
public class WebSecurity {
public boolean check(Authentication authentication, HttpServletRequest request) {
...
}
}
你可以使用以下方法引用该方法:
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
...
)
有时,能够在URL中引用路径变量是件好事。例如,考虑一个基于rest的应用程序,它通过/user/{userId}
格式的URL路径查找用户。
通过将路径变量放置在模式中,您可以轻松地引用它。例如,如果你有一个名为webSecurity
的Bean,它包含以下方法签名:
public class WebSecurity {
public boolean checkUserId(Authentication authentication, int id) {
...
}
}
你可以使用以下方法引用该方法:
http
.authorizeRequests(authorize -> authorize
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);
在这个配置中,匹配的url将把路径变量传递(并将其转换为checkUserId
方法)。例如,如果URL是/user/123/resource
,那么传入的id将是123
。
方法安全性比简单的允许或拒绝规则要复杂一些。Spring Security 3.0引入了一些新的注释,以便全面支持表达式的使用。
有四种注释支持表达式属性,以允许调用前和调用后的授权检查,并支持对提交的集合参数或返回值进行过滤。它们是@PreAuthorize
, @PreFilter
, @PostAuthorize
和@PostFilter
。它们的使用是通过global-method-security
命名空间元素启用的:
最明显有用的注释是@PreAuthorize
,它决定是否可以实际调用方法。例如(来自“Contacts”示例应用程序)
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
这意味着只允许角色为“ROLE_USER”的用户访问。显然,使用传统配置和所需角色的简单配置属性可以很容易地实现相同的功能。但:
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
这里我们实际上使用了一个方法参数作为表达式的一部分来决定当前用户是否拥有给定联系人的“admin”权限。内置的hasPermission()
表达式通过应用程序上下文链接到Spring Security ACL模块,如下所示。您可以通过名称访问任何方法参数作为表达式变量。
Spring Security可以通过多种方式解析方法参数。Spring Security使用DefaultSecurityParameterNameDiscoverer
发现参数名称。默认情况下,将对一个方法整体尝试以下选项。
@P
注释出现在该方法的单个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口很有用,因为JDK不包含任何关于参数名的信息。例如:import org.springframework.security.access.method.P;
...
@PreAuthorize("#c.name == authentication.name")
public void doSomething(@P("c") Contact contact);
在幕后,这是使用AnnotationParameterNameDiscoverer
实现的,它可以自定义以支持任何指定注释的value属性。
@Param
注释出现在该方法的至少一个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口很有用,因为JDK不包含任何关于参数名的信息。例如:import org.springframework.data.repository.query.Param;
...
@PreAuthorize("#n == authentication.name")
Contact findContactByName(@Param("n") String name);
在幕后,这是使用AnnotationParameterNameDiscoverer
实现的,它可以自定义以支持任何指定注释的value属性。
表达式中可以使用任何Spring-EL功能,因此您也可以访问参数上的属性。例如,如果您希望一个特定的方法只允许访问用户名与联系人的用户名匹配的用户,那么您可以这样写
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
这里我们访问另一个内置表达式authentication
,它是存储在安全上下文中的authentication
。您还可以使用表达式principal
直接访问它的“principal”属性。值通常是一个UserDetails
实例,所以您可以使用像principal.username
或principal.enabled
这样的表达式。
不太常见的情况是,您可能希望在调用该方法后执行访问控制检查。这可以使用@PostAuthorize
注释来实现。要访问方法的返回值,请在表达式中使用内置名称returnObject
。
Spring Security支持使用表达式过滤集合、数组、映射和流。这通常在方法的返回值上执行。例如:
@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List getAll();
当使用@PostFilter
注释时,Spring Security遍历返回的集合或映射,并删除所提供表达式为false的任何元素。对于数组,将返回包含筛选过的元素的新数组实例。名称filterObject
引用集合中的当前对象。当map被使用时,它将引用当前的Map.Entry
对象,这允许在表达式中使用filterObject.key
或filterObject.value
。您还可以使用@PreFilter
在方法调用之前进行筛选,尽管这是一个不太常见的需求。语法是相同的,但是如果有多个参数是集合类型,那么您必须使用该注释的filterTarget
属性按名称选择一个。
注意,筛选显然不能替代数据检索查询的调优。如果您正在过滤大型集合并删除许多条目,那么这可能是低效的。
有一些特定于方法安全性的内置表达式,我们在上面已经看到了它们的使用。filterTarget
和returnValue
值非常简单,但是hasPermission()
表达式的使用值得进一步研究。
hasPermission()
表达式被委托给PermissionEvaluator
的实例。它的目的是在表达式系统和Spring Security的ACL系统之间架起桥梁,允许您基于抽象权限对域对象指定授权约束。它对ACL模块没有显式依赖关系,因此如果需要,可以将其替换为其他实现。该接口有两个方法:
boolean hasPermission(Authentication authentication, Object targetDomainObject,
Object permission);
boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission);
它直接映射到表达式的可用版本,但不提供第一个参数(Authentication
对象)。第一个是在已经加载了域对象(对其访问进行控制)的情况下使用的。如果当前用户拥有该对象的权限,则expression将返回true。第二个版本用于未加载对象,但其标识符已知的情况。还需要域对象的抽象“类型”说明符,以允许加载正确的ACL权限。传统上这是对象的Java类,但只要它与权限加载方式一致,就不必这样做。
要使用hasPermission()
表达式,必须在应用程序上下文中显式配置PermissionEvaluator
。这看起来像这样:
其中myPermissionEvaluator
是实现PermissionEvaluator
的bean。通常这将是来自ACL模块的实现,该模块被称为AclPermissionEvaluator
。有关更多细节,请参见“Contacts”示例应用程序配置。
您可以使用元注释来保证方法安全性,从而使代码更具可读性。如果您发现在整个代码库中重复相同的复杂表达式,这尤其方便。例如,考虑以下情况:
@PreAuthorize("#contact.name == authentication.name")
我们可以创建一个元注释来代替它,而不是到处重复此操作。
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}
元注释可以用于任何Spring Security方法安全注释。为了与规范保持一致,JSR-250注释不支持元注释。
在Spring Security 2.0之前,保护MethodInvocation需要大量的常规配置。现在推荐的方法是使用名称空间配置。通过这种方式,方法安全基础结构bean将自动为您配置,因此您不需要真正了解实现类。我们将简要介绍这里涉及到的类。
方法安全性通过MethodSecurityInterceptor
强制执行,它保护MethodInvocation
。根据配置方法的不同,拦截器可能特定于单个bean,也可能在多个bean之间共享。拦截器使用MethodSecurityMetadataSource
实例获取应用于特定方法调用的配置属性。MapBasedMethodSecurityMetadataSource
用于存储以方法名(可以通配符)为键的配置属性,当使用
或
元素在应用程序上下文中定义这些属性时将在内部使用。其他实现将用于处理基于注释的配置。
当然,你可以直接在你的应用上下文中配置MethodSecurityInterceptor
,以便与Spring AOP的代理机制一起使用:
AspectJ安全拦截器与前一节讨论的AOP Alliance安全拦截器非常相似。实际上,我们将在本节中只讨论它们之间的区别。
AspectJ拦截器被命名为AspectJSecurityInterceptor
。AOP Alliance安全拦截器依赖于Spring应用程序上下文通过代理将其编织到安全拦截器中,与之不同的是,AspectJSecurityInterceptor
是通过AspectJ编译器进行编织的。在同一个应用程序中使用这两种类型的安全拦截器并不少见,AspectJSecurityInterceptor
用于域对象实例安全,AOP Alliance MethodSecurityInterceptor
用于服务层安全。
让我们首先考虑AspectJSecurityInterceptor
是如何在Spring应用程序上下文中配置的:
正如您所看到的,除了类名之外,AspectJSecurityInterceptor
与AOP Alliance安全拦截器完全相同。实际上,这两个拦截器可以共享相同的securityMetadataSource
,因为SecurityMetadataSource
使用java.lang.reflect.method
,而不是特定于AOP库的类。当然,您的访问决策可以访问相关的特定于AOP库的调用(如MethodInvocation
或JoinPoint
),因此在做出访问决策时可以考虑一系列附加条件(如方法参数)。
接下来需要定义AspectJ方面。例如:
package org.springframework.security.samples.aspectj;
import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;
public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {
private AspectJSecurityInterceptor securityInterceptor;
pointcut domainObjectInstanceExecution(): target(PersistableEntity)
&& execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);
Object around(): domainObjectInstanceExecution() {
if (this.securityInterceptor == null) {
return proceed();
}
AspectJCallback callback = new AspectJCallback() {
public Object proceedWithObject() {
return proceed();
}
};
return this.securityInterceptor.invoke(thisJoinPoint, callback);
}
public AspectJSecurityInterceptor getSecurityInterceptor() {
return securityInterceptor;
}
public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {
this.securityInterceptor = securityInterceptor;
}
public void afterPropertiesSet() throws Exception {
if (this.securityInterceptor == null)
throw new IllegalArgumentException("securityInterceptor required");
}
}
}
在上面的示例中,安全拦截器将应用于PersistableEntity
的每个实例,它是一个没有显示的抽象类(您可以使用任何其他类或切入点表达式)。对于那些好奇的人,需要AspectJCallback
,因为proceed();
语句只有在around()
语句体中才有特殊含义。当AspectJSecurityInterceptor
希望目标对象继续时,它会调用这个匿名的AspectJCallback
类。
您需要配置Spring来加载方面,并将它与AspectJSecurityInterceptor
连接起来。实现这一点的bean声明如下所示:
就是这样!现在您可以在应用程序的任何地方创建bean,使用您认为合适的任何方法(例如new Person();
),它们将应用安全拦截器。
从版本2.0开始,Spring Security已经大大改进了对为服务层方法添加安全性的支持。它提供了对JSR-250注释安全性的支持,以及框架的原始@Secured
注释。从3.0开始,您还可以使用新的基于表达式的注释。您可以使用intercept-methods
元素来修饰bean声明,将安全性应用于单个bean,或者可以使用AspectJ样式切入点来保护整个服务层中的多个bean。
我们可以在任何@Configuration
实例上使用@EnableGlobalMethodSecurity
注释来启用基于注释的安全性。例如,下面将启用Spring Security的@Secured
注释。
向方法(在类或接口上)添加注释将相应地限制对该方法的访问。Spring Security的原生注释支持为该方法定义了一组属性。这些将被传递给AccessDecisionManager
以便它做出实际的决策:
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
支持JSR-250注释可以使用
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}
它们是基于标准的,允许应用简单的基于角色的约束,但是不具有Spring Security的原生注释的强大功能。要使用新的基于表达式的语法,您将使用
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}
而等价的Java代码将是
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}
有时,您可能需要执行比@EnableGlobalMethodSecurity
注释allow所能执行的更复杂的操作。对于这些实例,您可以扩展GlobalMethodSecurityConfiguration
,确保@EnableGlobalMethodSecurity
注释出现在您的子类上。例如,如果你想提供一个自定义的MethodSecurityExpressionHandler
,你可以使用以下配置:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
// ... create and return custom MethodSecurityExpressionHandler ...
return expressionHandler;
}
}
有关可被覆盖方法的其他信息,请参阅GlobalMethodSecurityConfiguration
Javadoc。
此元素用于在应用程序中启用基于注释的安全性(通过在元素上设置适当的属性),还用于将安全性切入点声明组合在一起,这些声明将应用于整个应用程序上下文。你应该只声明一个
元素。下面的声明将启用Spring Security的@Secured
支持:
向方法(在类或接口上)添加注释将相应地限制对该方法的访问。Spring Security的原生注释支持为该方法定义了一组属性。这些将被传递给AccessDecisionManager
以便它做出实际的决策:
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
支持JSR-250注释可以使用
它们是基于标准的,允许应用简单的基于角色的约束,但是不具有Spring Security的原生注释的强大功能。要使用新的基于表达式的语法,您将使用
而等价的Java代码将是
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}
如果您需要定义简单的规则,而不仅仅是根据用户权限列表检查角色名,那么基于表达式的注释是一个很好的选择。
带注释的方法只对定义为Spring bean的实例(在启用方法安全性的同一应用程序上下文中)安全。如果您希望保护不是由Spring创建的实例(例如,使用
new
操作符),那么您需要使用AspectJ。
您可以在同一个应用程序中启用多个注释类型,但任何接口或类只能使用一种类型,否则将无法定义行为。如果找到两个适用于特定方法的注释,则只应用其中一个。
protect-pointcut
的使用尤其强大,因为它允许您仅用一个简单的声明就可以对许多bean应用安全性。考虑以下例子:
这将保护在应用程序上下文中声明的bean上的所有方法,这些bean的类位于com.mycompany
包中,类名以“Service”结尾。只有具有ROLE_USER
角色的用户才能调用这些方法。与URL匹配一样,最特定的匹配必须放在切入点列表的前面,因为将使用第一个匹配表达式。安全注释优先于切入点。
复杂的应用程序通常需要定义访问权限,而不是简单地在web请求或方法调用级别上定义访问权限。相反,安全决策需要包含who (Authentication
)、where (MethodInvocation
)和what (SomeDomainObject
)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。
假设您正在为一家宠物诊所设计一个应用程序。基于spring的应用程序主要有两组用户:宠物诊所的工作人员,以及宠物诊所的客户。工作人员将有权访问所有数据,而您的客户将只能看到他们自己的客户记录。为了让它更有趣一点,您的客户可以允许其他用户查看他们的客户记录,比如他们的“幼犬学前班”导师或当地“Pony Club”的主席。以Spring Security为基础,您可以使用以下几种方法:
Customer
域对象实例中的集合,以确定哪些用户具有访问权限。通过使用SecurityContextHolder.getContext().getAuthentication()
,您将能够访问Authentication
对象。AccessDecisionVoter
来从存储在Authentication
对象中的GrantedAuthority[]
强制安全性。这意味着AuthenticationManager
需要使用自定义的GrantedAuthority[]
来填充Authentication
,这些自定义的GrantAuthority[]
表示主体可以访问的每个Customer
域对象实例。AccessDecisionVoter
来强制安全性并直接打开目标Customer
域对象。这意味着投票者需要访问一个允许它检索Customer
对象的DAO。然后,它将访问Customer
对象的已批准用户集合,并做出适当的决策。每一种方法都是完全合法的。但是,第一种方法将授权检查与业务代码结合起来。这方面的主要问题包括单元测试的难度增加,以及在其他地方重用Customer
授权逻辑将变得更加困难。从Authentication
对象获取GrantAuthority[]
也可以,但是不能扩展到大量的Customer
。如果一个用户可能能够访问5000个Customer
(在本例中不可能,但是想象一下,如果它是一个大型Pony Club的流行兽医!),那么构造Authentication
对象所消耗的内存量和所需的时间将是不受欢迎的。最后一种方法,直接从外部代码打开Customer
,可能是这三种方法中最好的。它实现了关注点的分离,并且不会误用内存或CPU周期,但是它仍然是低效的,因为AccessDecisionVoter
和最终的业务方法本身都将执行对负责检索Customer
对象的DAO的调用。每个方法调用两次访问显然是不可取的。此外,对于列出的每种方法,您都需要从头编写自己的访问控制列表(ACL)持久性和业务逻辑。
幸运的是,还有另一种选择,我们将在下面讨论。
Spring Security的ACL服务在spring-security-acl-xxx.jar
中提供。您需要将这个JAR添加到您的类路径中,以使用Spring Security的域对象实例安全功能。
Spring Security的域对象实例安全功能以访问控制列表(ACL)的概念为中心。系统中的每个域对象实例都有自己的ACL, ACL记录了谁可以和不能使用该域对象的详细信息。考虑到这一点,Spring Security为您的应用程序提供了三个与acl相关的主要功能:
正如第一个要点所指出的,Spring Security ACL模块的主要功能之一是提供检索ACL的高性能方法。这个ACL存储库功能非常重要,因为系统中的每个域对象实例可能有多个访问控制项,并且每个ACL可能以树状结构从其他ACL继承(Spring Security可以开箱即用,并且非常常用)。Spring Security的ACL功能经过精心设计,提供了高性能的ACL检索,以及可插入的缓存、最小化死锁的数据库更新、独立于ORM框架(我们直接使用JDBC)、适当的封装和透明的数据库更新。
已知数据库是ACL模块操作的中心,让我们研究一下实现中默认使用的四个主要表。在一个典型的Spring Security ACL部署中,表的大小顺序如下所示,其中行数最多的表列在最后:
GrantedAuthority
的标志。因此,每个惟一主体或GrantedAuthority
对应一行。当在接收权限的上下文中使用时,SID通常被称为“接收者”。正如上一段提到的,ACL系统使用整数位屏蔽。不要担心,您不需要了解使用ACL系统时位转移的细节,但可以这样说,我们有32位可以打开或关闭。每一个位代表权限,默认情况下,权限阅读(0),写(1),创建(2),删除(第3位)和管理(4)。很容易实现自己的如果你想使用其他的权限,允许实例和剩余的ACL框架将没有知识的扩展。
重要的是要理解,系统中域对象的数量与我们选择使用整数位屏蔽的事实绝对没有关系。虽然您有32位可用于权限,但您可能有数十亿域对象实例(这意味着ACL_OBJECT_IDENTITY中有数十亿行,很可能还有ACL_ENTRY)。我们之所以提出这一点,是因为我们发现有时人们会错误地认为他们需要为每个潜在的域对象分配一点,但事实并非如此。
现在,我们已经基本概述了ACL系统的功能,以及它在表结构上的样子,接下来让我们研究一下关键的接口。关键接口有:
Acl
:每个域对象都有且只有一个Acl
对象,该对象内部保存AccessControlEntry
,并知道Acl
的所有者。Acl不直接引用域对象,而是引用ObjectIdentity
。Acl
存储在ACL_OBJECT_IDENTITY表中。AccessControlEntry
:一个Acl
包含多个AccessControlEntry
,在框架中通常缩写为ACEs。每个ACE都指向一个特定的Permission
、Sid
和Acl
元组。ACE也可以是授予的或非授予的,并且包含审计设置。ACE存储在ACL_ENTRY表中。Permission
:权限表示特定的不可变位掩码,提供了方便的位掩码和信息输出函数。上面显示的基本权限(位0到4)包含在BasePermission
类中。Sid
: ACL模块需要引用主体和GrantedAuthority[]
。一个间接级别是由Sid
接口提供的,它是“安全标识”的缩写。常见的类包括PrincipalSid
(表示Authentication
对象中的主体)和GrantedAuthoritySid
。安全标识信息存储在ACL_SID表中。ObjectIdentity
:每个域对象在ACL模块内部由一个ObjectIdentity
表示。默认实现称为ObjectIdentityImpl
。AclService
:检索适用于给定ObjectIdentity
的Acl。在包含的实现(JdbcAclService
)中,检索操作被委托给一个LookupStrategy
。LookupStrategy
为检索ACL信息提供了高度优化的策略,使用批处理检索(BasicLookupStrategy
)并支持利用物化视图、分层查询和类似的以性能为中心的非ansi SQL功能的自定义实现。MutableAclService
:允许显示修改后的Acl
以进行持久化。如果您不希望使用此接口,则无需使用该接口。请注意,我们的开箱即用的AclService
和相关的数据库类都使用ANSI SQL。因此,这应该适用于所有主要数据库。在撰写本文时,该系统已经成功地使用Hypersonic SQL、PostgreSQL、Microsoft SQL Server和Oracle进行了测试。
Spring Security附带的两个示例演示了ACL模块。第一个是联系人样本,另一个是文档管理系统(DMS)样本。我们建议看一看这些例子。
要开始使用Spring Security的ACL功能,您需要将ACL信息存储在某个地方。这就需要使用Spring实例化DataSource
。然后DataSource
被注入到JdbcMutableAclService
和BasicLookupStrategy
实例中。后者提供高性能ACL检索功能,前者提供变异功能。关于示例配置,请参考随Spring Security一起发布的一个示例。您还需要使用上一节中列出的四个特定于ACL的表填充数据库(请参阅ACL示例以获取适当的SQL语句)。
一旦您创建了所需的模式并实例化了JdbcMutableAclService
,接下来您需要确保您的域模型支持与Spring Security ACL包的互操作性。希望ObjectIdentityImpl
将被证明是足够的,因为它提供了大量可以使用它的方式。大多数人将拥有包含public Serializable getId()
方法的域对象。如果返回类型是long,或者与long兼容(例如int),你会发现你不需要进一步考虑ObjectIdentity
问题。ACL模块的许多部分依赖于long标识符。如果你不使用long(或int, byte等),很有可能你需要重新实现一些类。我们不打算在Spring Security的ACL模块中支持非long标识符,因为long标识符已经与所有数据库序列兼容,这是最常见的标识符数据类型,并且有足够的长度来适应所有常见的使用场景。
下面的代码片段展示了如何创建一个Acl
,或修改一个现有的Acl
:
// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;
// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}
// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);
在上面的示例中,我们检索与标识符为44的“Foo”域对象关联的ACL。然后添加一个ACE,以便名为“Samantha”的主体可以“管理”该对象。除了insertAce方法之外,代码片段相对来说是不言自明的。insertAce方法的第一个参数确定新条目将插入Acl中的哪个位置。在上面的示例中,我们只是将新的ACE放在现有ACE的末尾。最后一个参数是一个布尔值,指示ACE是授予还是拒绝。大多数情况下,它将授予(true),但如果拒绝(false),权限将被有效地阻止。
Spring Security没有提供任何特殊集成来自动创建、更新或删除作为DAO或存储库操作的一部分的acl。相反,您需要为您的单个域对象编写如上所示的代码。值得考虑在服务层上使用AOP来自动集成ACL信息和服务层操作。在过去,我们发现这是一种相当有效的方法。
使用上述技术在数据库中存储一些ACL信息后,下一步是实际使用ACL信息作为授权决策逻辑的一部分。你有很多选择。您可以编写自己的AccessDecisionVoter
或AfterInvocationProvider
,它们分别在方法调用之前或之后触发。这些类将使用AclService
检索相关ACL,然后调用ACL.isGranted(Permission[] Permission, Sid[] Sid, boolean administrativeMode)
决定是否授予或拒绝权限。或者,你可以使用我们的AclEntryVoter
、AclEntryAfterInvocationProvider
或AclEntryAfterInvocationCollectionFilteringProvider
类。所有这些类都提供了一种基于声明的方法来在运行时计算ACL信息,使您无需编写任何代码。请参考示例应用程序了解如何使用这些类。