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-AuthenticateHeader 等.)
  • 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) - CAS
  • Remember Me - 记住我
  • JAAS Authentication - JAAS
  • OpenID - OpenID Authentication
  • Pre-Authentication Scenarios - 预认证场景。 就是用其它机制(自己实现的、或者JEE的,反正不是spring security的)实现了身份认证, 但是要实用spring security 作 Authorization和 漏洞防护的情况。
  • X509 Authentication - X509 Authentication

SecurityContextHolder

Spring security 认证模型 的核心: SecurityContextHolder, 它里包含了SecurityContext

img

SecurityContextHolder 是存储已认证用户的地方。 判断用户登录时,Spring Security 不关心SecurityContextHolder值是怎么写进去的, 只关心里面有没有这个值, 如果有就认为这个用户登录了。

表示用户已通过身份验证的最简单方法是直接设置 SecurityContextHolder。

例: 设置SecurityContextHolder

1
2
3
4
5
6
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context);
  1. 先创建一个空的SecurityContext。 因为如果用SecurityContextHolder.getContext().setAuthentication(authentication)方式的话,在多线程下容易出现竞争。
  2. 再创建一个Authentication 对象。 spring security 不关心 Authentication的具体实现类是什么。这里使用了很简单的TestingAuthenticationToken, 大多数实际情况使用UsernamePasswordAuthenticationToken(userDetails, password, authorities)
  3. 最后把这个SecurityContext 设置到SecurityContextHolder, spring security 会使用这些信息来做身份认证。

访问SecurityContextHolder 可以获取已认证主体的信息。

例:访问当前认证过的用户

1
2
3
4
5
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

默认情况下, SecurityContextHolder 是通过ThreadLocal实现的, 所以SecurityContext同一个线程里的方法都是可用的, 不需要把SecurityContext作为参数传递。

SecurityContextHolder 的实现方式是可以改变的,两种方法: 设置系统属性spring.security.strategy 和 调用SecurityContextHolder.setStrategyName(String strategyName)的静态方法。

支持的实现有:

  1. MODE_THREADLOCAL: 默认实现
  2. MODE_INHERITABLETHREADLOCAL: 子线程(有SecurityContext的线程创建的)继承当前的SecurityContext
  3. MODE_GLOBAL: 独立应用,只用一个用户那种。客户端应用这种, 每个应用实例只登录一个用户, 实用这种场景

SecurityContext

SecurityContextSecurityContextHolder 获取,包含了一个 Authentication 对象

Authentication

Authentication 在Spring Security中主要有两个作用:

  1. 作为AuthenticationManager 的入参, 提供用户录入的身份凭据信息,以便身份认证时使用, 这时候isAuthenticated()返回false
  2. 代表当前认证通过的用户。 当前Authentication 可从SecurityContext中获取。

Authentication 包含:

  1. principal: 主体。用户唯一标识,使用用户/密码认证时,通常是UserDetails实例。
  2. credentials: 凭据。通常是密码。多数时候用户认证通过后会清除掉, 避免泄漏。
  3. authorities: 授权。GrantedAuthority是用户被授予的高级别的权限。如role或者 scopes。

GrantedAuthority

GrantedAuthority是用户被授予的高级别的权限。如role或者 scopes。

可以用Authentication.getAuthorities()方法获取,此方法返回GrantedAuthority集合。GrantedAuthority就是已授予给主体的权限。通常是角色,如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。这些角色是稍后在web authorizationmethod authorizationdomain 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

ProviderManagerAuthenticationManager最常用的实现。ProviderManager把认证工作委派给一组 AuthenticationProvider。每个AuthenticationProvider 都有机会指明认证是失败、成功,或者它不能决定允许下游AuthenticationProvider来决定。如果没有AuthenticationProvider能决定,那认证会失败并附带ProviderNotFoundException这样一个特殊的AuthenticationException,表明ProviderManager不能支持传入的认证类型。

img

实际使用时, 每个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. 禁用ProviderManagereraseCredentialsAfterAuthentication属性,ProviderManager.setEraseCredentialsAfterAuthentication(boolean eraseSecretData)

AuthenticationProvider

可以将多个 AuthenticationProviders 注入到 ProviderManager 中。每个 AuthenticationProvider 执行特定类型的身份验证。例如,DaoAuthenticationProvider 支持基于用户名/密码的身份验证,而 JwtAuthenticationProvider 支持对 JWT 令牌进行身份验证。

AuthenticationEntryPoint获取用户凭证信息

AuthenticationEntryPoint用来发送一个HTTP response,从客户端获取用户凭证信息。

有时,客户端会主动包含凭据(例如用户名/密码)来请求资源。在这些情况下,Spring Security 不需要提供从客户端请求凭据的 HTTP 响应,因为它们已经包含在内。

