端登錄鑒權(quán)全鏈路修復(fù)實戰(zhàn))
關(guān)鍵詞FastAPI · Tortoise ORM · Redis · JWT · Vue3 · 企業(yè)級登錄 · 驗證碼 · Elasticsearch一、背景項目是一個 BOSS 直聘類招聘平臺分三端企業(yè)端boss-company-ui企業(yè)招聘、職位發(fā)布、團隊管理管理端boss-manage-ui企業(yè)管理、認證審核、職位管理求職者端boss-candidate-ui后端是 FastAPI Tortoise ORM MySQL Redis登錄采用JWTjoseHS256驗證碼存 Rediskeyboss-api:enterprise-login:sms:{mobile}。企業(yè)端登錄入口POST /enterprise/login要同時兼容兩類賬號招聘團隊成員RecruitTeam手機號 驗證碼企業(yè)負責人EnterpriseQualification 資質(zhì)聯(lián)系人且企業(yè)已審核通過今天在聯(lián)調(diào)過程中登錄鏈路連續(xù)踩了 4 個坑下面按出現(xiàn)順序復(fù)盤。二、坑 1登錄成功后又被彈回登錄頁現(xiàn)象頁面提示“登錄成功”但瞬間又跳回登錄頁。根因/enterprise/login路由只調(diào)用了team_login要求手機號在RecruitTeam且statusNORMAL而企業(yè)負責人專用的EnterpriseService.login方法存在卻沒有掛載到任何路由。結(jié)果已注冊/認證、但未加入 RecruitTeam 的企業(yè)負責人根本無法登錄前端拿不到 token自然停在登錄頁。修復(fù)把企業(yè)主登錄兜底掛上并補全返回字段# app/apis/enterprise_api.pyenterprise_router.post(/login)asyncdeflogin(loginMobileRequest:LoginMobileRequest):resawaitEnterpriseService.login(loginMobileRequest)return{code:1,message:登錄成功,data:res}# app/services/enterprise_service.pyasyncdeflogin(loginMobileRequest):mobile,codeloginMobileRequest.mobile,loginMobileRequest.code EnterpriseService._verify_sms_code(mobile,code)# 1) 優(yōu)先團隊成員recruit_teamawaitRecruitTeam.get_or_none(mobilemobile,statusTeamMemberStatus.NORMAL,is_deletedDeleteStatus.NOT_DELETED)ifrecruit_team:access_token,refresh_tokencreate_tokens(str(recruit_team.id),mobile,enterprise_idstr(recruit_team.enterprise_id))return{enterprise_access_token:access_token,enterprise_refresh_token:refresh_token,enterprise_id:recruit_team.enterprise_id,user_type:team,is_owner:is_owner,mobile:mobile}# 2) 兜底企業(yè)負責人資質(zhì)聯(lián)系人 已審核通過foreqinawaitEnterpriseQualification.filter(contact_phonemobile):reviewawaitEnterpriseReview.get_or_none(enterprise_ideq.enterprise_id)ifreviewandreview.review_result1:access_token,refresh_tokencreate_tokens(str(eq.enterprise_id),mobile,roleenterprise,enterprise_idstr(eq.enterprise_id))return{enterprise_access_token:access_token,enterprise_refresh_token:refresh_token,enterprise_id:eq.enterprise_id,user_type:enterprise,is_owner:True,mobile:mobile}raiseBusinessError(該手機號未關(guān)聯(lián)可用團隊或已認證企業(yè))三、坑 2企業(yè)端一直顯示“尚未關(guān)聯(lián)到企業(yè)”現(xiàn)象進入「公司資料」頁提示“尚未關(guān)聯(lián)到企業(yè)”。根因舊 token 的user_id是recruit_team.id團隊成員或enterprise_id企業(yè)主前端從 token 里decode出user_id當企業(yè) ID 用 —— 團隊成員賬號直接拿錯壞 token 解不出則顯示未關(guān)聯(lián)。修復(fù)讓 token自包含真實 enterprise_id# app/utils/jwt_util.pydefcreate_tokens(user_id,username,roleNone,enterprise_idNone):payload{user_id:user_id,username:username,type:enterprise}ifrole:payload[role]roleifenterprise_id:payload[enterprise_id]enterprise_id# 關(guān)鍵字段...// 前端 src/utils/jwt.jsexportfunctiongetEnterpriseIdFromToken(token){constpayloaddecodeJwtPayload(token)if(!payload)returnnull// 優(yōu)先讀 token 自帶的 enterprise_id壞 token 也兜得住returnpayload.enterprise_id??payload.user_id??null}并新增后端GET /enterprise/me由后端統(tǒng)一從 token 解析企業(yè)上下文替代前端本地解碼enterprise_router.get(/me)asyncdefenterprise_me(infoDepends(get_enterprise_info)):return{code:1,message:查詢成功,data:info}四、坑 3登錄報“驗證碼已過期”實際是 Redis 與吞異常現(xiàn)象輸入正確驗證碼仍報“驗證碼已過期”后端日志卻顯示redis_code462780, input_code111111信息明顯錯亂。根因兩個redis_client寫死db6而.env.dev配置REDIS_DB8驗證碼的寫入與讀取不在同一個 Redis DB。舊登錄邏輯里team_login與enterprise login各校驗一次驗證碼路由再用try/except把第一次錯誤吞掉導(dǎo)致日志與提示對不上且業(yè)務(wù)錯誤走 HTTP 500前端只顯示“服務(wù)器內(nèi)部錯誤”。修復(fù)Redis 客戶端統(tǒng)一讀配置redis_clientredis.Redis(hostsettings.REDIS_HOST,portsettings.REDIS_PORT,dbsettings.REDIS_DB,decode_responsesTrue)新增BusinessError業(yè)務(wù)異常返回 HTTP 200 code:0message不再污染 500 日志# app/core/exception_handler.pyclassBusinessError(Exception):passapp.exception_handler(BusinessError)asyncdefbusiness_error_handler(request,exc:BusinessError):returnJSONResponse(status_code200,content{code:0,message:str(exc)})驗證碼只校驗一次并區(qū)分“已過期 / 錯誤”staticmethoddef_verify_sms_code(mobile,code):ifsettings.debug_modeandcode000000:# dev 萬能碼returnredis_coderedis_client.get(fboss-api:enterprise-login:sms:{mobile})ifredis_codeisNone:raiseBusinessError(驗證碼已過期請重新獲取)ifredis_code!code:raiseBusinessError(驗證碼錯誤)五、坑 4登錄提示“未獲取到有效令牌”現(xiàn)象登錄頁直接報“登錄失敗未獲取到有效令牌”。根因src/api/enterprise.js的enterpriseLogin用的是裸 axios沒走request攔截器返回的是AxiosResponse而Login.vue按攔截器習(xí)慣寫成const data res.data把后端統(tǒng)一響應(yīng)體{code,message,data}當成了內(nèi)層 token 對象自然取不到enterprise_access_token。修復(fù)Login.vue的onSubmit先取res.data作為后端統(tǒng)一響應(yīng)體再判斷code1取內(nèi)層dataconstresawaitenterpriseLogin({mobile:form.phone,code:form.code})constresultres.data||{}if(result.code!1){ElMessage.error(result.message||登錄失敗)return}constdataresult.data||{}consttokendata.enterprise_access_tokenif(!token){ElMessage.error(登錄失敗未獲取到有效令牌)return}localStorage.setItem(companyToken,token)localStorage.setItem(companyUserInfo,JSON.stringify({mobile:data.mobile||form.phone,enterprise_id:data.enterprise_id,user_type:data.user_type||team,is_owner:data.is_ownertrue}))awaitrouter.replace(/company/dashboard)經(jīng)驗裸 axios 與封裝 request 的返回值結(jié)構(gòu)不同調(diào)用方必須按實際返回取值。統(tǒng)一約定很重要。六、配套的權(quán)限與接口加固1. 鑒權(quán) 500 → 401get_enterprise_info在 token 無效/無權(quán)限時拋AuthenticationError全局異常處理器返回HTTP 401 code:401前端攔截器據(jù)此清 token 并跳登錄// 響應(yīng)攔截器if(res.code401){localStorage.removeItem(companyToken)localStorage.removeItem(companyUserInfo)router.replace(/login)}2. 企業(yè)管理 / 認證審核職責分離account_status1→ 出現(xiàn)在「認證審核」頁注冊的待審信息account_status0/2/3→ 出現(xiàn)在「企業(yè)管理」頁已審核正常/封禁/已駁回新增POST /enterprise/ban真實封禁/解封接口3. 職位管理搜索改用 Elasticsearch/job/list在有關(guān)鍵字/城市時走 ESboss_job_index_v2ik_max_word 中文分詞ES 異常自動降級到 MySQL 模糊查詢保證可用性。索引需先建POST /es-data/create-index-v2 POST /es-data/insert-data-v2七、避坑清單可直接抄Redis DB 一定要讀配置不要硬編碼否則換環(huán)境驗證碼讀寫不一致。驗證碼只校驗一次路由層不要 try/except 吞掉上游業(yè)務(wù)異常。業(yè)務(wù)錯誤用專用異常BusinessErrorcode:0前端能展示真實 message別用 500。JWT 把必要上下文如 enterprise_id寫進 payload避免前端再去 decode 推斷。前端封裝 request 與裸 axios 返回值結(jié)構(gòu)不同調(diào)用處務(wù)必統(tǒng)一約定。HTTP 401 清登錄態(tài)并跳轉(zhuǎn)避免壞 token 一直卡在某個頁面。團隊賬號與企業(yè)主賬號的 ID 空間可能重合靠role字段區(qū)分別用 user_id 硬猜。八、小結(jié)登錄看似簡單其實橫跨前端取值、JWT 簽發(fā)、Redis 驗證碼、后端鑒權(quán)依賴、異常體系五層。今天把這條鏈路從“反復(fù) 500 / 跳回登錄 / 未關(guān)聯(lián) / 無令牌”四個坑全部填平核心就三件事后端單一 login 一次校驗 BusinessError鑒權(quán)role 區(qū)分賬號類型 401 清態(tài)前端按真實返回結(jié)構(gòu)取值 攔截器統(tǒng)一跳轉(zhuǎn)。這樣企業(yè)端和管理端才真正跑通。