一次性令牌登入

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 時,一次性令牌登入頁面預設由 org.springframework.security.web.authentication.ui:DefaultLoginPageGeneratingFilter[] 自動生成。該 DSL 還會設定 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 建立帶有 token 作為查詢引數的登入處理 URL
3 根據使用者名稱檢索使用者的電子郵件
4 使用 JavaMailSender API 將包含魔術連結的電子郵件傳送給使用者
5 使用 RedirectOneTimeTokenGenerationSuccessHandler 執行重定向到您所需的 URL

電子郵件內容將類似於

使用以下連結登入應用程式:https://:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b

預設提交頁面將檢測到 URL 包含 token 查詢引數,並會自動用令牌值填充表單欄位。

更改一次性令牌生成 URL

預設情況下,GenerateOneTimeTokenFilter 監聽 POST /ott/generate 請求。該 URL 可以透過使用 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 也可以像這樣更改

配置預設提交頁面 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 可以自動拾取它

將 OneTimeTokenService 作為 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 {
     // ...
}

第二種選擇是將 OneTimeTokenService 例項傳遞給 DSL,這在存在多個 SecurityFilterChain 並且每個都需要不同的 OneTimeTokenService 時很有用。

使用 DSL 傳遞 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 {
     // ...
}

自定義 GenerateOneTimeTokenRequest 例項

您可能希望調整 GenerateOneTimeTokenRequest 有很多原因。例如,您可能希望將 expiresIn 設定為 10 分鐘,而 Spring Security 預設將其設定為 5 分鐘。

您可以透過將 GenerateOneTimeTokenRequestResolver 釋出為 @Bean 來定製 GenerateOneTimeTokenRequest 的元素,如下所示

  • Java

  • Kotlin

@Bean
GenerateOneTimeTokenRequestResolver generateOneTimeTokenRequestResolver() {
    DefaultGenerateOneTimeTokenRequestResolver delegate = new DefaultGenerateOneTimeTokenRequestResolver();
        return (request) -> {
		    GenerateOneTimeTokenRequest generate = delegate.resolve(request);
		    return new GenerateOneTimeTokenRequest(generate.getUsername(), Duration.ofSeconds(600));
	};
}
@Bean
fun generateRequestResolver() : GenerateOneTimeTokenRequestResolver {
    return DefaultGenerateOneTimeTokenRequestResolver().apply {
        this.setExpiresIn(Duration.ofMinutes(10))
    }
}
© . This site is unofficial and not affiliated with VMware.