在其他情况下,客户端将对他们无权访问的资源发出未经身份验证的请求。在这种情况下,AuthenticationEntryPoint 的实现用于从客户端请求凭据。如:执行重定向到登录页面或使用 WWW-Authenticate 标头等进行响应。

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 是认证用户凭据的父Filter

img

  1. 当用户提交凭证信息的时候, AbstractAuthenticationProcessingFilterHttpServletRequest创建一个Authentication对象以进行身份认证。Authentication对象的类型依赖于AbstractAuthenticationProcessingFilter子类。如UsernamePasswordAuthenticationFilter 根据在 HttpServletRequest 中提交的用户名和密码创建一个 UsernamePasswordAuthenticationToken
  2. Authentication对象传给AuthenticationManager进行认证。
  3. 如果认证失败:
    • 清除SecurityContextHolder
    • 调用 RememberMeServices.loginFail。如果没有配置记住我,这个方法什么都不干。
    • 调用AuthenticationFailureHandler
  4. 如果成功:
    • SessionAuthenticationStrategy 收到新登录通知
    • Authentication设置在SecurityContextHolder上,然后SecurityContextPersistenceFilter SecurityContext 保存到 HttpSession
    • 调用RememberMeServices.loginSuccess, 如果没有配置记住我功能, 则此方法撒都不干
    • ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件。

用户名/密码 认证

用户名/密码 是最常见的认方式, spring security 为此提供了全面的支持。

读取 Username & Password

Spring security 提供了以下内建机制从HttpServletRequest读取用户名/密码:

  1. 表单登录
  2. Basic Authentication
  3. Digest Authentication

Storage Mechanisms

每种读取方式, 都可以使用以下任意的存储机制:

  1. In-Memory Authentication简单的存到内存里
  2. JDBC Authentication存到关系数据库
  3. UserDetailsService实现自定义存储
  4. LDAP Authentication 存储 (不太了解)

表单登录

Spring Security 支持通过 html 表单提供用户名和密码。

看一下Spring Security里基于表单登录的流程, 首先是如何重写向到登录表单的:

6. 重定向到登录页

  1. 用户向其未授权的资源 /private 发出未经身份验证的请求
  2. Spring Security 的 FilterSecurityInterceptor 通过抛出 AccessDeniedException 指示未经身份验证的请求被拒绝。
  3. 由于用户未通过身份验证,ExceptionTranslationFilter 启动启动身份验证并使用已配置的 AuthenticationEntryPoint 将重定向发送到登录页面。在大多数情况下,AuthenticationEntryPointLoginUrlAuthenticationEntryPoint 的一个实例。
  4. 然后浏览器将重定向到登录页面。
  5. 应用程序渲染登录页面

提交用户名和密码后,UsernamePasswordAuthenticationFilter 会验证用户名和密码。UsernamePasswordAuthenticationFilter 扩展了 AbstractAuthenticationProcessingFilter,所以这个图应该看起来非常相似。

用户名密码认证

  1. 用户提交用户名和密码,UsernamePasswordAuthenticationFilter 从 HttpServletRequest 中提取用户名和密码来创建一个UsernamePasswordAuthenticationToken Authentication的一种实现)。
  2. 然后,将 UsernamePasswordAuthenticationToken 传入 AuthenticationManager 进行身份验证。 AuthenticationManager 的实现细节取决于用户信息的存储方式。
  3. 如果认证失败:
    • 清除SecurityContextHolder
    • 调用 RememberMeServices.loginFai。如果没有配置记住我,这个方法什么都不干。
    • 调用AuthenticationFailureHandler
  4. 如果成功:
    • SessionAuthenticationStrategy 收到新登录通知
    • Authentication设置在SecurityContextHolder上,然后SecurityContextPersistenceFilter SecurityContext 保存到 HttpSession
    • 调用RememberMeServices.loginSuccess, 如果没有配置记住我功能, 则此方法撒都不干
    • ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件。
    • 调用AuthenticationSuccessHandler 通常,这是一个 SimpleUrlAuthenticationSuccessHandler,它会把页面重写向到ExceptionTranslationFilter 保存的请求去,这个请求是前面重写向到登录页时保存的 ,也就是用户本来想访问的页面。

默认情况下 Spring Security 启用表单登录。但是,一旦提供了任何基于 servlet 的配置,就必须明确提供基于表单的登录。可以在下面找到一个最小的、显式的 Java 配置:

1
2
3
4
5
protected void configure(HttpSecurity http) {
http
// ...
.formLogin(withDefaults());
}

在这个配置中,Spring Security 将呈现一个默认的登录页面。大多数生产应用程序都需要自定义登录表单。

下面的配置演示了如何提供自定义登录表单。

