知識庫 / Spring / Spring Security RSS 訂閱

一次性令牌登錄指南(Spring Security)

Spring Security
HongKong
11
10:51 AM · Dec 06 ,2025

1. 概述

為網站提供流暢的登錄體驗需要精妙的平衡。一方面,我們希望具有不同程度計算機知識的用户能夠儘快完成登錄;另一方面,我們需要確保訪問我們系統的人員身份,以防發生潛在的災難性安全事件。

在本教程中,我們將演示如何在基於 Spring Boot 的應用程序中使用一次性令牌登錄。 這種機制在易用性和安全性之間取得了良好的平衡,並且自 Spring Boot 3.4 版本起,在僅使用 Spring Security 6.4 或更高版本 時,即可內置支持。

2. 一次性令牌登錄簡介

傳統的計算機應用程序中,用户身份驗證是通過提供一個包含用户名和密碼的表單來實現的。如果用户忘記了自己的密碼,那會怎樣呢?通常的做法是提供“忘記密碼”按鈕。

當用户點擊此按鈕時,後端會向用户發送一條消息,其中包含一個具有時間限制的令牌,允許用户重置自己的密碼。

然而,對於許多應用程序,用户不經常訪問網站,或者不希望記住密碼。 在這些情況下,用户傾向於不斷使用重置密碼功能,這會造成沮喪,並在某些情況下導致憤怒的客户支持電話。以下是一些屬於此類應用程序的示例:

  • 社區網站(俱樂部、學校、教堂、遊戲)
  • 文檔分發/簽署服務
  • 彈出式營銷網站

以下是一次性令牌登錄(簡稱 OTT)的工作原理:

  1. 用户提供他的用户名,通常與他的電子郵件地址對應
  2. 系統生成一個具有時間限制的令牌,並通過非標準方式(例如電子郵件、短信消息、移動通知等)發送該令牌
  3. 用户在電子郵件/消息應用程序中打開消息並點擊提供的鏈接,該鏈接包含一次性令牌
  4. 用户的設備瀏覽器打開該鏈接,將用户引導回系統的 OTT 登錄位置
  5. 系統檢查嵌入在鏈接中的令牌值。如果該令牌有效,則允許訪問,並且用户可以繼續操作。 另一種方法是顯示一個令牌提交表單,提交該表單將完成登錄過程

3. 何時應使用 OTT?

在為給定應用程序考慮一次登錄機制之前,最好先評估其優缺點:

優點 缺點
無需管理用户密碼,從而也消除了安全風險 基於單因素認證,至少從應用程序端點
易於使用和理解,即使是非技術人員也能輕鬆上手 容易受到中間人攻擊

我們可能會思考:為什麼不使用社交登錄? 從技術角度來看,基於 OAuth2/OIDC 的社交登錄比 OTT 更安全。

但是,啓用它需要更多的運維工作(例如,為每個提供商請求和維護客户端 ID)並且可能由於對個人數據共享的日益關注而導致參與度降低。

4. 使用 Spring Boot 和 Spring Security 實現 OTT 功能

讓我們創建一個簡單的 Spring Boot 應用,該應用利用了自 3.4 版本開始提供的 OTT 支持。 正如往常一樣,首先我們需要添加所需的 Maven 依賴項:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.4.1<version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.4.1<version>
</dependency>

最新版本的這些依賴項可在 Maven Central 上獲取:

5. OTT 配置

在當前版本中,為應用程序啓用 OTT 需要我們提供一個 SecurityFilterChain Bean:

@Bean
SecurityFilterChain ottSecurityFilterChain(HttpSecurity http) throws Exception {
    return http
      .authorizeHttpRequests(ht -> ht.anyRequest().authenticated())
      .formLogin(withDefaults())
      .oneTimeTokenLogin(withDefaults())
      .build();
}

在此,關鍵在於使用新引入的 oneTimeTokenLogin() 方法,該方法是在 6.4 版本中作為 DSL 配置項推出的。

如往常一樣,此方法允許我們自定義機制的所有方面。在本例中,我們僅使用 Customizer.withDefaults() 來接受默認值。

此外,請注意我們已將 formLogin() 添加到配置中。 如果沒有它,Spring Security 將默認使用 Basic 身份驗證,這與 OTT 不兼容。

最後,在 authorizeHttpRequests() 部分,我們僅添加了要求所有請求進行身份驗證的配置。

6. 發送令牌

OTT 機制沒有內置的方法來實現將令牌實際交付給用户的功能。文檔中所述,這是有意的設計決策,因為實現此功能的方式實在太多了。

相反,令牌交付的責任由應用程序代碼承擔,該代碼必須暴露一個實現 OneTimeTokenGenerationSuccessHandler 接口的 Bean

