跳至主要內容
Skip to content

登出與 Session 管理

登出不是把前端的 user state 清掉,也不是單純刪掉 localStorage。真正的登出要讓伺服器不再接受這個登入狀態;真正的 Session 管理,則要能回答「這個使用者現在有哪些裝置登入?哪些要撤銷?哪些是可疑登入?密碼重設後哪些 token 還有效?」

一句話回答:登出與 Session 管理的核心是撤銷 server-side authentication state:Session Cookie 架構下要 revoke session 並清除 cookie;JWT + Refresh Token 架構下至少要撤銷 refresh token / token family,必要時 denylist access token;系統應支援單裝置登出、全裝置登出、管理員踢下線、密碼重設後撤銷舊 session、remember me 生命週期、裝置列表與 audit log。


一、 必考觀念

1. 登出要撤銷伺服器信任

前端清掉狀態:

typescript
authStore.user = null
localStorage.removeItem('accessToken')

只代表前端不再顯示登入狀態。

真正登出要讓 server 端也不再接受:

  • session id。
  • refresh token。
  • remember me token。
  • device session。
  • 可疑 access token,視風險而定。

否則攻擊者如果已經偷到 token 或 cookie,仍可能繼續使用。

2. 登出策略取決於登入架構

架構登出重點
Session Cookierevoke server session + clear cookie
JWT access tokenaccess token 短效,必要時 denylist
Refresh tokenrevoke refresh token / token family
BFFrevoke BFF session,server-side token 一起清
Mobile清本地 secure storage + revoke refresh token

所以面試回答不能只說「清 token」。

3. Session 管理是使用者安全功能

成熟產品通常會有:

  • 目前登入裝置。
  • 最近活動時間。
  • 大略位置。
  • 登出某台裝置。
  • 登出全部裝置。
  • 新裝置登入通知。
  • 可疑 session 標記。

這不只是後端狀態,也是使用者安全體驗。


二、 單裝置登出

單裝置登出:

text
revoke current session
clear current cookie
typescript
async function logout(request: Request, reply: Reply) {
  const sessionId = request.cookies['__Host-sid']

  if (sessionId) {
    await sessionRepository.revoke({
      sessionId,
      reason: 'logout',
    })
  }

  reply.setCookie('__Host-sid', '', {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: 0,
  })

  return {
    status: 'ok',
  }
}

注意:

  • 清 cookie 要匹配原本 Path / Domain。
  • server session 也要 revoke。
  • 登出 API 本身也要防 CSRF。

2. JWT + Refresh Token 架構

單裝置登出通常撤銷目前 token family:

typescript
async function logout(input: LogoutInput) {
  await refreshTokenRepository.revokeFamily({
    familyId: input.refreshTokenFamilyId,
    reason: 'logout',
  })

  if (input.accessTokenJti) {
    await accessTokenDenylist.add({
      jti: input.accessTokenJti,
      expiresAt: input.accessTokenExp,
      reason: 'logout',
    })
  }
}

如果 access token 很短效,也可以只撤銷 refresh token,接受 access token 在剩餘有效期內仍可用的風險。

延伸讀:Access Token 與 Refresh Token

3. 前端登出流程

前端應該:

text
呼叫 POST /logout
-> server revoke
-> 清本地 user state
-> 導回 login 或首頁
-> 停止背景請求

不要只做:

typescript
localStorage.clear()
router.push('/login')

這只是 UI 登出,不是安全登出。


三、 全裝置登出

1. 什麼時候需要全裝置登出?

常見情境:

  • 使用者點「登出所有裝置」。
  • 修改密碼。
  • 忘記密碼重設成功。
  • 帳號疑似被盜。
  • 管理員停用帳號。
  • MFA 被關閉。
  • Refresh token reuse detected。

這些情況代表舊登入狀態可能不再可信。

2. Session 架構