1
2
3
4
5
6
7
8
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
}

当在 Spring Security 配置中指定登录页面时,需要应用程序自己写登录页面。下面是一个 Thymeleaf 模板,它生成一个符合 /login 登录页面的 HTML 登录表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
</head>
<body>
<h1>Please Log In</h1>
<div th:if="${param.error}">
Invalid username and password.</div>
<div th:if="${param.logout}">
You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
</body>
</html>

关于默认 HTML 表单有几个关键点:

  • 表单应该使用POST方式提交到/login
  • 表单要包含 CSRF TokenThymeleaf会自动添加
  • 用户名参数名:username, 密码参数名:password
  • 传入页面的参数如果有名为error的参数,表示用户名/密码认证失败(这种是用户提交登录,但是信息不对, 重新返回登录页面时,应用返回的错误信息)
  • 传入页面的参数如果有名为logout的参数, 表示用户退出登录成功

许多用户只需要自定义登录页面即可。但是,如果需要,以上所有内容都可以通过其他配置进行自定义。

如果您使用的是 Spring MVC,您将需要一个控制器来将 GET /login 映射到我们创建的登录模板。一个最小的示例 LoginController 可以在下面看到:

1
2
3
4
5
6
7
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}

Basic Authentication

暂不需要

Digest Authentication

暂不需要

In-Memory Authentication

暂不需要

JDBC Authentication

需要按照spring security的要求建表。暂不需要

UserDetails

UserDetailsUserDetailsService 返回。 DaoAuthenticationProvider 验证 UserDetails,然后返回具有主体的Authentication,该主体是配置的 UserDetailsService 返回的 UserDetails

UserDetailsService

DaoAuthenticationProvider 使用 UserDetailsService 来检索用户名、密码和其他属性,以使用用户名和密码进行身份验证。 Spring Security 提供了 UserDetailsService 的内存和 JDBC 实现。

可以通过将自定义 UserDetailsService 公开为 bean 来定义自定义身份验证。例如,假设 CustomUserDetailsService 实现 UserDetailsService,以下将自定义身份验证:

注意: 这仅在未填充 AuthenticationManagerBuilder 且未定义 AuthenticationProviderBean 时使用。

1
2
3
4
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

PasswordEncoder

Spring Security 的 servlet 通过与 PasswordEncoder 集成来支持安全存储密码。可以通过公开 PasswordEncoder Bean 来自定义 Spring Security 使用的 PasswordEncoder 实现。

DaoAuthenticationProvider

DaoAuthenticationProvider 是一个 AuthenticationProvider 实现,它利用 UserDetailsServicePasswordEncoder 来验证用户名和密码。

我们来看看 DaoAuthenticationProvider 在 Spring Security 中是如何工作的。该图详细说明了AuthenticationManager读取用户名密码时如何工作。

img

  1. 认证相关的Filter(不同认证类型Filter不同,用户名/密码方式是UsernamePasswordAuthenticationFilter)传递一个UsernamePasswordAuthenticationTokenAuthenticationManager(具体实现类是 ProviderManager)。
  2. ProviderManager配置了一个类型为DaoAuthenticationProviderAuthenticationProvider
  3. DaoAuthenticationProvider使用UserDetailsService查找UserDetails
  4. DaoAuthenticationProvider 然后使用 PasswordEncoder 验证上一步返回的 UserDetails 里的密码。
  5. 当身份验证成功时,返回的Authtication类型为 UsernamePasswordAuthenticationToken 并且具有一个配置的 UserDetailsService 返回的 UserDetails主体。最终,返回的 UsernamePasswordAuthenticationToken 将由身份认证 Filter 设置到 SecurityContextHolder 里。

LDAP Authentication

暂不使用

Session 管理

Http session 相关功能由SessionManagementFilterSessionAuthenticationStrategy组合来处理,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
2
3
protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);

protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

调用这两个方法后,它根据返回数据,创建一个 PreAuthenticatedAuthenticationToken 并将其提交以进行身份验证。这里的“身份验证”,我们实际上只是指进一步处理以加载可能需要的用户权限,但遵循标准的 Spring Security 身份验证架构。

与其他 Spring Security Authentication Filter 一样,pre-authentication filter也具有 authenticationDetailsSource 属性,默认情况下,这个属性将创建一个 WebAuthenticationDetails 对象,以在 Authentication 对象的 details 属性中存储附加信息,例如会话标识符和原始 IP 地址。

//todo

PreAuthenticatedAuthenticationProvider

Http403ForbiddenEntryPoint

AuthenticationEntryPoint 负责为未经身份验证的用户启动身份验证流程(当他们尝试访问受保护的资源时),但在预身份验证的情况下,这并不适用。

//todo

具体实现

//todo