一次性令牌登入
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 |
電子郵件內容將類似於
使用以下連結登入應用程式:https://: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 {
// ...
}