typescript
async function logoutAllDevices(userId: string, reason: RevokeReason) {
  await sessionRepository.revokeAllForUser({
    userId,
    reason,
  })
}

如果要保留目前裝置:

typescript
await sessionRepository.revokeAllExcept({
  userId,
  exceptSessionId: currentSessionId,
  reason: 'logout_other_devices',
})

3. Token 架構

typescript
async function revokeAllTokenFamilies(userId: string, reason: RevokeReason) {
  await refreshTokenRepository.revokeAllForUser({
    userId,
    reason,
  })
}

同時可以更新:

text
user.tokenVersion += 1

讓 resource server 拒絕舊 token version。

4. 密碼重設後要撤銷

忘記密碼成功後,通常要:

text
撤銷所有 session / refresh token
要求重新登入
通知使用者
寫 audit log

延伸讀:忘記密碼流程設計


四、 管理員踢下線

1. 管理員操作場景

管理員可能需要:

  • 停用使用者。
  • 踢下某個可疑 session。
  • 登出使用者所有裝置。
  • 強制使用者重設密碼。
  • 因安全事件撤銷全部 token。

這些操作必須有權限與稽核。

2. API 設計

http
POST /admin/users/:userId/sessions/revoke

Payload:

json
{
  "reason": "account_compromised",
  "scope": "all"
}

Server 要檢查:

  • 管理員是否有權限。
  • 是否需要 step-up authentication。
  • 是否寫 audit log。
  • 是否通知使用者。
  • 是否避免管理員踢掉自己造成事故。

3. Audit log 很重要

typescript
await auditLog.write({
  type: 'admin_revoke_sessions',
  actorUserId: admin.id,
  targetUserId: userId,
  reason: input.reason,
  requestId: request.id,
})

踢下線是高權限操作,不應無痕。


五、 Remember Me

1. Remember me 是延長登入,不是永久登入

Remember me 常見語意:

text
使用者勾選後,session 或 refresh token 有效期較長。

例如:

模式Idle timeoutAbsolute timeout
一般登入30 分鐘8 小時
Remember me30 天30 天

但高風險操作仍應要求重新驗證。

2. Remember me token 要可撤銷

不要用永不過期 cookie。

應該:

  • 每個裝置一筆 remember token。
  • server-side 記錄。
  • token 只存 hash。
  • 可撤銷。
  • 有到期時間。
  • 修改密碼後撤銷。

3. Remember me 與 MFA

使用者可能問:

我勾了 remember me,還要 MFA 嗎?

可以設計:

  • 記住登入狀態。
  • 記住可信裝置。
  • 但高風險操作仍要求 MFA。
  • 管理員帳號不允許長期 remember me。

不要讓 remember me 繞過所有安全檢查。


六、 裝置列表

1. 裝置列表資料

typescript
type UserSessionView = {
  id: string
  deviceName: string
  browser: string
  os: string
  approximateLocation?: string
  createdAt: string
  lastSeenAt: string
  current: boolean
}

顯示給使用者:

text
Chrome on macOS
Taipei, Taiwan
Last active: 10 minutes ago
Current device

避免顯示過度精細位置或敏感資訊。

2. 登出其他裝置

http
DELETE /sessions/:sessionId

或:

http
POST /sessions/revoke-others

使用者應能撤銷不認識的 session。

3. 新裝置通知

新 session 建立時,如果是新裝置:

text
你的帳號剛從新的裝置登入。
如果不是你本人,請立即重設密碼並登出其他裝置。

這和帳號保護系統互相配合。

延伸讀:防暴力破解與帳號保護


七、 Session 狀態設計

1. Session 資料模型

typescript
type Session = {
  id: string
  userId: string
  createdAt: string
  lastSeenAt: string
  expiresAt: string
  lastAuthenticatedAt: string
  mfaVerifiedAt?: string
  revokedAt?: string
  revokedReason?: RevokeReason
  ip?: string
  userAgent?: string
}

