跳到主要内容
版本:7.0.2

一次性令牌登录

DeepSeek V3 中英对照 One-Time Token One-Time Token Login

Spring Security 通过 oneTimeTokenLogin() DSL 提供对一次性令牌(OTT)认证的支持。在深入实现细节之前,有必要先阐明该框架内 OTT 功能的范围,明确其支持与不支持的功能。

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

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

安装要求

  • OTT:无需初始设置。用户无需预先进行任何配置。

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

令牌交付

  • OTT:通常需要自定义一个 OneTimeTokenGenerationSuccessHandler,负责将令牌交付给最终用户。

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

令牌生成

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

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

  1. 用户通过提交其用户标识符(通常是用户名)来请求令牌,随后令牌会通过电子邮件、短信等方式(通常以魔法链接的形式)发送给用户。

  2. 用户将令牌提交到一次性令牌登录端点,如果令牌有效,用户即可成功登录。

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

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

当使用 oneTimeTokenLogin() DSL 时,默认情况下,一次性令牌登录页面由 org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 自动生成。该 DSL 还会配置 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";
}

}
  • MagicLinkOneTimeTokenGenerationSuccessHandler 设为 Spring Bean

  • 创建以 token 作为查询参数的登录处理 URL

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

  • 使用 JavaMailSender API 向用户发送包含魔法链接的电子邮件

  • 使用 RedirectOneTimeTokenGenerationSuccessHandler 重定向到目标 URL

邮件内容将类似于:

默认提交页面会检测到 URL 中包含 token 查询参数,并自动将令牌值填入表单字段。

修改一次性令牌生成URL

默认情况下,GenerateOneTimeTokenFilter 监听 POST /ott/generate 请求。该 URL 可通过 generateTokenUrl(String) DSL 方法进行修改:

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

更改默认提交页面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 {
// ...
}

禁用默认提交页面

如果您希望使用自定义的一次性令牌提交页面,可以禁用默认页面,然后提供您自己的端点。

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

自定义如何生成和消费一次性令牌

定义一次性令牌生成与消费通用操作的接口是 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 {
// ...
}

第二种选择是将 OneTimeTokenService 实例传递给 DSL,这在存在多个 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 {
// ...
}

自定义生成一次性令牌请求实例

您可能需要调整 GenerateOneTimeTokenRequest 的原因有多种。例如,您可能希望将 expiresIn 设置为 10 分钟,而 Spring Security 默认将其设置为 5 分钟。

你可以通过发布一个 GenerateOneTimeTokenRequestResolver 作为 @Bean 来定制 GenerateOneTimeTokenRequest 的元素,如下所示:

@Bean
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
return (request) -> {
GenerateOneTimeTokenRequest generate = delegate.resolve(request);
return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
};
}