一次性令牌登录
Spring Security 通过 oneTimeTokenLogin()
DSL 提供了一次性令牌 (OTT) 认证支持。在深入了解实现细节之前,重要的是要阐明框架内 OTT 功能的范围,强调支持哪些功能以及不支持哪些功能。
理解一次性令牌与一次性密码
一次性令牌 (OTT) 和 一次性密码 (OTP) 常常被混淆,但在 Spring Security 中,这两个概念在几个关键方面存在差异。为了清晰起见,我们将假设 OTP 指的是 TOTP (基于时间的一次性密码) 或 HOTP (基于 HMAC 的一次性密码)。
令牌分发
-
通常必须实现自定义的
OneTimeTokenGenerationSuccessHandler
,负责将令牌分发给最终用户。 -
OTP:令牌通常由外部工具生成,因此无需通过应用程序将其发送给用户。
令牌生成
-
OTT:
OneTimeTokenService.generate(GenerateOneTimeTokenRequest)
方法要求返回一个OneTimeToken
,强调服务器端生成。 -
OTP:令牌不一定在服务器端生成,通常由客户端使用共享密钥创建。
总而言之,一次性令牌 (OTT) 提供了一种无需额外账户设置即可认证用户的方式,这与一次性密码 (OTP) 不同,后者通常涉及更复杂的设置过程并依赖外部工具生成令牌。
一次性令牌登录主要分为两个步骤。
-
用户通过提交其用户标识符(通常是用户名)请求令牌,然后令牌会被分发给他们,通常作为“魔法链接”,通过电子邮件、短信等方式发送。
-
用户将令牌提交到一次性令牌登录端点,如果有效,用户即可登录。
在以下部分中,我们将探讨如何根据您的需求配置 OTT 登录。
默认登录页面和默认一次性令牌提交页面
oneTimeTokenLogin()
DSL 可以与 formLogin()
结合使用,这会在 默认生成的登录页面 中生成一个额外的一次性令牌请求表单。它还会设置 DefaultOneTimeTokenSubmitPageGeneratingFilter
来生成一个默认的一次性令牌提交页面。
将令牌发送给用户
Spring Security 无法合理地确定应如何将令牌发送给您的用户。因此,必须提供一个自定义的 OneTimeTokenGenerationSuccessHandler
,以便根据您的需求将令牌分发给用户。最常见的分发策略之一是“魔法链接”,通过电子邮件、短信等方式发送。在下面的示例中,我们将创建一个魔法链接并发送到用户的电子邮件中。
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@Component (1)
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
// constructor omitted
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()); (2)
String magicLink = builder.toUriString();
String email = getUserEmail(oneTimeToken.getUsername()); (3)
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); (4)
this.redirectHandler.handle(request, response, oneTimeToken); (5)
}
private String getUserEmail() {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http{
formLogin {}
oneTimeTokenLogin { }
}
return http.build()
}
}
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
@Component (1)
class MagicLinkOneTimeTokenGenerationSuccessHandler(
private val mailSender: MailSender,
private val redirectHandler: OneTimeTokenGenerationSuccessHandler = RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent")
) : OneTimeTokenGenerationSuccessHandler {
override fun handle(request: HttpServletRequest, response: HttpServletResponse, oneTimeToken: OneTimeToken) {
val builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.contextPath)
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()) (2)
val magicLink = builder.toUriString()
val email = getUserEmail(oneTimeToken.getUsername()) (3)
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: $magicLink")(4)
this.redirectHandler.handle(request, response, oneTimeToken) (5)
}
private fun getUserEmail(): String {
// ...
}
}
@Controller
class PageController {
@GetMapping("/ott/sent")
fun ottSent(): String {
return "my-template"
}
}
1 | 将 MagicLinkOneTimeTokenGenerationSuccessHandler 声明为一个 Spring bean |
2 | 创建一个登录处理 URL,其中 token 作为查询参数 |
3 | 根据用户名检索用户电子邮件 |
4 | 使用 JavaMailSender API 将包含魔法链接的电子邮件发送给用户 |
5 | 使用 RedirectOneTimeTokenGenerationSuccessHandler 重定向到您想要的 URL |
电子邮件内容将类似于
使用以下链接登录应用程序:http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
默认提交页面将检测到 URL 包含 token
查询参数,并自动用令牌值填充表单字段。
更改一次性令牌生成 URL
默认情况下,GenerateOneTimeTokenFilter
监听 POST /ott/generate
请求。可以使用 generateTokenUrl(String)
DSL 方法更改该 URL
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.generateTokenUrl("/ott/my-generate-url")
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
generateTokenUrl = "/ott/my-generate-url"
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
更改默认提交页面 URL
默认的一次性令牌提交页面由 DefaultOneTimeTokenSubmitPageGeneratingFilter
生成,并监听 GET /login/ott
。也可以像这样更改 URL:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.submitPageUrl("/ott/submit")
);
return http.build();
}
}
@Component
public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
submitPageUrl = "/ott/submit"
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
禁用默认提交页面
如果您想使用自己的一次性令牌提交页面,可以禁用默认页面,然后提供您自己的端点。
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my-ott-submit").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.showDefaultSubmitPage(false)
);
return http.build();
}
}
@Controller
public class MyController {
@GetMapping("/my-ott-submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
@Component
public class OneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize("/my-ott-submit", authenticated)
authorize(anyRequest, authenticated)
}
formLogin { }
oneTimeTokenLogin {
showDefaultSubmitPage = false
}
}
return http.build()
}
}
@Controller
class MyController {
@GetMapping("/my-ott-submit")
fun ottSubmitPage(): String {
return "my-ott-submit"
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
自定义如何生成和使用一次性令牌
定义生成和使用一次性令牌的常用操作的接口是 OneTimeTokenService
。如果未提供其他实现,Spring Security 将使用 InMemoryOneTimeTokenService
作为该接口的默认实现。对于生产环境,请考虑使用 JdbcOneTimeTokenService
。
自定义 OneTimeTokenService
的一些最常见原因包括(但不限于):
-
更改一次性令牌过期时间
-
存储来自生成令牌请求的更多信息
-
更改令牌值的创建方式
-
使用一次性令牌时的额外验证
有两种选项可以自定义 OneTimeTokenService
。一种选项是将其作为 bean 提供,这样 oneTimeTokenLogin()
DSL 就可以自动获取它
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public OneTimeTokenService oneTimeTokenService() {
return new MyCustomOneTimeTokenService();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin { }
}
return http.build()
}
@Bean
open fun oneTimeTokenService(): OneTimeTokenService {
return MyCustomOneTimeTokenService()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}
第二种选项是将 OneTimeTokenService
实例传递给 DSL,这在存在多个 SecurityFilterChain
并且每个都需要不同的 OneTimeTokenService
时非常有用。
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.oneTimeTokenService(new MyCustomOneTimeTokenService())
);
return http.build();
}
}
@Component
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
//...
formLogin { }
oneTimeTokenLogin {
oneTimeTokenService = MyCustomOneTimeTokenService()
}
}
return http.build()
}
}
@Component
class MagicLinkOneTimeTokenGenerationSuccessHandler : OneTimeTokenGenerationSuccessHandler {
// ...
}