type RevokeReason =
  | 'logout'
  | 'logout_all'
  | 'password_reset'
  | 'admin_revoke'
  | 'account_disabled'
  | 'risk_detected'

2. Active、Expired、Revoked

Session 狀態可以這樣判斷:

typescript
function isSessionActive(session: Session) {
  return !session.revokedAt && session.expiresAt > new Date()
}

expiredrevoked 不同:

狀態意義
expired自然過期
revoked被主動撤銷

Revoked reason 對稽核很重要。

3. Soft delete vs hard delete

Hard delete:

  • 實作簡單。
  • 沒有歷史。

Soft revoke:

  • 可稽核。
  • 可查安全事件。
  • 可顯示最近登出原因。

安全系統通常會保留事件 log,即使 active session 本身被清掉。


八、 Access Token 剩餘有效期

1. 登出後 access token 還能用嗎?

如果 access token 是 stateless JWT,且還沒過期:

text
server 只驗簽章和 exp

那登出後它可能仍可用到過期。

這是 token-based 架構的核心取捨。

2. 解法

解法取捨
短效 access token簡單,但仍有短窗口
jti denylist可立即撤銷,但要查 store
session id claim每次查 session,有狀態
token version權限或密碼變更時整批失效
introspectionresource server 查 auth server

高風險系統通常不能只靠很長效的 access token。

3. 面試時要講清楚風險窗口

可以說:

text
登出時撤銷 refresh token,access token 因為短效,最多只在剩餘幾分鐘內有效。
如果業務不能接受這個窗口,就加 denylist 或 session check。

這比直接說「JWT 登出不了」更精準。


九、 稽核與通知

1. 需要記錄的事件

事件說明
logout使用者登出目前裝置
logout_all使用者登出所有裝置
session_revokedsession 被撤銷
admin_revoke_sessions管理員踢下線
password_reset_revoke密碼重設撤銷 session
refresh_reuse_detectedrefresh token 重放
account_disabled_revoke帳號停用撤銷登入

2. 通知使用者

適合通知:

  • 密碼重設導致所有裝置登出。
  • 新裝置登入。
  • 管理員撤銷帳號登入。
  • MFA 被關閉後撤銷 session。
  • 可疑 token 重放。

通知要提供下一步:

text
如果這不是你本人操作,請立即重設密碼並聯絡客服。

3. 不要把 token 寫進 log

Audit log 記:

text
sessionId
tokenId
familyId
reason

不要記:

text
raw refresh token
access token
cookie value

十、 追問題庫

Q1:登出到底要做什麼?

要撤銷 server-side 登入狀態。Session Cookie 架構要 revoke session 並清 cookie;Token 架構至少要撤銷 refresh token,必要時 denylist access token 或撤銷 session。

Q2:只刪 localStorage 算登出嗎?

不算完整登出。那只是前端丟掉 token。Server 端如果還接受 refresh token 或 session,攻擊者拿到憑證仍可能使用。

Q3:JWT 登出怎麼做?

常見做法是 access token 短效,登出時撤銷 refresh token。若需要立即失效 access token,可以用 jti denylist、session id claim、token version 或 introspection。

Q4:修改密碼後要不要登出所有裝置?

通常要撤銷其他 session / refresh token,因為修改密碼可能是帳號被盜後的恢復動作。高風險系統甚至會要求所有裝置重新登入。

Q5:Remember me 安全嗎?

可以安全設計,但不能是永不過期 token。它應該有 server-side record、hash 儲存、可撤銷、可列入裝置管理,並在高風險操作時要求重新驗證。

Q6:使用者裝置列表是怎麼來的?

通常來自 session table 或 refresh token session。每個裝置或登入狀態一筆紀錄,保存 userAgent、lastSeenAt、大略位置與 revokedAt 等資訊。


十一、 實作題

題目:設計 Session 管理 API

