登出與 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. 登出要撤銷伺服器信任
前端清掉狀態:
authStore.user = null
localStorage.removeItem('accessToken')只代表前端不再顯示登入狀態。
真正登出要讓 server 端也不再接受:
- session id。
- refresh token。
- remember me token。
- device session。
- 可疑 access token,視風險而定。
否則攻擊者如果已經偷到 token 或 cookie,仍可能繼續使用。
2. 登出策略取決於登入架構
| 架構 | 登出重點 |
|---|---|
| Session Cookie | revoke server session + clear cookie |
| JWT access token | access token 短效,必要時 denylist |
| Refresh token | revoke refresh token / token family |
| BFF | revoke BFF session,server-side token 一起清 |
| Mobile | 清本地 secure storage + revoke refresh token |
所以面試回答不能只說「清 token」。
3. Session 管理是使用者安全功能
成熟產品通常會有:
- 目前登入裝置。
- 最近活動時間。
- 大略位置。
- 登出某台裝置。
- 登出全部裝置。
- 新裝置登入通知。
- 可疑 session 標記。
這不只是後端狀態,也是使用者安全體驗。
二、 單裝置登出
1. Session Cookie 架構
單裝置登出:
revoke current session
clear current cookieasync 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:
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. 前端登出流程
前端應該:
呼叫 POST /logout
-> server revoke
-> 清本地 user state
-> 導回 login 或首頁
-> 停止背景請求不要只做:
localStorage.clear()
router.push('/login')這只是 UI 登出,不是安全登出。
三、 全裝置登出
1. 什麼時候需要全裝置登出?
常見情境:
- 使用者點「登出所有裝置」。
- 修改密碼。
- 忘記密碼重設成功。
- 帳號疑似被盜。
- 管理員停用帳號。
- MFA 被關閉。
- Refresh token reuse detected。
這些情況代表舊登入狀態可能不再可信。
2. Session 架構
async function logoutAllDevices(userId: string, reason: RevokeReason) {
await sessionRepository.revokeAllForUser({
userId,
reason,
})
}如果要保留目前裝置:
await sessionRepository.revokeAllExcept({
userId,
exceptSessionId: currentSessionId,
reason: 'logout_other_devices',
})3. Token 架構
async function revokeAllTokenFamilies(userId: string, reason: RevokeReason) {
await refreshTokenRepository.revokeAllForUser({
userId,
reason,
})
}同時可以更新:
user.tokenVersion += 1讓 resource server 拒絕舊 token version。
4. 密碼重設後要撤銷
忘記密碼成功後,通常要:
撤銷所有 session / refresh token
要求重新登入
通知使用者
寫 audit log延伸讀:忘記密碼流程設計。
四、 管理員踢下線
1. 管理員操作場景
管理員可能需要:
- 停用使用者。
- 踢下某個可疑 session。
- 登出使用者所有裝置。
- 強制使用者重設密碼。
- 因安全事件撤銷全部 token。
這些操作必須有權限與稽核。
2. API 設計
POST /admin/users/:userId/sessions/revokePayload:
{
"reason": "account_compromised",
"scope": "all"
}Server 要檢查:
- 管理員是否有權限。
- 是否需要 step-up authentication。
- 是否寫 audit log。
- 是否通知使用者。
- 是否避免管理員踢掉自己造成事故。
3. Audit log 很重要
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 常見語意:
使用者勾選後,session 或 refresh token 有效期較長。例如:
| 模式 | Idle timeout | Absolute timeout |
|---|---|---|
| 一般登入 | 30 分鐘 | 8 小時 |
| Remember me | 30 天 | 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. 裝置列表資料
type UserSessionView = {
id: string
deviceName: string
browser: string
os: string
approximateLocation?: string
createdAt: string
lastSeenAt: string
current: boolean
}顯示給使用者:
Chrome on macOS
Taipei, Taiwan
Last active: 10 minutes ago
Current device避免顯示過度精細位置或敏感資訊。
2. 登出其他裝置
DELETE /sessions/:sessionId或:
POST /sessions/revoke-others使用者應能撤銷不認識的 session。
3. 新裝置通知
新 session 建立時,如果是新裝置:
你的帳號剛從新的裝置登入。
如果不是你本人,請立即重設密碼並登出其他裝置。這和帳號保護系統互相配合。
延伸讀:防暴力破解與帳號保護。
七、 Session 狀態設計
1. Session 資料模型
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 狀態可以這樣判斷:
function isSessionActive(session: Session) {
return !session.revokedAt && session.expiresAt > new Date()
}expired 和 revoked 不同:
| 狀態 | 意義 |
|---|---|
| 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,且還沒過期:
server 只驗簽章和 exp那登出後它可能仍可用到過期。
這是 token-based 架構的核心取捨。
2. 解法
| 解法 | 取捨 |
|---|---|
| 短效 access token | 簡單,但仍有短窗口 |
| jti denylist | 可立即撤銷,但要查 store |
| session id claim | 每次查 session,有狀態 |
| token version | 權限或密碼變更時整批失效 |
| introspection | resource server 查 auth server |
高風險系統通常不能只靠很長效的 access token。
3. 面試時要講清楚風險窗口
可以說:
登出時撤銷 refresh token,access token 因為短效,最多只在剩餘幾分鐘內有效。
如果業務不能接受這個窗口,就加 denylist 或 session check。這比直接說「JWT 登出不了」更精準。
九、 稽核與通知
1. 需要記錄的事件
| 事件 | 說明 |
|---|---|
| logout | 使用者登出目前裝置 |
| logout_all | 使用者登出所有裝置 |
| session_revoked | session 被撤銷 |
| admin_revoke_sessions | 管理員踢下線 |
| password_reset_revoke | 密碼重設撤銷 session |
| refresh_reuse_detected | refresh token 重放 |
| account_disabled_revoke | 帳號停用撤銷登入 |
2. 通知使用者
適合通知:
- 密碼重設導致所有裝置登出。
- 新裝置登入。
- 管理員撤銷帳號登入。
- MFA 被關閉後撤銷 session。
- 可疑 token 重放。
通知要提供下一步:
如果這不是你本人操作,請立即重設密碼並聯絡客服。3. 不要把 token 寫進 log
Audit log 記:
sessionId
tokenId
familyId
reason不要記:
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:
GET /sessions
DELETE /sessions/:sessionId
POST /sessions/revoke-others
POST /logout查詢:
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,
}))
}撤銷某台裝置:
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',
})
}登出其他裝置:
async function revokeOtherSessions(input: {
userId: string
currentSessionId: string
}) {
await sessionRepository.revokeAllExcept({
userId: input.userId,
exceptSessionId: input.currentSessionId,
reason: 'logout_all',
})
}題目延伸:Token family 管理
如果使用 refresh token:
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 |
| Audit | logout、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,並在高風險事件通知使用者。