跳到主要内容

一次性令牌登录

QWen Max 中英对照 One-Time Token One-Time Token Login

Spring Security 通过 oneTimeTokenLogin() DSL 提供了一次性令牌(OTT)认证的支持。在深入实现细节之前,重要的是要明确框架内 OTT 功能的范围,突出支持和不支持的内容。

了解一次性令牌与一次性密码

一次性令牌(OTT)经常与一次性密码(OTP)混淆,但在 Spring Security 中,这些概念在几个关键方面有所不同。为了清晰起见,我们假设 OTP 指的是 TOTP(基于时间的一次性密码)或 HOTP(基于 HMAC 的一次性密码)。

设置要求

  • OTT:无需初始设置。用户不需要提前配置任何内容。

  • OTP:通常需要设置,例如生成密钥并与外部工具共享以生成一次性密码。

令牌传递

  • OTT: 通常需要实现一个自定义的 OneTimeTokenGenerationSuccessHandler,负责将令牌传递给最终用户。

  • OTP: 令牌通常由外部工具生成,因此无需通过应用程序将其发送给用户。

令牌生成

简而言之,一次性令牌(OTT)提供了一种无需额外账户设置即可验证用户身份的方法,这与通常涉及更复杂设置过程并依赖外部工具生成令牌的一次性密码(OTP)有所不同。

一次性令牌登录主要分为两个步骤。

  1. 用户通过提交其用户标识符(通常是用户名)来请求令牌,然后该令牌会通过电子邮件、短信等方式作为魔术链接发送给他们。

  2. 用户将令牌提交到一次性令牌登录端点,如果有效,用户就会登录。

在接下来的章节中,我们将探讨如何根据您的需求配置 OTT 登录。

默认登录页面和默认一次性令牌提交页面

oneTimeTokenLogin() DSL 可以与 formLogin() 一起使用,这将在默认生成的登录页面中生成一个额外的一次性令牌请求表单。它还将设置 DefaultOneTimeTokenSubmitPageGeneratingFilter 以生成默认的一次性令牌提交页面。

将令牌发送给用户

Spring Security 无法合理地确定令牌应该如何传递给用户。因此,必须提供一个自定义的 OneTimeTokenGenerationSuccessHandler,以便根据您的需求将令牌传递给用户。最常见的传递策略之一是通过电子邮件、短信等方式发送魔术链接。在下面的示例中,我们将创建一个魔术链接并通过用户的电子邮件发送。

@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";
}

}
java
  • MagicLinkOneTimeTokenGenerationSuccessHandler 作为一个 Spring bean

  • 创建一个带有 token 作为查询参数的登录处理 URL

  • 根据用户名检索用户的电子邮件

  • 使用 JavaMailSender API 将带有 magic link 的电子邮件发送给用户

  • 使用 RedirectOneTimeTokenGenerationSuccessHandler 重定向到您所需的 URL

电子邮件内容将类似于:

默认的提交页面会检测 URL 是否包含 token 查询参数,并会自动将表单字段填充为该 token 值。

更改一次性令牌生成 URL

默认情况下,GenerateOneTimeTokenFilter 监听 POST /ott/generate 请求。可以通过使用 generateTokenUrl(String) DSL 方法来更改该 URL:

@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 {
// ...
}
java

更改默认提交页面 URL

默认的一次性令牌提交页面由 DefaultOneTimeTokenSubmitPageGeneratingFilter 生成,并监听 GET /login/ott。也可以更改该 URL,如下所示:

@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 {
// ...
}
java

禁用默认提交页面

如果你想要使用自己的一次性令牌提交页面,你可以禁用默认页面,然后提供你自己的端点。

@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 {
// ...
}
java

自定义如何生成和使用一次性令牌

定义生成和使用一次性令牌的通用操作的接口是 OneTimeTokenService。如果未提供该接口的实现,Spring Security 使用 InMemoryOneTimeTokenService 作为该接口的默认实现。对于生产环境,请考虑使用 JdbcOneTimeTokenService

自定义 OneTimeTokenService 的一些最常见原因包括但不限于:

  • 更改一次性令牌的过期时间

  • 存储更多来自生成令牌请求的信息

  • 更改令牌值的创建方式

  • 在使用一次性令牌时进行额外验证

有两种方法来自定义 OneTimeTokenService。一种方法是将其作为 bean 提供,这样它就可以被 oneTimeTokenLogin() DSL 自动识别:

@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 {
// ...
}
java

第二种选项是将 OneTimeTokenService 实例传递给 DSL,如果存在多个 SecurityFilterChain 并且每个 SecurityFilterChain 需要不同的 OneTimeTokenService,这将非常有用。

@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 {
// ...
}
java