spring security 用户认证
spring security 提供了完整的Authentication
支持, 本文主要讨论:
架构组件
本节描述了用于 Servlet 身份验证的 Spring Security 的主要架构组件。如果您需要解释这些部分如何组合在一起的具体流程,请查看身份验证机制特定部分
- SecurityContextHolder -
SecurityContextHolder
是Spring Security存储身份验
证主体的详细信息的地方. - SecurityContext- 从
SecurityContextHolder
获得,包含当前已认证用户的认证信息. - Authentication - 可以是
AuthenticationManager
的输入(用户录入的信息,作为身份认证的凭据), 也可以是SecurityContext
中的当前用户. - GrantedAuthority -
Authentication
中授予主体的权限 (如: roles, scopes, etc.) - AuthenticationManager - 定义身份身份认证的api.
- ProviderManager -
AuthenticationManager
的一个常见实现. - AuthenticationProvider -
ProviderManager
用来执行指定类型的身份认证. - Request Credentials with
AuthenticationEntryPoint
- 用来从客户端获取用户凭证 (i.e. 重定向到登录页, 发送WWW-Authenticate
Header 等.) - AbstractAuthenticationProcessingFilter - 做身份的父
Filter
. 同时也是一个关于身份验证的总体流程以及各个部分如何协同工作的很好的建议。
认证机制
Username and Password
- 用户名/密码 认证OAuth 2.0 Login
- OAuth 2.0 登录以及 非标准的OAuth 2.0 登录 (i.e. GitHub)SAML 2.0 Login
- SAML 2.0登录Central Authentication Server (CAS)
- CASRemember Me
- 记住我JAAS Authentication
- JAASOpenID
- OpenID AuthenticationPre-Authentication Scenarios
- 预认证场景。 就是用其它机制(自己实现的、或者JEE的,反正不是spring security的)实现了身份认证, 但是要实用spring security 作 Authorization和 漏洞防护的情况。X509 Authentication
- X509 Authentication
SecurityContextHolder
Spring security 认证模型 的核心: SecurityContextHolder
, 它里包含了SecurityContext
。
SecurityContextHolder
是存储已认证用户的地方。 判断用户登录时,Spring Security 不关心SecurityContextHolder
值是怎么写进去的, 只关心里面有没有这个值, 如果有就认为这个用户登录了。
表示用户已通过身份验证的最简单方法是直接设置 SecurityContextHolder。
例: 设置SecurityContextHolder
1 | SecurityContext context = SecurityContextHolder.createEmptyContext(); |
- 先创建一个空的
SecurityContext
。 因为如果用SecurityContextHolder.getContext().setAuthentication(authentication)
方式的话,在多线程下容易出现竞争。 - 再创建一个
Authentication
对象。 spring security 不关心Authentication
的具体实现类是什么。这里使用了很简单的TestingAuthenticationToken
, 大多数实际情况使用UsernamePasswordAuthenticationToken(userDetails, password, authorities)
。 - 最后把这个
SecurityContext
设置到SecurityContextHolder
, spring security 会使用这些信息来做身份认证。
访问SecurityContextHolder
可以获取已认证主体的信息。
例:访问当前认证过的用户
1 | SecurityContext context = SecurityContextHolder.getContext(); |
默认情况下, SecurityContextHolder
是通过ThreadLocal
实现的, 所以SecurityContext
同一个线程里的方法都是可用的, 不需要把SecurityContext
作为参数传递。
SecurityContextHolder
的实现方式是可以改变的,两种方法: 设置系统属性spring.security.strategy
和 调用SecurityContextHolder.setStrategyName(String strategyName)
的静态方法。
支持的实现有:
MODE_THREADLOCAL
: 默认实现MODE_INHERITABLETHREADLOCAL
: 子线程(有SecurityContext
的线程创建的)继承当前的SecurityContext
MODE_GLOBAL
: 独立应用,只用一个用户那种。客户端应用这种, 每个应用实例只登录一个用户, 实用这种场景
SecurityContext
SecurityContext
从SecurityContextHolder
获取,包含了一个 Authentication
对象
Authentication
Authentication
在Spring Security中主要有两个作用:
- 作为
AuthenticationManager
的入参, 提供用户录入的身份凭据信息,以便身份认证时使用, 这时候isAuthenticated()
返回false
- 代表当前认证通过的用户。 当前
Authentication
可从SecurityContext
中获取。
Authentication
包含:
principal
: 主体。用户唯一标识,使用用户/密码认证时,通常是UserDetails
实例。credentials
: 凭据。通常是密码。多数时候用户认证通过后会清除掉, 避免泄漏。authorities
: 授权。GrantedAuthority
是用户被授予的高级别的权限。如role或者 scopes。
GrantedAuthority
GrantedAuthority
是用户被授予的高级别的权限。如role或者 scopes。
可以用Authentication.getAuthorities()
方法获取,此方法返回GrantedAuthority
集合。GrantedAuthority
就是已授予给主体的权限。通常是角色,如ROLE_ADMINISTRATOR
、 ROLE_HR_SUPERVISOR
。这些角色是稍后在web authorization
、method authorization
、domain object authorization
的时候配置的。Spring Security 的其他部分能够解释这些权限,并期望它们存在。基于用户名/密码
方式认证的时候,GrantedAuthoritys
通常通过 UserDetailsService
加载.
通常情况下GrantedAuthority
是应用范围的权限, 而不是具体到特定的领域对象上。因此,您不太可能使用 GrantedAuthority
来表示对 id为54的Employee对象的权限, 因为如果存在成千上万的这种权限的话,那内存很快就耗尽了, 至少在认证的时候会非常慢。当然spring security为这种常见需求处理做了明确的设计的, 使用domain object security功能, 可达到此目的。
AuthenticationManager
AuthenticationManager
一个API,定义了 security Filters
如何执行认证。认证处理者(如:Spring Security Filters
)调用AuthenticationManager
返回Authentication
对象,并将其设置到SecurityContextHolder
里。如果没有集成Spring Security’s Filters
,那可以直接设置SecurityContextHolder
, 不需要AuthenticationManager
。
虽然 AuthenticationManager
的实现可以是任何东西,但最常见的实现是 ProviderManager
。
ProviderManager
ProviderManager
是AuthenticationManager
最常用的实现。ProviderManager
把认证工作委派给一组 AuthenticationProvider
。每个AuthenticationProvider
都有机会指明认证是失败、成功,或者它不能决定允许下游AuthenticationProvider
来决定。如果没有AuthenticationProvider
能决定,那认证会失败并附带ProviderNotFoundException
这样一个特殊的AuthenticationException
,表明ProviderManager
不能支持传入的认证类型。
实际使用时, 每个AuthenticationProvider
都知道如何执行指定类型的认证。 如一个验证用户名/密码,一个认证SAML assertion 。这样就可以让每个AuthenticationProvider
只处理特定的一个认证类型,同时只用一个AuthenticationManager
就可以支持多种类型的认证。
ProviderManager
还允许配置一个可选的父 AuthenticationManager
,在没有 AuthenticationProvider
可以执行身份验证的情况下咨询它。父级AuthenticationManager
可以是任何类型的 AuthenticationManager
,但它通常是 ProviderManager
的实例。
多个 ProviderManager
实例可能共享同一个父 AuthenticationManager
。 这在有多个 SecurityFilterChain 实例具有一些共同的身份验证(共享父 AuthenticationManager),但是不同的身份验证机制(不同的 ProviderManager 实例的情况下有些常见。
默认情况下,ProviderManager
会清理认证成功返回的Authentication
对象中的凭据信息(如密码)。 这样可以防止像密码这样的信息被过长时间的保留在session中。
这样做话, 使用缓存中的user 对象就可能会产生问题。 因为缓存中的用户信息对象(如UserDetails
实例),可能已经被清理了凭证信息, 那后面再认证这个用户的时候,如果取到的缓存中的user值,就没办法认证了。如果使用缓存的话,需要注意这一点。 两个解决办法: 1. 创建一个user对象备份(可以在缓存实现中创建, 也可在AuthenticationProvider
实现), 用这个备份来创建Authentication
对象。2. 禁用ProviderManager
的eraseCredentialsAfterAuthentication
属性,ProviderManager.setEraseCredentialsAfterAuthentication(boolean eraseSecretData)
。
AuthenticationProvider
可以将多个 AuthenticationProviders
注入到 ProviderManager
中。每个 AuthenticationProvider
执行特定类型的身份验证。例如,DaoAuthenticationProvider
支持基于用户名/密码的身份验证,而 JwtAuthenticationProvider
支持对 JWT 令牌进行身份验证。
用AuthenticationEntryPoint
获取用户凭证信息
AuthenticationEntryPoint
用来发送一个HTTP response
,从客户端获取用户凭证信息。
有时,客户端会主动包含凭据(例如用户名/密码)来请求资源。在这些情况下,Spring Security 不需要提供从客户端请求凭据的 HTTP 响应,因为它们已经包含在内。
在其他情况下,客户端将对他们无权访问的资源发出未经身份验证的请求。在这种情况下,AuthenticationEntryPoint
的实现用于从客户端请求凭据。如:执行重定向到登录页面或使用 WWW-Authenticate
标头等进行响应。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
是认证用户凭据的父Filter
。
- 当用户提交凭证信息的时候,
AbstractAuthenticationProcessingFilter
从HttpServletRequest
创建一个Authentication
对象以进行身份认证。Authentication
对象的类型依赖于AbstractAuthenticationProcessingFilter
子类。如UsernamePasswordAuthenticationFilter
根据在HttpServletRequest
中提交的用户名和密码创建一个UsernamePasswordAuthenticationToken
。 Authentication
对象传给AuthenticationManager
进行认证。- 如果认证失败:
- 清除SecurityContextHolder
- 调用
RememberMeServices.loginFai
l。如果没有配置记住我
,这个方法什么都不干。 - 调用
AuthenticationFailureHandler
。
- 如果成功:
SessionAuthenticationStrategy
收到新登录通知Authentication
设置在SecurityContextHolder
上,然后SecurityContextPersistenceFilter
将SecurityContext
保存到HttpSession
。- 调用
RememberMeServices.loginSuccess
, 如果没有配置记住我
功能, 则此方法撒都不干 ApplicationEventPublisher
发布一个InteractiveAuthenticationSuccessEvent
事件。
用户名/密码 认证
用户名/密码 是最常见的认方式, spring security 为此提供了全面的支持。
读取 Username & Password
Spring security 提供了以下内建机制从HttpServletRequest
读取用户名/密码:
- 表单登录
- Basic Authentication
- Digest Authentication
Storage Mechanisms
每种读取方式, 都可以使用以下任意的存储机制:
In-Memory Authentication
简单的存到内存里- 用
JDBC Authentication
存到关系数据库 UserDetailsService
实现自定义存储LDAP Authentication
存储 (不太了解)
表单登录
Spring Security 支持通过 html 表单提供用户名和密码。
看一下Spring Security里基于表单登录的流程, 首先是如何重写向到登录表单的:
- 用户向其未授权的资源
/private
发出未经身份验证的请求 - Spring Security 的
FilterSecurityInterceptor
通过抛出AccessDeniedException
指示未经身份验证的请求被拒绝。 - 由于用户未通过身份验证,
ExceptionTranslationFilter
启动启动身份验证并使用已配置的AuthenticationEntryPoint
将重定向发送到登录页面。在大多数情况下,AuthenticationEntryPoint
是LoginUrlAuthenticationEntryPoint
的一个实例。 - 然后浏览器将重定向到登录页面。
- 应用程序渲染登录页面
提交用户名和密码后,UsernamePasswordAuthenticationFilter
会验证用户名和密码。UsernamePasswordAuthenticationFilter
扩展了 AbstractAuthenticationProcessingFilter
,所以这个图应该看起来非常相似。
- 用户提交用户名和密码,
UsernamePasswordAuthenticationFilter
从 HttpServletRequest 中提取用户名和密码来创建一个UsernamePasswordAuthenticationToken
(Authentication
的一种实现)。 - 然后,将
UsernamePasswordAuthenticationToken
传入AuthenticationManager
进行身份验证。AuthenticationManager
的实现细节取决于用户信息的存储方式。 - 如果认证失败:
- 清除SecurityContextHolder
- 调用
RememberMeServices.loginFai
。如果没有配置记住我
,这个方法什么都不干。 - 调用
AuthenticationFailureHandler
。
- 如果成功:
SessionAuthenticationStrategy
收到新登录通知Authentication
设置在SecurityContextHolder
上,然后SecurityContextPersistenceFilter
将SecurityContext
保存到HttpSession
。- 调用
RememberMeServices.loginSuccess
, 如果没有配置记住我
功能, 则此方法撒都不干 ApplicationEventPublisher
发布一个InteractiveAuthenticationSuccessEvent
事件。- 调用
AuthenticationSuccessHandler
通常,这是一个SimpleUrlAuthenticationSuccessHandler
,它会把页面重写向到ExceptionTranslationFilter
保存的请求去,这个请求是前面重写向到登录页时保存的 ,也就是用户本来想访问的页面。
默认情况下 Spring Security 启用表单登录。但是,一旦提供了任何基于 servlet 的配置,就必须明确提供基于表单的登录。可以在下面找到一个最小的、显式的 Java 配置:
1 | protected void configure(HttpSecurity http) { |
在这个配置中,Spring Security 将呈现一个默认的登录页面。大多数生产应用程序都需要自定义登录表单。
下面的配置演示了如何提供自定义登录表单。
1 | protected void configure(HttpSecurity http) throws Exception { |
当在 Spring Security 配置中指定登录页面时,需要应用程序自己写登录页面。下面是一个 Thymeleaf 模板,它生成一个符合 /login 登录页面的 HTML 登录表单:
1 | <!DOCTYPE html> |
关于默认 HTML 表单有几个关键点:
- 表单应该使用
POST
方式提交到/login
- 表单要包含
CSRF Token
,Thymeleaf
会自动添加 - 用户名参数名:
username
, 密码参数名:password
- 传入页面的参数如果有名为
error
的参数,表示用户名/密码认证失败(这种是用户提交登录,但是信息不对, 重新返回登录页面时,应用返回的错误信息) - 传入页面的参数如果有名为
logout
的参数, 表示用户退出登录成功
许多用户只需要自定义登录页面即可。但是,如果需要,以上所有内容都可以通过其他配置进行自定义。
如果您使用的是 Spring MVC,您将需要一个控制器来将 GET /login
映射到我们创建的登录模板。一个最小的示例 LoginController
可以在下面看到:
1 | @Controller |
Basic Authentication
暂不需要
Digest Authentication
暂不需要
In-Memory Authentication
暂不需要
JDBC Authentication
需要按照spring security的要求建表。暂不需要
UserDetails
UserDetails
由 UserDetailsService
返回。 DaoAuthenticationProvider
验证 UserDetails
,然后返回具有主体的Authentication
,该主体是配置的 UserDetailsService
返回的 UserDetails
。
UserDetailsService
DaoAuthenticationProvider
使用 UserDetailsService
来检索用户名、密码和其他属性,以使用用户名和密码进行身份验证。 Spring Security 提供了 UserDetailsService
的内存和 JDBC 实现。
可以通过将自定义 UserDetailsService
公开为 bean 来定义自定义身份验证。例如,假设 CustomUserDetailsService
实现 UserDetailsService
,以下将自定义身份验证:
注意: 这仅在未填充 AuthenticationManagerBuilder
且未定义 AuthenticationProviderBean
时使用。
1 | @Bean |
PasswordEncoder
Spring Security 的 servlet 通过与 PasswordEncoder
集成来支持安全存储密码。可以通过公开 PasswordEncoder
Bean 来自定义 Spring Security 使用的 PasswordEncoder
实现。
DaoAuthenticationProvider
DaoAuthenticationProvider
是一个 AuthenticationProvider
实现,它利用 UserDetailsService
和 PasswordEncoder
来验证用户名和密码。
我们来看看 DaoAuthenticationProvider
在 Spring Security 中是如何工作的。该图详细说明了AuthenticationManager
读取用户名密码时如何工作。
- 认证相关的
Filter
(不同认证类型Filter
不同,用户名/密码方式是UsernamePasswordAuthenticationFilter
)传递一个UsernamePasswordAuthenticationToken
给AuthenticationManager(
具体实现类是ProviderManager
)。 ProviderManager
配置了一个类型为DaoAuthenticationProvider
的AuthenticationProvider
DaoAuthenticationProvider
使用UserDetailsService
查找UserDetails
DaoAuthenticationProvider
然后使用PasswordEncoder
验证上一步返回的UserDetails
里的密码。- 当身份验证成功时,返回的
Authtication
类型为UsernamePasswordAuthenticationToken
并且具有一个配置的UserDetailsService
返回的UserDetails
主体。最终,返回的UsernamePasswordAuthenticationToken
将由身份认证Filter
设置到SecurityContextHolder
里。
LDAP Authentication
暂不使用
Session 管理
Http session 相关功能由SessionManagementFilter
和SessionAuthenticationStrategy
组合来处理,SessionManagementFilter
把具体操作委派给SessionAuthenticationStrategy
。典型用途包括会话固定保护攻击预防、会话超时检测以及对经过身份验证的用户可能同时打开的会话数量的限制。
Detecting Timeouts
暂不使用
并发 Session 数控制
限制单个用户最多同时登录的session数. 暂不使用:请看官网文档
Session Fixation Attack Protection 固定session攻击保护
默认开启。可通过session-fixation-protection
属性关闭。 请看官网文档
SessionManagementFilter
SessionManagementFilter
根据 SecurityContextHolder
的当前内容检查 SecurityContextRepository
的内容,以确定在当前请求期间用户是否已通过身份验证,通常通过非交互式身份验证机制,例如pre-authentication
或者 remember-me
。表单登录方式不会被SessionManagementFilter
检测到。
//todo
SessionAuthenticationStrategy
//todo
Remember-Me Authentication
//todo
OpenID Support
暂不使用
Anonymous Authentication
暂不使用
Pre-Authentication Scenarios
在某些情况下,希望使用 Spring Security 进行授权,但用户在访问应用程序之前已经通过某些外部系统的可靠身份验证。这种场景就称为pre-authenticated
。
使用预认证时,Spring Security 必须:
- 识别发出请求的用户。
- 为用户获取权限
具体细节依赖于外部认证机制。在 X.509 的情况下,用户可能通过他们的证书信息来标识,或者在 Siteminder 的情况下,通过 HTTP 请求标头来标识。如果依赖容器身份验证,则将通过对传入的 HTTP 请求调用 getUserPrincipal()
方法来识别用户。在某些情况下,外部机制可能会为用户提供角色/权限信息,但在其他情况下,权限必须从单独的来源获得,例如 UserDetailsService
。
Pre-Authentication 框架里的类
因为大多数预身份验证机制都遵循相同的模式,所以 Spring Security 有一组类,这些类为实现预身份验证提供程序提供了一个内部框架。这消除了重复并允许以结构化的方式添加新的实现,而无需从头开始编写所有内容。如果想使用 X.509 身份验证之类的东西,则不需要了解这些类,因为它已经有一个名称空间配置选项,使用起来更简单,开始使用也更简单。如果您需要使用显式 bean 配置或计划编写自己的实现,那么了解spring security已提供的实现如何工作的,就很有用,可以找找org.springframework.security.web.authentication.preauth
包下的类。我们只是在此处提供了一个大纲,因此您应该在适当的情况下查阅 Javadoc 和源代码。
AbstractPreAuthenticatedProcessingFilter
此类将检查SecurityContext
的当前内容,如果为空,它将尝试从 HTTP 请求中提取用户信息并将其提交给 AuthenticationManager
。子类覆盖以下方法以获取此信息:
1 | protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request); |
调用这两个方法后,它根据返回数据,创建一个 PreAuthenticatedAuthenticationToken
并将其提交以进行身份验证。这里的“身份验证”,我们实际上只是指进一步处理以加载可能需要的用户权限,但遵循标准的 Spring Security 身份验证架构。
与其他 Spring Security Authentication Filter 一样,pre-authentication filter
也具有 authenticationDetailsSource
属性,默认情况下,这个属性将创建一个 WebAuthenticationDetails 对象,以在 Authentication
对象的 details
属性中存储附加信息,例如会话标识符和原始 IP 地址。
//todo
PreAuthenticatedAuthenticationProvider
Http403ForbiddenEntryPoint
AuthenticationEntryPoint
负责为未经身份验证的用户启动身份验证流程(当他们尝试访问受保护的资源时),但在预身份验证的情况下,这并不适用。
//todo
具体实现
//todo