多裝置登入與指定裝置登出任務規劃
更新時間:2026-07-03
目標
讓使用者可以看到自己目前有效登入裝置,並可登出指定裝置或登出其他所有裝置。
本任務核心不是新增另一套 token,而是管理既有 api_sessions:
- 同一個
user_id可以有多筆api_sessions。 - 不同
device_id通常會有不同session_id。 - 同一個
user_id + device_id再登入時,legacy/new 都會優先重用既有api_sessions.id,並刷新data、expires、api_key。 - 登出某台裝置就是刪掉該裝置對應的
api_sessionsrow。
目前 Session 機制
DB
主要資料表:api_sessions
| 欄位 | 用途 |
|---|---|
id | session token,也就是 response 的 session_id / request 的 X-PRO360-User-Session-Token / cookie pro360_u_s_token |
user_id | session 屬於哪個 user |
device_id | 登入時傳入的裝置識別 |
data | 登入 IP,通常來自 HTTP_X_FORWARDED_FOR |
expires | session 到期 unix timestamp |
api_key | 登入時使用的 X-PRO360-Rest-Api-Key |
查詢目前帳號 session:
SELECT
id AS session_id,
user_id,
device_id,
api_key,
FROM_UNIXTIME(expires) AS expires_at
FROM api_sessions
WHERE user_id = :current_user_id
ORDER BY expires DESC;只列有效 session:
SELECT
id AS session_id,
device_id,
api_key,
data AS ip,
FROM_UNIXTIME(expires) AS expires_at
FROM api_sessions
WHERE user_id = :current_user_id
AND expires > UNIX_TIMESTAMP()
ORDER BY expires DESC;Legacy 對照
session_id 產生與 upsert
legacy 主要方法:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/ApiSession.php
ApiSession::upsert($user_id, $device_id)行為:
- 呼叫
session_regenerate_id()。 - 用
session_id()取得新的候選 session id。 - 查
api_sessions是否已有相同user_id + device_id。 - 如果已有,回傳舊的
ApiSession.id,只更新:dataexpires = time() + 86400api_key
- 如果沒有,新增一筆:
id = session_id()user_iddevice_iddataexpiresapi_key
- 寫
api_sessionlog,context 為existing/new、device_id、session_id。
因此 legacy 下:
- 不同
device_id登入會有不同 session。 - 相同
device_id再登入會重用同一個session_id。
legacy login 入口
legacy /users/login.json 成功後會呼叫:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/MobileApp/Event/MobileAppEventHandler.php
ClassRegistry::init('ApiSession')->upsert($user_id, $device_id)成功 response 會包含:
{
"error": 0,
"message": "Success",
"user_id": "...",
"session_id": "..."
}同時呼叫:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php
User::setCookieLogin($session_id)會設定 cookie:
pro360_u_s_token=<session_id>cookie 屬性:
Expires = time() + 31536000Path=/Domain=<site.api_url host>SecureHttpOnlySameSite=None
legacy session 驗證
legacy 主要方法:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/RestApiHelper.php
RestApiHelper::hasValidApiSession($controller)流程:
- 先驗
X-PRO360-Rest-Api-Key。 - 取 session id:
- 優先
pro360_u_s_tokencookie - 沒 cookie 才讀
X-PRO360-User-Session-Token
- 優先
- 用 session id 查
api_sessions。 - 用
api_sessions.user_id查users。 - blocked user 直接回 HTTP 403。
- 更新
users.iphone_last_access、last_access_client。 - 通過後把 user id 寫入 request params。
注意:RestApiHelper::getUserId($request) 不是純 getter,還會寫 full request info log。
legacy 既有刪 session 參考
legacy 有 admin/CS 工具:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Lib/CsModifyUtil.php
CsModifyUtil::logoutAllSessionToken($user_id)行為:
- 查該 user 所有
api_sessions。 - 寫操作 log。
- 逐筆
DELETE FROM api_sessions WHERE id = :id。
這是「登出所有裝置」概念,不是使用者自行登出指定裝置。
New 對照
session_id 產生與 upsert
new 主要方法:
Lib/Model/ApiSession.php
\PRO360\Model\ApiSession::upsert($user_id, $device_id)行為對齊 legacy:
- 可啟動 PHP session 時先
session_start()。 - 查
api_sessions是否已有相同user_id + device_id。 - 如果已有,回傳既有
id,更新:dataexpires = time() + 86400api_key
- 如果沒有,產生 session id:
- web runtime:
session_regenerate_id()+session_id() - CLI/test:
bin2hex(random_bytes(16))
- web runtime:
- 新增
api_sessionsrow。 - 寫
api_sessionlog。
new login 入口
new /users/login.json:
Endpoint/V1/Users.php
Users::login()會呼叫:
Lib/Util/Pro360LoginUtil.php
Pro360LoginUtil::validate_user()成功後:
ApiSession::upsert($user_id, $device_id)建立或重用 session。- response 回
session_id。 - 設定 cookie
pro360_u_s_token=<session_id>。 - 寫
user_logins、login_logs、user_login_infos等 login side effect。
new session 驗證
new 主要方法:
Lib/Validation/ApiValidation.php
ApiValidation::assertApiSession($request, $usersDO)
ApiValidation::hasValidApiSession($request, ...)流程:
- 先驗
X-PRO360-Rest-Api-Key。 - 取 session id:
- 優先
pro360_u_s_tokencookie - 沒 cookie 才讀
X-PRO360-User-Session-Token
- 優先
- 用 session id 查
api_sessions。 - 用
api_sessions.user_id查users。 - 檢查 blocked / inactive。
- 更新 legacy access side effect,例如
iphone_last_access、last_access_client。
指定裝置登出設計
建議 API
以 users endpoint 實作,保持和既有 session 管理一致:
GET /users/sessions.json
POST /users/sessions/delete.json
POST /users/sessions/delete_others.json如果 routing 不適合巢狀 path,也可用:
GET /users/list_sessions.json
POST /users/delete_session.json
POST /users/delete_other_sessions.jsonGET /users/sessions.json
認證:
- valid API key
- valid user session
查詢:
SELECT
id AS session_id,
device_id,
api_key,
data AS ip,
FROM_UNIXTIME(expires) AS expires_at
FROM api_sessions
WHERE user_id = :current_user_id
AND expires > UNIX_TIMESTAMP()
ORDER BY expires DESC;response 建議:
{
"error": 0,
"sessions": [
{
"session_id": "...",
"device_id": "...",
"expires_at": "...",
"is_current": true
}
]
}注意:
api_sessions沒有created/modified,不能直接顯示登入時間。data是 IP,但可能是 CloudFront / proxy chain;前端顯示前要確認產品需求。api_key不建議原樣回前端;若要顯示來源,用ApiKey::getPlatform()或後端 mapping 成 platform。
POST /users/sessions/delete.json
request:
{
"session_id": "target_session_id"
}認證:
- valid API key
- valid current user session
刪除 SQL:
DELETE FROM api_sessions
WHERE id = :target_session_id
AND user_id = :current_user_id;重點:
- 一定要加
user_id = :current_user_id,避免刪到別人的 session。 - 不建議只用
device_id刪,因為 device id 可能重複、空值或my-device-id。 - 若
target_session_id === current_session_id,刪除後要清pro360_u_s_tokencookie,前端也要執行本機 logout。 - 若刪的是其他裝置,當前裝置不應被登出。
建議 log:
api_session_logs.action = delete_session
api_session_logs.user_id = current_user_id
api_session_logs.params = deleted api_session row json
api_session_logs.admin_user_id = 0POST /users/sessions/delete_others.json
認證:
- valid API key
- valid current user session
刪除 SQL:
DELETE FROM api_sessions
WHERE user_id = :current_user_id
AND id <> :current_session_id;這等同「登出所有其他裝置」,保留目前裝置。
new 目前已有可參考方法:
Endpoint/V1/Users.php
Users::clear_login()既有行為:
keep_current=1時保留目前 session,刪其他 session。- 刪除前寫
api_session_logs。
可評估是否直接補 route / 文件讓前端使用,或新增更明確的 user-facing API。
實作檢查清單
- 確認前端要顯示哪些欄位:device id、平台、IP、到期時間、目前裝置。
- 新增 list endpoint,僅查目前 user 的有效 sessions。
- 新增 delete target endpoint,用
session_id + current_user_id刪除。 - 刪目前 session 時清 cookie;刪其他 session 時不影響目前 session。
- 寫
api_session_logs,保存刪除前 row。 - 補 PHPUnit:
- list 只回目前 user sessions
- 不回過期 sessions
- 刪其他 device session 成功
- 刪別人的 session 不成功
- 刪目前 session 後 cookie 清除
- delete_others 保留目前 session
- staging 驗證:
- A/B device login 產生不同 session
- A 刪 B session 後,B 下一次 API invalid session
- A 刪自己 session 後,A 前端登出
風險與決策點
- cookie 優先於 header:測試時如果同時帶 cookie 和 header,實際使用的是 cookie。
device_id不是安全邊界,只能當顯示資訊;刪除目標應使用session_id。- 既有
api_sessions.expires過期 row 不會自動消失;列表應預設只顯示expires > UNIX_TIMESTAMP()。 - 若要顯示「登入時間」,目前
api_sessionsschema 不足,需要另外查user_logins或新增欄位;這是產品/DB migration decision。 - legacy/new 都會在相同
user_id + device_id重用 session id;若前端產生固定 device id,使用者可能看不到多筆同裝置 session。