該接口只有一個方法,handle(),它接收當前 Servlet 請求、響應以及最重要的,一個 OneTimeToken 對象。

該對象具有以下屬性:

  • tokenValue:生成的令牌,需要發送給用户的令牌
  • username:告知的用户名
  • expiresAt:生成的令牌到期的時間(Instant)

典型的實現流程如下:

  1. 使用提供的用户名作為鍵來查找所需的交付詳情。例如,這些詳情可能包括電子郵件地址或電話號碼以及用户的區域設置
  2. 構建一個將用户引導到 OTT 登錄頁面的 URL
  3. 準備併發送包含 OTT 鏈接的消息給用户
  4. 向客户端發送重定向響應,將瀏覽器的操作引導到 OTT 登錄頁

在我們的實現中,我們選擇將步驟 1 到 3 的職責委託給一個專門的 OttSenderService

對於步驟 4,我們將重定向的細節委託給 Spring Security 的 RedirectOneTimeTokenGenerationSuccessHandler

public class OttLoginLinkSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
    private final OttSenderService senderService;
    private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/login/ott");

    // ... constructor omitted

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
      OneTimeToken oneTimeToken) throws IOException, ServletException {
        senderService.sendTokenToUser(oneTimeToken.getUsername(),
          oneTimeToken.getTokenValue(), oneTimeToken.getExpiresAt());
        redirectHandler.handle(request, response, oneTimeToken);
    }
}

請注意,“/login/ott” 構造函數參數已傳遞給 RedirectOneTimeTokenGenerationSuccessHandler 。這對應於令牌提交表單的默認位置,並且可以使用 OTT DSL 配置為不同的位置。

至於 OttSenderService ,我們將使用一個模擬發送器實現,該實現將令牌存儲在以用户名索引的 Map 中,並記錄其值:

public class FakeOttSenderService implements OttSenderService {
    private final Map<String,String> lastTokenByUser = new HashMap<>();

    @Override
    public void sendTokenToUser(String username, String token, Instant expiresAt) {
        lastTokenByUser.put(username, token);
        log.info("Sending token to username '{}'. token={}, expiresAt={}", username,token,expiresAt);
    }

    @Override
    public Optional<String> getLastTokenForUser(String username) {
        return Optional.ofNullable(lastTokenByUser.get(username));
    }
}

請注意,OttSenderService 具有一個可選方法,允許我們為用户名恢復令牌。 此方法的的主要目的是簡化單元測試的實現,正如我們在自動化測試部分所看到的。

7. 手動測試

讓我們使用簡單的導航測試來檢查我們的應用程序與 OTT 機制的行為。 通過 IDE 或使用 mvn spring-boot:run 啓動後,請使用您選擇的瀏覽器,並導航到 http://localhost:8080。應用程序將返回一個包含標準用户名/密碼錶單和 OTT 表單的登錄頁面:

由於我們沒有提供任何 UserDetailsService,Spring Boot 的自動配置將創建一個默認的配置,其中包含一個名為“user”的單個用户。當我們在 OTT 表單的用户名字段中輸入該用户名並點擊“發送令牌”按鈕時,我們應該到達令牌提交表單:

現在,如果我們查看應用程序日誌,我們將會看到類似的消息:

c.b.s.ott.service.FakeOttSenderService   : Sending token to username 'user'. token=a0e3af73-0366-4e26-b68e-0fdeb23b9bb2, expiresAt=...

要完成登錄過程,只需將令牌值複製並粘貼到表單中,然後點擊 登錄 按鈕。 這樣,您將看到一個歡迎頁面,其中顯示當前用户名:

8. 自動化測試

測試 OTT 登錄流程需要導航一系列頁面,因此我們將使用 Jsoup 庫來輔助我們。

完整的代碼會按照我們在手動測試中走過的步驟進行,並在過程中添加檢查。

唯一棘手的部分是獲取生成的令牌。 這時,OttSenderService 接口中提供的查找方法就派上用場了。 鑑於我們利用了 Spring Boot 的測試基礎設施,我們可以簡單地將服務注入到我們的測試類中,並使用它來查詢令牌:

@Test
void whenLoginWithOtt_thenSuccess() throws Exception {
    // ... Jsoup setup and initial navigation omitted

    var optToken = this.ottSenderService.getLastTokenForUser("user");
    assertTrue(optToken.isPresent());

    var homePage = conn.newRequest(baseUrl + tokenSubmitAction)
      .data("token", optToken.get())
      .data("_csrf",csrfToken)
      .post();

    var username = requireNonNull(homePage.selectFirst("span#current-username")).text();
    assertEquals("user",username);
}

9. 結論

在本教程中,我們詳細介紹了單次令牌登錄機制,並説明了如何在基於 Spring Boot 的應用程序中添加該機制。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.