POST /users/login.json
快速結論
- 狀態:已補 legacy JSON path parity、response allowlist、cookie / last-login side effect 與 PHPUnit;staging 部署後 full diff 待補。
- 正式機 access log 統計見
API 路由與遷移/API Migration/pro360_api_82/api_migration/users.md:POST /users/login.json29407 筆200,OPTIONS14095 筆。 - legacy JSON path 不是頁面版 login 流程;controller 只 dispatch
Controller.User.validate_user,實際邏輯在MobileAppEventHandler::validate_user()。 - 2026-06-29 已補 Google
sublookup、Facebook Limited login、Apple transfer fallback、UserProfile.user_idresponse、email OTP consume id、login_logs、user_login_infos、device-id 無密碼失敗分支。 - 2026-06-30 已補 success response
Userlegacy allowlist、CakeCookie[user_language]/il=1/pro360_u_s_tokencookie、users.mobile_app_time_modifiedlegacy 12 小時制、users.last_login_ip_id/last_logged_in_time、user_logins.device_id = NULLfordevice_id="undefined"。 - 2026-07-01 staging Google login 確認
platform=ios需同步 legacygoogle.oauth_client_id_ios = 847081740396-cvvdmv56t4fmfdi4sdakut8qf0jqspk6.apps.googleusercontent.com;new 若仍用舊值847081740396-c58g51kptvticcv5tkumfmgpm78munf9.apps.googleusercontent.com,會因 tokenaud=cvvd...寫Pro360LoginUtilclient id mismatch error。 - 2026-07-21 migration decision:一般 login 成功時不再重用同
user_id + device_idtoken;舊 row 先逐欄寫入api_session_logs(action=login_rotate)並刪除,再於同一 transaction 建立新 token。其他 device 與其他ApiSession::upsert()caller 不受影響。
Legacy 對照
| 檔案 | 行號 | 行為 |
|---|---|---|
/Users/mattsu/Documents/Site/get-lancer-php56/app/Controller/UsersController.php | 2025-2032 | JSON request logout session 後 dispatch Controller.User.validate_user |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/MobileApp/Event/MobileAppEventHandler.php | 3293-3456 | password / Facebook / Google / Apple / email OTP / phone OTP login lookup |
| 同上 | 3353-3359 | Google login 用 token sub 查 users.google_user_id |
| 同上 | 3367-3373 | Facebook Limited login 用 token sub 查 users.facebook_user_id |
| 同上 | 3374-3392 | Apple login 先查 apple_user_id = sub,找不到再用 apple_user_transfers.old_sub -> new_sub |
| 同上 | 3415-3421 | email OTP 成功後 consume email_verifications.id |
| 同上 | 3424-3430 | username 無密碼時不允許 device-id 直接登入,固定走失敗分支 |
| 同上 | 3464-3488、3516-3529、3567-3581 | 成功 / 失敗寫 login_logs;成功且有 seon_session / passwd 寫 user_login_infos |
| 同上 | 3490-3559 | 成功後寫 session、user_logins、response User / UserProfile |
| 同上 | 3498-3504 | 更新 users.mobile_app_hash / mobile_app_time_modified;legacy mobile_app_time_modified 使用 date('Y-m-d h:i:s') 12 小時制 |
| 同上 | 3513-3514 | 寫 api_sessions、user_logins;legacy UserLogin::insertUserLogin() 對 device_id="undefined" 寫 NULL |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/ApiSession.php | 44-79 | upsert() 找到同 user_id + device_id row 時重用原 id,只更新 IP、expires、api key;不寫 api_session_logs |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/UserLogin.php | 23-31、35-51、53-62 | user_logins 欄位、device_id="undefined" 轉 NULL、有 UserProfile.Language.iso2 時設定 CakeCookie[user_language]、設定 il=1 cookie;afterSave() 更新 users.last_login_ip_id / last_logged_in_time |
/Users/mattsu/Documents/Site/get-lancer-php56/app/Model/User.php | 1223-1229 | login response User legacy brief allowlist |
| 同上 | 2061-2087 | 成功後設定 pro360_u_s_token cookie |
New 對照
| 檔案 | 行號 | 行為 |
|---|---|---|
Endpoint/V1/Users.php | 51-58、741-779 | Users::login() action、response mapping;User 使用本 API legacy allowlist,不直接使用 shared User::$SELF_FIELDS_BRIEF |
Endpoint/V1/Users.php | 765-768 | UserProfile 以 user_id 查詢,對齊 legacy contain |
Lib/Util/Pro360LoginUtil.php | 49-108 | Google id_token 驗證;client id mismatch log 會帶 platform、expected_config_key、expected_client_id、actual_aud |
Lib/Util/Pro360LoginUtil.php | 181-385 | login 驗證主流程,依 legacy 順序解析 social token 再找 user |
Lib/Util/Pro360LoginUtil.php | 225-263、400-405 | Google / Facebook / Facebook Limited / Apple lookup;Apple 含 transfer fallback |
Lib/Util/Pro360LoginUtil.php | 269-276 | email OTP consume EmailVerification::getByVerifyCode() 回傳的 flat row id |
Lib/Util/Pro360LoginUtil.php | 334-350 | device-id 無密碼與密碼錯誤維持 legacy failure response / log |
Lib/Util/Pro360LoginUtil.php | 355-382、407-533 | 成功後呼叫 login 專用 session rotation,再寫 user_logins、login_logs、user_login_infos;device_id="undefined" 只在 user_logins 轉 NULL,cookie / response 使用新 token |
Lib/Model/ApiSession.php | 20、120-211 | ACTION_LOGIN_ROTATE 與 rotateForLogin();鎖定同 user/device、archive、delete、insert new session 共用同一 transaction |
Lib/DataObject/ApiSessionLogsDO.php | 13-56 | 對齊實際 schema,包含 session_id、device_id、api_key 欄位 mapping |
Lib/Model/LoginLog.php | 12-28 | 寫 login_logs,payload 與 legacy model addLog() 對齊 |
Lib/Model/UserLoginInfo.php | 24-34 | 寫 user_login_infos,對齊 legacy addInfo() |
差異清單
| 項目 | Legacy | New 目前 | 判斷 |
|---|---|---|---|
| Google user lookup | google_user_id = token.sub | 已補 sub lookup | 已對齊 |
| Facebook Limited login | facebook_user_id = token.sub | 已補 authentication_token path | 已對齊 |
UserProfile response | contain by user_profiles.user_id | 已改查 user_profiles.user_id | 已對齊 |
| email OTP consume | consume EmailVerification.id | 已改用 flat row id | 已對齊 |
login_logs | 成功 / 失敗都寫 | 已補成功 / no user / empty password / wrong password log | 已對齊 |
user_login_infos | 有 seon_session / passwd 時寫 | 已補 INFO_TYPE_SEON 與 HMAC 後的 INFO_TYPE_PASS | 已對齊 |
| Apple transfer fallback | old_sub -> new_sub fallback | 已補 | 已對齊 |
| 無密碼 + device id | legacy 實際固定不通過 | 已移除成功分支,回 error=3 / checkauth=YES | 已對齊 |
success response User 欄位 | User::$SELF_FIELDS_BRIEF legacy 清單,不含 apple_user_id / is_apple_connected / multi_lang_id | Users::LEGACY_LOGIN_USER_FIELDS_BRIEF 專用 allowlist | 已對齊 |
| language cookie | 若 user_profiles.language_id -> languages.iso2 有值,UserLogin::insertUserLogin() 寫 CakeCookie[user_language] | 已補 Pro360LoginUtil::sendLegacyUserLanguageCookie(),無 language 時不送 | 已對齊 |
| login indicator cookie | UserLogin::insertUserLogin() 設 il=1、1 小時 | Pro360LoginUtil::sendLegacyLoginIndicatorCookie() 設同名 cookie | 已對齊 |
| session cookie | User::setCookieLogin($session_id) 設 pro360_u_s_token | Pro360LoginUtil::sendLegacySessionCookie() 設同名 cookie | 已對齊 |
mobile_app_time_modified | date('Y-m-d h:i:s'),12 小時制 | 已補本 API success path 同格式 | 已對齊 |
| last login 欄位 | UserLogin::afterSave() 更新 last_login_ip_id / last_logged_in_time | Pro360LoginUtil::updateLegacyLastLogin() 僅本 API success path 更新 | 已對齊 |
device_id="undefined" | api_sessions.device_id 保留字串;user_logins.device_id 轉 NULL | api_sessions 保留字串;user_logins 呼叫點 normalize 為 NULL | 已對齊 |
| 同 device 重登 token | 重用同 user_id + device_id 的 api_sessions.id | 舊 row 寫 api_session_logs 後刪除,建立新 id | migration decision;2026-07-21 reviewer 明確接受 |
Session rotation migration decision
範圍與順序
只套用 POST /users/login.json 成功路徑,且只處理同一 user_id + device_id row。失敗登入不讀寫 session log;其他 device 保留;login_once、sign_up、search_add、Fetnet、55688 與其他直接呼叫 ApiSession::upsert() 的流程維持既有行為。
驗證 login credential 成功
-> SELECT api_sessions WHERE user_id/device_id FOR UPDATE
-> 有舊 row 時 INSERT api_session_logs(action=login_rotate)
-> DELETE 該舊 row
-> INSERT 新 api_sessions row
-> COMMIT
-> response / pro360_u_s_token cookie 使用新 session_idarchive、delete、new insert 任一步失敗會 rollback,不回傳一個沒有落 DB 的新 token。api_session application log 沿用原 channel;一般首次登入 context status 為 new,有輪替時為 login_rotate。
api_session_logs insert 欄位分類
| 欄位 | 來源 | 說明 |
|---|---|---|
created | application current datetime | archive 當下時間 |
user_id | 舊 api_sessions.user_id | 同已驗證登入 user |
params | json_encode($existingSession) | 完整舊 session snapshot,包含 data / expires;不放 login request、password、social token 或 id_token |
session_id | 舊 api_sessions.id | 被撤銷 token |
device_id | 舊 api_sessions.device_id | 僅撤銷相同 device |
api_key | 舊 api_sessions.api_key | 可為 NULL |
action | constant login_rotate | 表示成功重登造成 token rotation |
admin_user_id | constant 0 | 非 admin 操作 |
新 api_sessions insert 欄位分類
| 欄位 | 來源 |
|---|---|
id | PHP web session regenerate;CLI/test 使用 random_bytes() |
user_id | 已驗證 login user id |
device_id | request device_id;未傳時沿用 legacy my-device-id |
data | HTTP_X_FORWARDED_FOR,fallback REMOTE_ADDR / 127.0.0.1 |
expires | current epoch + 86400 |
api_key | HTTP_X_PRO360_REST_API_KEY,未提供時空字串 |
Schema / deployment 狀態
- 2026-07-21 於本地目前連線的
database-staging-84-test以SHOW COLUMNS/SHOW CREATE TABLE確認api_session_logs已有session_id、device_id、api_key、action、admin_user_id。 - reviewer 決定 log 的
session_id/device_id/api_key維持varchar(190);來源欄位雖為varchar(255),目前api_sessions5202 rows 的實際最大長度分別為 49 / 85 / 32,沒有超過 190 的資料。 - staging / production 實際 schema 尚待部署前確認。若缺 table 或三個新增 mapping 欄位,同 device 第二次 login archive 會失敗並 rollback。
- 本次沒有新增 config key,
Configuration.php不需修改;既有 cookie / social login config checklist 仍照下節處理。
Config 對照
要補哪個
這次 /users/login.json migration 的 cookie domain 在 new 專案讀 api.url。legacy User::setCookieLogin() 讀 site.api_url,但 new Pro360LoginUtil::buildLegacySessionCookieHeader() 已改用既有 api.url,所以部署時要確認 api.url 對應 API host。其他 key 是「確認既有設定」,不是這次 cookie parity 的必要新增。
| 動作 | key | 何時需要 | 目前本地值 | 要怎麼處理 |
|---|---|---|---|---|
| 必確認 | api.url | 每次 login success 都要用來組 pro360_u_s_token cookie domain | https://api-staging.pro360.com.tw | staging / production deployment config 都要確認。staging 應為 https://api-staging.pro360.com.tw;production 要填 production API host。 |
| 缺才補 | user_login_seon_pass_hash | request 有 passwd 時會寫 user_login_infos.INFO_TYPE_PASS HMAC | NULL | 若 staging / production 也缺,需補 legacy 同值;否則 HMAC 會用空字串,雖不影響登入成功,但 DB side effect 不對齊。 |
| 只確認 | google.oauth_client_id* | 只有 Google id_token login 會用 | 本地多數為 NULL | 這是既有 Google / sign_up 共用設定。若要測 Google login,staging / production 必須已有對應 platform client id;password login 不需要。 |
| 只確認 | Apple sign-in config | 只有 Apple code login 會用 | 本地已有 apple_sign_in_pro / apple_sign_in_consumer / apple_sign_in_client_id_web | 這是既有 Apple 共用設定。若要測 Apple login,確認 staging / production 有同組 key;password login 不需要。 |
2026-07-01 Google client id 實測
- legacy staging
/var/www/html/app/Config/settings.yml實際值:google.oauth_client_id_ios = 847081740396-cvvdmv56t4fmfdi4sdakut8qf0jqspk6.apps.googleusercontent.com。 - new PHP82 staging
Lib/Common/Configuration.php當時值:google.oauth_client_id_ios = 847081740396-c58g51kptvticcv5tkumfmgpm78munf9.apps.googleusercontent.com。 - new 實測錯誤:
monolog_error.id = 121763、channel = Pro360LoginUtil、message = Failed to verify google id_token:false client_id。 monolog_error.context新增診斷欄位後可直接看到:platform = ios、expected_config_key = google.oauth_client_id_ios、expected_client_id = 847081740396-c58g51kptvticcv5tkumfmgpm78munf9.apps.googleusercontent.com、actual_aud = 847081740396-cvvdmv56t4fmfdi4sdakut8qf0jqspk6.apps.googleusercontent.com。- migration decision:不新增多 client id allowlist;依 legacy deployment config 同步
google.oauth_client_id_ios到cvvd...。Configuration.php被.gitignore忽略,staging / production 需人工確認實際部署設定。 - config 同步後 staging 重測:
login_logs.id = 11923、created = 2026-07-01 03:52:46、platform = ios、is_login = 1、result = {"l":"google"};monolog_error最新false client_id仍停在id = 121763/2026-07-01 03:49:15,未再新增。
Config 同步提醒
- 最小必確認:
api.url。 - 建議一起確認:
user_login_seon_pass_hash。 - legacy
User::setCookieLogin()讀的是site.api_url;newPro360LoginUtil::buildLegacySessionCookieHeader()讀的是api.url。兩者都要是對應環境的 API host,否則pro360_u_s_tokencookie domain 會錯。 - 同步範圍:
Lib/Common/Configuration.php或等效 deployment config;該檔被.gitignore忽略,切 staging / production 前要人工確認實際環境有同步。
測試結果
docker exec -w /project-data cd63f9147e8d php -l Endpoint/V1/Users.php
docker exec -w /project-data cd63f9147e8d php -l Lib/DataObject/ApiSessionLogsDO.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Model/ApiSession.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Util/Pro360LoginUtil.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Model/LoginLog.php
docker exec -w /project-data cd63f9147e8d php -l Lib/Model/UserLoginInfo.php
docker exec -w /project-data cd63f9147e8d php -l tests/UsersLoginTest.php
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersLoginTest.php
OK (11 tests, 234 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/Pro360LoginUtilTest.php
OK (2 tests, 22 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersLoginOnceTest.php
OK (5 tests, 50 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersForgotPasswordTest.php
OK (3 tests, 39 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/UsersSignUpTest.php
OK (11 tests, 129 assertions)
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/SearchAddStep8IntegrationTest.php
OK (9 tests, 93 assertions)UsersLoginTest.php 覆蓋:
- password login 成功時
UserProfile來自user_profiles.user_id。 - success response 型別:
error/credit_card_debt為 number,user_id/session_id/ legacy id、flag、count 欄位維持 string;nullable DB 欄位允許null。 - failure response 型別:
error為 number,checkauth/message為 string,not-found 類錯誤不回checkauth。 - password login 成功時寫
login_logs、user_login_infos.INFO_TYPE_SEON、user_login_infos.INFO_TYPE_PASS。 - password login success response
User不回 legacy 沒有的apple_user_id/is_apple_connected/multi_lang_id。 - password login 成功時設定 legacy
CakeCookie[user_language]、il=1與pro360_u_s_tokencookie header;沒有 profile language 時不送 language cookie。 - login 成功時更新
users.last_login_ip_id/last_logged_in_time,且users.user_login_count加 1。 - login 成功時
users.mobile_app_time_modified維持 legacyY-m-d h:i:s12 小時制。 - email OTP login 成功時 consume flat
email_verifications.id。 - Google login 使用 token
sub查users.google_user_id,不使用id。 - Facebook Limited login 使用
authentication_token驗出的sub查users.facebook_user_id。 - Apple login 找不到
apple_user_id = old_sub時,用apple_user_transfers.old_sub -> new_subfallback。 - username + matching
device_id但無密碼時仍回 legacy failure,不建立 session /user_logins。 - inactive user 維持 legacy
is_active = 1guard,不建立 session。 device_id="undefined"成功登入時,api_sessions.device_id保留undefined,user_logins.device_id寫NULL。- 同 device 成功重登會產生不同的新 token;舊 token 不再能 load user,archive 欄位與
paramssnapshot 完整,其他 device session 保留。 - 同 device 登入失敗不刪除既有 token,也不寫
action=login_rotate。
部署後驗證
- 部署前必須在 staging / production 分別確認
api_session_logsschema;Configuration.php沒有本次新增 key。 - staging 部署後需補同 device 連續成功登入、錯誤密碼重登、不同 device 保留與舊 token invalid 的 response / DB side effect 實測。
- password / Google / Facebook Limited / Apple / email OTP / failure path full diff 仍待補。