一次性令牌登录
Spring Security 通过oneTimeTokenLogin()
DSL 的
在深入研究实施细节之前,请务必阐明框架中 OTT 功能的范围,重点介绍支持和不支持的功能。
了解一次性令牌与一次性密码
将一次性令牌 (OTT) 与一次性密码 (OTP) 混淆是很常见的,但在 Spring Security 中,这些概念在几个关键方面有所不同。 为清楚起见,我们假设 OTP 是指 TOTP(基于时间的一次性密码)或 HOTP(基于 HMAC 的一次性密码)。
Token Delivery
-
OTT:通常是自定义的
OneTimeTokenGenerationSuccessHandler
必须实现,负责将 Token 交付给最终用户。 -
OTP:令牌通常由外部工具生成,因此无需通过应用程序将其发送给用户。
代币生成
-
OTT: 这
OneTimeTokenService.generate(GenerateOneTimeTokenRequest)
方法需要OneTimeToken
要返回,强调服务器端生成。 -
OTP:令牌不一定在服务器端生成,通常由客户端使用共享密钥创建。
总之,一次性令牌 (OTT) 提供了一种无需额外帐户设置即可对用户进行身份验证的方法,将其与一次性密码 (OTP) 区分开来,后者通常涉及更复杂的设置过程,并依赖外部工具生成令牌。
一次性令牌登录分两个主要步骤进行。
-
用户通过提交其用户标识符(通常是用户名)来请求令牌,令牌通常以 Magic Link 的形式通过电子邮件、短信等方式发送给他们。
-
用户将令牌提交到一次性令牌登录终端节点,如果有效,则用户登录。
在以下部分中,我们将探讨如何根据您的需求配置 OTT 登录。
默认登录页面和默认一次性令牌提交页面
这oneTimeTokenLogin()
DSL 可以与formLogin()
,这将在默认生成的登录页面中生成一个额外的一次性令牌请求表单。
它还将设置DefaultOneTimeTokenSubmitPageGeneratingFilter
以生成默认的 One-Time Token 提交页面。
将 Token 发送给用户
Spring Security 无法合理地确定应将令牌交付给用户的方式。
因此,自定义OneTimeTokenGenerationSuccessHandler
根据您的需求将 Token 投递给用户。
最常见的交付策略之一是 Magic Link,通过电子邮件、短信等。
在以下示例中,我们将创建一个 magic link 并将其发送到用户的电子邮件。
-
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
Make the MagicLinkOneTimeTokenGenerationSuccessHandler
a Spring bean
2
Create a login processing URL with the token
as a query param
3
Retrieve the user’s email based on the username
4
Use the JavaMailSender
API to send the email to the user with the magic link
5
Use the RedirectOneTimeTokenGenerationSuccessHandler
to perform a redirect to your desired URL
The email content will look similar to:
Use the following link to sign in into the application: http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
The default submit page will detect that the URL has the token
query param and will automatically fill the form field with the token value.
Changing the One-Time Token Generate URL
By default, the GenerateOneTimeTokenFilter
listens to POST /ott/generate
requests.
That URL can be changed by using the generateTokenUrl(String)
DSL method:
Changing the Generate 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 {
// ...
}
Changing the Default Submit Page URL
The default One-Time Token submit page is generated by the DefaultOneTimeTokenSubmitPageGeneratingFilter
and listens to GET /login/ott
.
The URL can also be changed, like so:
Configuring the Default Submit Page 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 {
// ...
}
Disabling the Default Submit Page
If you want to use your own One-Time Token submit page, you can disable the default page and then provide your own endpoint.
Disabling the Default Submit Page
-
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 {
// ...
}
Customize How to Generate and Consume One-Time Tokens
The interface that define the common operations for generating and consuming one-time tokens is the OneTimeTokenService
.
Spring Security uses the InMemoryOneTimeTokenService
as the default implementation of that interface, if none is provided.
For production environments consider using JdbcOneTimeTokenService
.
Some of the most common reasons to customize the OneTimeTokenService
are, but not limited to:
-
Changing the one-time token expire time
-
Storing more information from the generate token request
-
Changing how the token value is created
-
Additional validation when consuming a one-time token
There are two options to customize the OneTimeTokenService
.
One option is to provide it as a bean, so it can be automatically be picked-up by the oneTimeTokenLogin()
DSL:
Passing the OneTimeTokenService as a Bean
-
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 {
// ...
}
The second option is to pass the OneTimeTokenService
instance to the DSL, which is useful if there are multiple SecurityFilterChain
and a different OneTimeTokenService
is needed for each of them.
Passing the OneTimeTokenService using the DSL
-
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 {
// ...
}