需求:

  • 查詢目前登入裝置。
  • 登出某一台裝置。
  • 登出其他所有裝置。
  • 修改密碼後撤銷其他 session。

API:

text
GET /sessions
DELETE /sessions/:sessionId
POST /sessions/revoke-others
POST /logout

查詢:

typescript
async function listSessions(userId: string, currentSessionId: string) {
  const sessions = await sessionRepository.findActiveByUser(userId)

  return sessions.map(session => ({
    id: session.id,
    deviceName: parseDeviceName(session.userAgent),
    approximateLocation: session.approximateLocation,
    createdAt: session.createdAt,
    lastSeenAt: session.lastSeenAt,
    current: session.id === currentSessionId,
  }))
}

撤銷某台裝置:

typescript
async function revokeSession(input: {
  userId: string
  sessionId: string
}) {
  const session = await sessionRepository.findById(input.sessionId)

  if (!session || session.userId !== input.userId) {
    throw new NotFoundError()
  }

  await sessionRepository.revoke({
    sessionId: session.id,
    reason: 'logout',
  })
}

登出其他裝置:

typescript
async function revokeOtherSessions(input: {
  userId: string
  currentSessionId: string
}) {
  await sessionRepository.revokeAllExcept({
    userId: input.userId,
    exceptSessionId: input.currentSessionId,
    reason: 'logout_all',
  })
}

題目延伸:Token family 管理

如果使用 refresh token:

typescript
async function revokeCurrentDevice(sessionId: string) {
  await refreshTokenRepository.revokeFamiliesBySession({
    sessionId,
    reason: 'logout',
  })
}

用 session 作為裝置與 refresh token family 的共同邊界,會更容易管理。


十二、 資深視角

1. 登出是安全事件

登出不只是 UX。

它牽涉:

  • 憑證撤銷。
  • 風險控制。
  • 使用者通知。
  • 稽核紀錄。
  • 多裝置一致性。

尤其是管理員踢下線、密碼重設、refresh token reuse,這些都要當安全事件處理。

2. Session 管理要支援未來擴展

未來可能會加:

  • MFA 狀態。
  • Passkey 裝置。
  • OAuth client sessions。
  • SSO logout。
  • 裝置信任。
  • 風險分數。
  • 管理員 impersonation。

如果一開始只有 isLogin,後面會很難擴展。

3. 不同風險有不同生命週期

一般會員、管理員、醫療、金融、政府服務不應同一套 session 策略。

例如:

場景策略
一般會員可 remember me,可裝置管理
管理員短 session,強制 MFA
金流高風險操作 step-up
醫療資料嚴格 timeout 與 audit
政府服務高信任身份與重新驗證

總結

問題回答重點
登出撤銷 server-side auth state,不只是清前端
單裝置登出revoke current session / token family
全裝置登出revoke all sessions / refresh tokens
JWT 登出短效 access + revoke refresh,必要時 denylist
Remember me長一點的登入生命週期,但要可撤銷
裝置列表基於 session / token family 顯示與撤銷
密碼重設應撤銷舊 session / refresh token
Auditlogout、revoke、admin kick、reuse detected 都要記

一句完整的面試回答可以是:

登出與 Session 管理的核心是撤銷伺服器對登入狀態的信任。Session Cookie 架構下,登出要 revoke server session,並用 Set-Cookie 清掉瀏覽器 cookie;JWT 架構下,至少要撤銷 refresh token 或 token family,access token 因為短效可以接受剩餘幾分鐘有效,若業務不能接受,就用 jti denylist、session id claim 或 introspection。系統應支援單裝置登出、登出其他裝置、全裝置登出、管理員踢下線,並在修改密碼、忘記密碼成功、帳號停用、refresh token reuse detected 時撤銷相關 session。Remember me 不是永久登入,而是較長生命週期的 session 或 refresh token,也要可撤銷。所有登出和撤銷操作都應寫 audit log,並在高風險事件通知使用者。

延伸閱讀