跳至主要内容

啟用免密碼登入

本指南說明如何在專案中啟用免密碼登入(Magic Link):使用者輸入 email、收到一封含登入連結的信,點擊即登入、無需密碼。

底層的簽章與驗章由框架原語 SignedLinkService 負責,運作原理與 API 細節見框架文檔 簽章連結(Signed Link);本頁聚焦在專案中怎麼接、怎麼設定、怎麼測

運作流程

使用者輸入 email
→ POST /api/v1/auth/passwordless 後端簽 SINGLE_USE 連結、寄信(回應一律 200)
→ 使用者點信中的連結
→ GET /api/v1/auth/link/consume 後端驗章、換發 session、簽一次性 exchange code
→ 303 導回前端 {redirect-base}/?code=...
→ 前端落地,POST /api/v1/auth/exchange 以 code 換回正式 token,清掉 URL 的 code
→ 已登入

關鍵設計:session 放進導向 URL,只短暫出現秒級單次的 code,前端落地後立即換成正式 token。詳見框架文檔的「exchange code 模式」。

已內建的元件

本專案(參考實作)已接好整條管線,啟用免密碼登入不需要寫新程式,只需設定與前端入口:

元件位置
後端 BeanSignedLinkStoreCacheSignedLinkStore)+ SignedLinkService(與 JWT 共用金鑰)config/SecurityConfig.java
後端服務SignedLinkAppService — 簽連結、組信、consume、exchangeservice/auth/SignedLinkAppService.java
後端端點SignedLinkControllerpasswordless / link/consume / exchangecontroller/auth/SignedLinkController.java
前端落地App.tsx 啟動時檢測 ?code= → 換 token → 清 code前端 SPA 入口

步驟

1. 確認後端 Bean 已接線

SecurityConfig與 JWT 共用的 RSA 金鑰建立 SignedLinkService,store 用框架 CacheSignedLinkStore

@Bean
public SignedLinkStore signedLinkStore(Cache<String, Boolean> signedLinkCache) {
return new CacheSignedLinkStore(signedLinkCache);
}

@Bean
public SignedLinkService signedLinkService(KeyPair jwtKeyPair, SignedLinkStore signedLinkStore) {
return new SignedLinkService(jwtKeyPair.getPrivate(), jwtKeyPair.getPublic(), signedLinkStore);
}

:::warning 快取 TTL 須涵蓋連結壽命 signedLinkCache 的 TTL 必須不小於最長的 SINGLE_USE 連結有效期(link-ttl-minutes),否則 jti 會在 token 仍有效時被逐出,合法連結會被誤判為「已使用」。調大連結有效期時,同步調大 CacheConfigsignedLinkCache TTL。 :::

2. 設定連結與導向參數

設定屬性掛在 app.security.signed-link.*app-server.yml):

app:
security:
signed-link:
link-ttl-minutes: 15 # 免密碼登入連結有效期(分鐘),SINGLE_USE
exchange-code-ttl-seconds: 60 # session 換發 code 有效期(秒),極短效
link-base-url: "https://api.example.com/app-server" # 連結指向的「伺服器」base URL(含 context-path)
redirect-base-url: "https://app.example.com" # consume 後導回的「前端」base URL
dev-log-link: false # dev 才開:把簽出的連結印到 log,方便本機測試
屬性預設說明
link-ttl-minutes15登入連結有效期(分鐘)
event-link-ttl-hours48事件深連結有效期(小時),見下方延伸
exchange-code-ttl-seconds60落地換 token 用的一次性 code 有效期(秒)
link-base-urlhttp://localhost:8080/app-server信中連結指向的伺服器 public URL(含 context-path)
redirect-base-urlhttp://localhost:5173consume 後 303 導回的前端 URL
dev-log-linkfalsedev-only:把連結印到 log(連結是機密,正式環境務必保持 false

這些屬性帶 @conf-env 標記,可用 /env-config 依環境(dev / test / prod)產出外部覆蓋設定,不需手改 YAML。

3. 確認金鑰與郵件已就緒

  • 金鑰:連結與 JWT 共用 app.security.jwt.* 的 RSA 金鑰。dev 由 SecurityConfig 每次啟動產臨時金鑰即可;test / prod 需佈建持久金鑰對(見 /env-configjwt 群組)。
  • 郵件:寄信由 EmailServiceMailer 承擔。dev / test 預設停用郵件——此時連結不會寄出,改用 dev-log-link: true 從 log 取連結貼到瀏覽器測試。正式環境須設定可用的 SMTP / OAuth2 mailer。

4. 前端落地入口

前端 SPA 入口(App.tsx)在啟動時、路由前檢測 URL 的 ?code=,以一次性 code 換回 session,再清掉 code、保留 path:

// 後端 consume 連結後 303 導回前端時附上 ?code=
const signedLinkCode = landingUrl.searchParams.get('code')
if (signedLinkCode) {
await dispatch(exchangeAuthCode(signedLinkCode)).unwrap() // POST /api/v1/auth/exchange
landingUrl.searchParams.delete('code') // 清掉 code、保留 path(深連結 target 正常渲染)
window.history.replaceState({}, '', landingUrl.pathname + landingUrl.search)
}

登入頁只需提供「寄送登入連結」入口,呼叫 service:

// services/iam/auth-service.ts
await apiClient.post('/api/v1/auth/passwordless', { email })

5. 本機測試

  1. 後端 ./gradlew bootRun,前端 npm run dev
  2. 確認 app.security.signed-link.dev-log-link: true(dev tier)。
  3. 在登入頁送出 email → 後端 log 出現 [DEV] Signed link ...
  4. 把連結貼到瀏覽器 → 自動 303 導回前端 → 落地換 token → 已登入。

延伸:事件通知深連結

同一條管線也支援事件通知深連結——系統因事件(如低庫存)主動寄一條 REUSABLE 連結,使用者點擊即免登入直接進入某資源頁(如 /products/123/edit)。由 SignedLinkAppService.sendEventActionLink(email, target, attributes) 觸發,前端落地處理與免密碼登入完全相同。兩情境的差異(贖回政策、是否開對外端點)見框架文檔 簽章連結 的「參考實作走查」。

安全與正式環境注意

  • 連結即憑證:能收到信的人即可登入。link-ttl-minutes 宜短、且為 SINGLE_USE 防重放。
  • 不洩漏帳號passwordless 端點無論 email 是否存在都回 200。
  • 連結是機密:正式環境 dev-log-link 務必 false,連結不應入 log。
  • base URL 要對link-base-url(伺服器)與 redirect-base-url(前端)須指向正式網域,否則信中連結或落地導向會壞掉。
  • 嚴格單次 / 多節點:預設快取 store 為 best-effort;多節點佈署要嚴格單次保證時,改用資料庫 SignedLinkStore(見框架文檔)。

相關文檔