啟用免密碼登入
本指南說明如何在專案中啟用免密碼登入(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 模式」。
已內建的元件
本專案(參考實作)已接好整條管線,啟用免密碼登入不需要寫新程式,只需設定與前端入口:
| 層 | 元件 | 位置 |
|---|---|---|
| 後端 Bean | SignedLinkStore(CacheSignedLinkStore)+ SignedLinkService(與 JWT 共用金鑰) | config/SecurityConfig.java |
| 後端服務 | SignedLinkAppService — 簽連結、組信、consume、exchange | service/auth/SignedLinkAppService.java |
| 後端端點 | SignedLinkController — passwordless / link/consume / exchange | controller/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 仍有效時被逐出,合法連結會被誤判為「已使用」。調大連結有效期時,同步調大 CacheConfig 的 signedLinkCache 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-minutes | 15 | 登入連結有效期(分鐘) |
event-link-ttl-hours | 48 | 事件深連結有效期(小時),見下方延伸 |
exchange-code-ttl-seconds | 60 | 落地換 token 用的一次性 code 有效期(秒) |
link-base-url | http://localhost:8080/app-server | 信中連結指向的伺服器 public URL(含 context-path) |
redirect-base-url | http://localhost:5173 | consume 後 303 導回的前端 URL |
dev-log-link | false | dev-only:把連結印到 log(連結是機密,正式環境務必保持 false) |
這些屬性帶
@conf-env標記,可用/env-config依環境(dev / test / prod)產出外部覆蓋設定,不需手改 YAML。
3. 確認金鑰與郵件已就緒
- 金鑰:連結與 JWT 共用
app.security.jwt.*的 RSA 金鑰。dev 由SecurityConfig每次啟動產臨時金鑰即可;test / prod 需佈建持久金鑰對(見/env-config的jwt群組)。 - 郵件:寄信由
EmailService→ Mailer 承擔。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. 本機測試
- 後端
./gradlew bootRun,前端npm run dev。 - 確認
app.security.signed-link.dev-log-link: true(dev tier)。 - 在登入頁送出 email → 後端 log 出現
[DEV] Signed link ...。 - 把連結貼到瀏覽器 → 自動 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(見框架文檔)。
相關文檔
- 簽章連結(Signed Link) — 框架原語、贖回政策、store 接縫(API 權威來源)
- Mailer 郵件模組 — 寄送連結信件
- 安全性模組使用指南 — Token 黑名單、登入鎖定