表单登录

让我们来看看基于表单的登录在 Spring Security 中是如何工作的。首先,我们看到如何将用户重定向到登录表单。

QNetworkAccessManager 用户名密码验证 用户名密码认证_表单


该图构建了我们的 SecurityFilterChain 图。

  1. 首先,用户向未授权的资源/私有发出未经身份验证的请求。
  2. Spring Security 的 FilterSecurityInterceptor 通过抛出 AccessDeniedException 表示拒绝未经身份验证的请求。
  3. 由于用户没有经过身份验证,ExceptionTranslationFilter 启动 Start Authentication,并使用配置的 AuthenticationEntryPoint 重定向到登录页面。在大多数情况下,AuthenticationEntryPoint 是 LoginUrlAuthenticationEntryPoint 的实例。
  4. 浏览器会请求重定向到的登录页面。
  5. 应用程序中的某些内容必须呈现登录页面。
    提交用户名和密码后,UsernamePasswordAuthenticationFilter 对用户名和密码进行身份验证。UsernamePasswordAuthenticationFilter 扩展了 AbstractAuthenticationProcessingFilter,因此这个图看起来应该非常相似。

    该图构建了我们的 SecurityFilterChain 图:
  6. 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter 通过从 HttpServletRequest 中提取用户名和密码创建一个 UsernamePasswordAuthenticationToken,这是一种身份验证类型。
  7. 接下来,UsernamePasswordAuthenticationToken 被传递到 AuthenticationManager 以进行身份验证。AuthenticationManager 的详细内容取决于用户信息的存储方式。
  8. 如果身份验证失败,则失败:
  • SecurityContextHolder被清空
  • 调用 RememberMeServices.loginFail。如果没有配置 remember me,这是一个 no-op。
  • 调用 AuthenticationFailureHandler
  1. 如果身份验证成功,则为成功:
  • 会在新登录时通知 SessionAuthenticationStrategy。
  • 在 SecurityContextHolder 上设置 身份验证。
  • 调用 RememberMeServices.loginSuccess。如果没有配置 remember me,这是一个 no-op。
  • 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()
		);
}

当在 springsecurity 配置中指定登录页面时,您负责呈现该页面。下面是一个 Thymeleaf 模板,它生成一个 HTML 登录表单,该表单遵循/login 的登录页面:

<!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 表单有几个关键点:

  • 该表单应执行一个 /login 的 Post 提交
  • 表单应该在一个名为 username 的参数中指定用户名
  • 表单应该在名为 password 的参数中指定密码
  • 如果发现 param.error,则表明用户未能提供有效的用户名/密码
  • 如果找到了 param.logout ,则表明用户已成功注销

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

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

@Controller
class LoginController {
	@GetMapping("/login")
	String login() {
		return "login";
	}
}

Basic Authentication

让我们来看看在 Spring Security 中 HTTP 基本身份验证是如何工作的。首先,我们看到 WWW-Authenticate 标头被发送回未经身份验证的客户端。

QNetworkAccessManager 用户名密码验证 用户名密码认证_表单_02


该图构建了我们的 SecurityFilterChain 图。

  1. 首先,用户向未授权的资源 /private 发出未经身份验证的请求。
  2. Spring Security 的 FilterSecurityInterceptor 通过抛出 AccessDeniedException 表示拒绝未经身份验证的请求。
  3. 由于用户没有经过身份验证,ExceptionTranslationFilter 启动“启动身份验证”。已配置的 AuthenticationEntryPoint 是 BasicAuthenticationEntryPoint 的一个实例,它发送一个 WWW-Authenticate 标头。RequestCache 通常是一个 NullRequestCache,它不保存请求,因为客户机能够重放它最初请求的请求。

当客户端收到 WWW-Authenticate 标头时,它知道应该使用用户名和密码重试。下面是处理用户名和密码的流程。

QNetworkAccessManager 用户名密码验证 用户名密码认证_Authentication_03


该图构建了我们的 SecurityFilterChain 图:

  1. 当用户提交他们的用户名和密码时,BasicAuthenticationFilter 通过从 HttpServletRequest 中提取用户名和密码来创建一个 UsernamePasswordAuthenticationToken,这是一种身份验证类型。
  2. 接下来,UsernamePasswordAuthenticationToken 被传递到 AuthenticationManager 以进行身份验证。AuthenticationManager 的详细内容取决于用户信息的存储方式。
  3. 如果身份验证失败,则失败:
  • SecurityContextHolder 被清空
  • 调用 RememberMeServices.loginFail。如果没有配置 rememberme,这是一个 no-op。
  • AuthenticationEntryPoint 被调用来触发再次发送的 WWW-Authenticate。
  1. 如果身份验证成功,则为成功:
  • 在 SecurityContextHolder 上设置身份验证
  • 调用 RememberMeServices.loginSuccess。如果没有配置 remember me,这是一个 no-op。
  • BasicAuthenticationFilter 调用 FilterChain.doFilter (请求,响应)以继续应用程序逻辑的其余部分。

Spring Security 的 HTTP 基本身份验证支持在默认情况下是启用的。但是,一旦提供了任何基于 servlet 的配置,就必须显式地提供 HTTP Basic。
一个最小的,显式的配置可以在下面找到:

protected void configure(HttpSecurity http) {
	http
		// ...
		.httpBasic(withDefaults());
}

Digest Authentication

警告: 您不应该在现代应用程序中使用摘要式身份验证,因为它被认为是不安全的。最明显的问题是必须以明文、加密或 md5格式存储密码。所有这些存储格式都是不安全的。相反,您应该使用单向自适应密码散列(即 bCrypt、 PBKDF2、 SCrypt 等)来存储凭据,而这是摘要身份验证不支持的。

摘要身份验证试图解决基本身份验证的许多弱点,特别是通过确保永远不会跨网络以明文形式发送凭据。许多浏览器支持摘要式身份验证。

管理 HTTP 摘要认证的标准是由 RFC 2617定义的,它更新了 RFC 2069规定的摘要认证标准的早期版本。大多数用户代理实现 RFC 2617。Spring Security 的 Digest Authentication 支持与 RFC 2617规定的“ auth”质量保护(qop)兼容,RFC 2617还为向下兼容提供了 RFC 2069。如果您需要使用未加密的 HTTP (即不使用 TLS/HTTPS)并希望最大限度地提高认证过程的安全性,那么摘要认证被认为是一个更有吸引力的选择。然而,每个人都应该使用 HTTPS。

摘要式认证的核心是“ nonce”。这是服务器生成的值。的 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());
}

参考 SpringSecurity 官方文档