一次性令牌登录

Spring Security 通过oneTimeTokenLogin()DSL 的 在深入研究实施细节之前,请务必阐明框架中 OTT 功能的范围,重点介绍支持和不支持的功能。spring-doc.cadn.net.cn

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

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

设置要求

Token Delivery

代币生成

总之,一次性令牌 (OTT) 提供了一种无需额外帐户设置即可对用户进行身份验证的方法,将其与一次性密码 (OTP) 区分开来,后者通常涉及更复杂的设置过程,并依赖外部工具生成令牌。spring-doc.cadn.net.cn

一次性令牌登录分两个主要步骤进行。spring-doc.cadn.net.cn

  1. 用户通过提交其用户标识符(通常是用户名)来请求令牌,令牌通常以 Magic Link 的形式通过电子邮件、短信等方式发送给他们。spring-doc.cadn.net.cn

  2. 用户将令牌提交到一次性令牌登录终端节点,如果有效,则用户登录。spring-doc.cadn.net.cn

在以下部分中,我们将探讨如何根据您的需求配置 OTT 登录。spring-doc.cadn.net.cn

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

oneTimeTokenLogin()DSL 可以与formLogin(),这将在默认生成的登录页面中生成一个额外的一次性令牌请求表单。 它还将设置DefaultOneTimeTokenSubmitPageGeneratingFilter以生成默认的 One-Time Token 提交页面。spring-doc.cadn.net.cn

将 Token 发送给用户

Spring Security 无法合理地确定应将令牌交付给用户的方式。 因此,自定义OneTimeTokenGenerationSuccessHandler根据您的需求将 Token 投递给用户。 最常见的交付策略之一是 Magic Link,通过电子邮件、短信等。 在以下示例中,我们将创建一个 magic link 并将其发送到用户的电子邮件。spring-doc.cadn.net.cn

一次性令牌登录配置
@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:spring-doc.cadn.net.cn

Use the following link to sign in into the application: http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5bspring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

Changing the Generate 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 {
    // ...
}
@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:spring-doc.cadn.net.cn

Configuring the Default Submit Page 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
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.spring-doc.cadn.net.cn

Disabling the Default Submit Page
@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.spring-doc.cadn.net.cn

Some of the most common reasons to customize the OneTimeTokenService are, but not limited to:spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

Passing the OneTimeTokenService as a Bean
@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.spring-doc.cadn.net.cn

Passing the OneTimeTokenService using the DSL
@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 {
     // ...
}