表单登录

Spring Security 提供了对通过 HTML 表单提供用户名和密码的支持。本节详细介绍了基于表单的身份验证在 Spring Security 中的工作原理。

本节探讨了基于表单的登录在 Spring Security 中的工作原理。首先,我们将了解用户如何重定向到登录表单。

loginurlauthenticationentrypoint
图 1. 重定向到登录页面

上图基于我们的 SecurityFilterChain 图表。

数字 1 首先,用户对未经授权的资源(/private)发出未经身份验证的请求。

数字 2 Spring Security 的 AuthorizationFilter 指示未经身份验证的请求被拒绝,并抛出 AccessDeniedException 异常。

数字 3 由于用户未经身份验证,ExceptionTranslationFilter 启动开始身份验证并重定向到登录页面,并使用配置的 AuthenticationEntryPoint。在大多数情况下,AuthenticationEntryPointLoginUrlAuthenticationEntryPoint 的实例。

数字 4 浏览器请求它被重定向到的登录页面。

数字 5 应用程序中的某些内容必须 呈现登录页面

提交用户名和密码后,UsernamePasswordAuthenticationFilter 将对用户名和密码进行身份验证。UsernamePasswordAuthenticationFilter 扩展了 AbstractAuthenticationProcessingFilter,因此以下图表看起来应该非常相似。

usernamepasswordauthenticationfilter
图 2. 验证用户名和密码

该图基于我们的 SecurityFilterChain 图表。

数字 1 当用户提交用户名和密码时,UsernamePasswordAuthenticationFilter 会创建一个 UsernamePasswordAuthenticationToken,这是一种 Authentication,它通过从 HttpServletRequest 实例中提取用户名和密码来创建。

数字 2 接下来,UsernamePasswordAuthenticationToken 被传递给 AuthenticationManager 实例进行身份验证。AuthenticationManager 的具体实现取决于 用户信息存储方式

数字 3 如果身份验证失败,则为失败

  1. SecurityContextHolder 被清空。

  2. RememberMeServices.loginFail 被调用。如果未配置记住我功能,则此操作为空操作。请参阅 Javadoc 中的 RememberMeServices 接口。

  3. AuthenticationFailureHandler 被调用。请参阅 Javadoc 中的 AuthenticationFailureHandler 类。

数字 4 如果身份验证成功,则为成功

  1. SessionAuthenticationStrategy 收到新的登录通知。请参阅 Javadoc 中的 SessionAuthenticationStrategy 接口。

  2. SecurityContextHolder 上设置 Authentication。请参阅 Javadoc 中的 SecurityContextPersistenceFilter 类。

  3. RememberMeServices.loginSuccess 被调用。如果未配置记住我功能,则此操作为空操作。请参阅 Javadoc 中的 RememberMeServices 接口。

  4. ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent 事件。

  5. AuthenticationSuccessHandler 被调用。通常,这是一个 SimpleUrlAuthenticationSuccessHandler,它重定向到 ExceptionTranslationFilter 在我们重定向到登录页面时保存的请求。

默认情况下,Spring Security 启用表单登录。但是,一旦提供了任何基于 Servlet 的配置,就必须显式提供基于表单的登录。以下示例显示了一个最小的显式 Java 配置

表单登录
  • Java

  • XML

  • Kotlin

public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		.formLogin(withDefaults());
	// ...
}
<http>
	<!-- ... -->
	<form-login />
</http>
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
	http {
		formLogin { }
	}
	// ...
}

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

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

自定义登录表单配置
  • Java

  • XML

  • Kotlin

public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		.formLogin(form -> form
			.loginPage("/login")
			.permitAll()
		);
	// ...
}
<http>
	<!-- ... -->
	<intercept-url pattern="/login" access="permitAll" />
	<form-login login-page="/login" />
</http>
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
	http {
		formLogin {
			loginPage = "/login"
			permitAll()
		}
	}
	// ...
}

当在 Spring Security 配置中指定登录页面时,您负责呈现该页面。以下 Thymeleaf 模板生成一个符合 /login 登录页面的 HTML 登录表单。

登录表单 - src/main/resources/templates/login.html
<!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 令牌,该令牌由 Thymeleaf 自动包含

  • 表单应在名为 username 的参数中指定用户名。

  • 表单应在名为 password 的参数中指定密码。

  • 如果找到名为 error 的 HTTP 参数,则表示用户未能提供有效的用户名或密码。

  • 如果找到名为 logout 的 HTTP 参数,则表示用户已成功注销。

许多用户只需要自定义登录页面。但是,如果需要,您可以使用其他配置自定义前面显示的所有内容。

如果使用 Spring MVC,则需要一个将 GET /login 映射到我们创建的登录模板的控制器。以下示例显示了一个最小的 LoginController

LoginController
  • Java

  • Kotlin

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