實(shí)戰(zhàn)教程(十二):安全地交換雙方微信號(hào))
“AI 軟件開發(fā)實(shí)戰(zhàn)教程”系列第 12 篇讓候選中的一方主動(dòng)確認(rèn)后雙方同時(shí)獲得對(duì)方微信號(hào)同時(shí)保證確認(rèn)前不泄露、重復(fù)點(diǎn)擊不重復(fù)、資料修改不改寫歷史。系統(tǒng)找到“可能同路”的人以后真正的溝通仍要回到微信。鄰行不做站內(nèi)聊天因此聯(lián)系方式交換是第一條核心流程中風(fēng)險(xiǎn)最高的一步。如果只把微信號(hào)顯示在候選詳情里會(huì)出現(xiàn)三個(gè)問(wèn)題用戶沒(méi)有明確同意把自己的聯(lián)系方式給對(duì)方頁(yè)面權(quán)限的一處疏漏就可能批量泄露用戶修改微信號(hào)后過(guò)去已經(jīng)交換的結(jié)果會(huì)悄悄變化。K4 先把交換定義成一個(gè)獨(dú)立、不可覆蓋的業(yè)務(wù)事實(shí)。“雙向交換”不是“雙向?qū)徟碑a(chǎn)品規(guī)則是候選雙方中的任何一方都可以發(fā)起。確認(rèn)頁(yè)必須明確告訴發(fā)起者確認(rèn)后你會(huì)看到對(duì)方的微信號(hào) 你的微信號(hào)也會(huì)同時(shí)提供給對(duì)方。 這只是方便雙方回微信溝通 不代表已經(jīng)約定同行。對(duì)方不需要再點(diǎn)一次同意。因此頁(yè)面不能寫成“雙向確認(rèn)”否則發(fā)起者會(huì)誤以為還要等待。一次操作同時(shí)披露是這個(gè)產(chǎn)品在小范圍社區(qū)場(chǎng)景中的明確選擇。它減少了雙方異步上線造成的等待但也要求確認(rèn)文案、權(quán)限復(fù)核和審計(jì)事實(shí)都足夠清楚。紅燈先固定授權(quán)邊界K4 的第一組測(cè)試在導(dǎo)入階段失敗因?yàn)榻粨Q模型和服務(wù)還不存在。隨后測(cè)試逐個(gè)固定這些拒絕條件操作者不是候選雙方之一任一方賬戶被限制或不可用候選已經(jīng)失效任一出行信息已經(jīng)關(guān)閉、取消或過(guò)期已到雙方較早的預(yù)計(jì)出發(fā)時(shí)間雙方不再屬于同一個(gè)社區(qū)任一方尚未設(shè)置微信號(hào)。交換服務(wù)不相信用戶剛剛打開的候選頁(yè)面。點(diǎn)擊確認(rèn)到請(qǐng)求抵達(dá)服務(wù)器之間狀態(tài)可能已經(jīng)改變所以服務(wù)會(huì)在短事務(wù)中重新鎖定和檢查候選及雙方信息。一個(gè)容易混淆的邊界是匹配截止。它只表示停止產(chǎn)生新候選。截止前已經(jīng)形成的候選即使現(xiàn)在已經(jīng)停止匹配只要雙方尚未到較早預(yù)計(jì)出發(fā)時(shí)間仍然可以交換微信號(hào)。專門的注入時(shí)鐘測(cè)試固定了這條規(guī)則。保存“當(dāng)時(shí)披露的值”交換不能只保存兩個(gè)用戶 ID然后每次打開頁(yè)面去讀取最新個(gè)人資料。假設(shè)乘客交換時(shí)的微信號(hào)是wx_old后來(lái)改成wx_new。過(guò)去的車主已經(jīng)拿到wx_old系統(tǒng)卻顯示wx_new歷史事實(shí)就不再可信。因此ContactExchange保存候選 發(fā)起者 交換時(shí)間 車主當(dāng)時(shí)微信號(hào)的加密快照 乘客當(dāng)時(shí)微信號(hào)的加密快照 密鑰版本服務(wù)先用個(gè)人資料對(duì)應(yīng)版本的密鑰解密再用當(dāng)前活動(dòng)密鑰重新加密為交換快照。測(cè)試在交換后修改乘客資料舊結(jié)果仍必須返回交換時(shí)的值。這里保存快照不是為了無(wú)限保留。賬戶刪除任務(wù)仍要按產(chǎn)品規(guī)則擦除該用戶在在線資料和交換快照中的敏感值那屬于后續(xù) K8 的數(shù)據(jù)生命周期工作。一對(duì)一約束是冪等的最后防線同一個(gè)候選只能有一次聯(lián)系方式交換。數(shù)據(jù)模型使用候選的一對(duì)一關(guān)系事務(wù)中也先返回已經(jīng)存在的結(jié)果。測(cè)試讓車主先發(fā)起再由乘客重復(fù)發(fā)起最終只能得到1 個(gè) ContactExchange 1 個(gè) contact.exchanged 業(yè)務(wù)事件 雙方各 1 條站內(nèi)提醒 雙方各 1 條渠道投遞記錄無(wú)論雙擊按鈕、網(wǎng)絡(luò)重試還是雙方幾乎同時(shí)操作業(yè)務(wù)語(yǔ)義都不會(huì)變成交換兩次。SQLite 上的測(cè)試可以證明順序重試和數(shù)據(jù)庫(kù)唯一事實(shí)。真正的 PostgreSQL 雙連接并發(fā)仍應(yīng)在具備服務(wù)環(huán)境時(shí)補(bǔ)跑當(dāng)前機(jī)器沒(méi)有 PostgreSQL不能把 SQLite 冒充并發(fā)證據(jù)。不要把秘密交給模板再隱藏一個(gè)常見(jiàn)但危險(xiǎn)的做法是把車主和乘客的微信號(hào)都放入模板上下文然后用頁(yè)面條件決定顯示哪一個(gè)。隱藏的 DOM、調(diào)試輸出、錯(cuò)誤頁(yè)或以后新增的前端序列化都可能把不該出現(xiàn)的值送到瀏覽器。鄰行的結(jié)果服務(wù)先判斷當(dāng)前查看者身份查看者是車主 → 只解密乘客快照 查看者是乘客 → 只解密車主快照 其他人 → 拒絕模板上下文只有一個(gè)other_wechat_id。HTTP 測(cè)試分別斷言確認(rèn)頁(yè)源碼不包含雙方任一微信號(hào)交換后頁(yè)面包含對(duì)方微信號(hào)頁(yè)面不包含自己的微信號(hào)頁(yè)面不包含登錄賬號(hào)非參與者打開確認(rèn)和結(jié)果地址都得到 404。安全邊界應(yīng)當(dāng)盡量發(fā)生在數(shù)據(jù)進(jìn)入表現(xiàn)層之前而不是依賴 CSS 或前端腳本隱藏。復(fù)制按鈕必須允許失敗微信內(nèi)置瀏覽器、系統(tǒng)權(quán)限和非安全上下文都可能讓 Clipboard API 不可用。一鍵復(fù)制只能是增強(qiáng)能力不能成為唯一出口。結(jié)果頁(yè)把對(duì)方微信號(hào)放在只讀但可以聚焦、長(zhǎng)按和選擇的輸入框中。復(fù)制成功顯示“已復(fù)制”失敗時(shí)自動(dòng)選中文本并提示復(fù)制失敗請(qǐng)長(zhǎng)按或手動(dòng)選擇上面的微信號(hào)。頁(yè)面不會(huì)嘗試打開未知的微信協(xié)議也不會(huì)自動(dòng)跳走。下一步文案只告訴用戶返回微信添加對(duì)方并確認(rèn)具體上下車位置。自動(dòng)瀏覽器可以證明結(jié)構(gòu)和降級(jí)路徑已經(jīng)存在但 iOS 和 Android 微信中的長(zhǎng)按選擇、復(fù)制權(quán)限與返回操作仍必須由 Gate B 真機(jī)驗(yàn)收不能由桌面 Chromium 最終代替。雙用戶瀏覽器閉環(huán)K3 已有兩個(gè)隔離瀏覽器上下文。K4 在同一旅程后繼續(xù)執(zhí)行乘客打開候選 → 點(diǎn)擊“想和對(duì)方聯(lián)系” → 確認(rèn)頁(yè)看不到任何微信號(hào) → 閱讀互相披露說(shuō)明并確認(rèn) → 乘客只看到車主微信號(hào) → 車主刷新候選并打開結(jié)果 → 車主只看到乘客微信號(hào) → 雙方都看到返回微信的下一步兩個(gè)上下文有獨(dú)立 Cookie 和會(huì)話。測(cè)試不是把同一個(gè)客戶端來(lái)回切換用戶因此更接近雙方異步操作的真實(shí)授權(quán)路徑。本節(jié)點(diǎn)結(jié)果K4 收口時(shí)85 個(gè)非瀏覽器測(cè)試通過(guò)分支覆蓋率 90.79%Ruff、格式、mypy strict、遷移和 Django 檢查通過(guò)4 條 Chromium 旅程通過(guò)其中雙用戶旅程已走到互相披露結(jié)果重復(fù)交換、資料修改、非參與者、受限賬戶、失效狀態(tài)和時(shí)間邊界都有自動(dòng)證據(jù)確認(rèn)前不含微信號(hào)結(jié)果頁(yè)每人只收到對(duì)方微信號(hào)外部提醒實(shí)際發(fā)送、PostgreSQL 并發(fā)、WebKit 和微信真機(jī)仍未宣稱通過(guò)結(jié)果只形成本地提交不推送。驗(yàn)收矩陣中AC-21 至 AC-23 可以在當(dāng)前頁(yè)面范圍內(nèi)自動(dòng)通過(guò)AC-41 也完成交換前后身份展示閉環(huán)。AC-24 仍是部分通過(guò)交換事實(shí)與雙方獨(dú)立投遞記錄已經(jīng)形成第三方一方失敗不影響另一方要等 K7 故障發(fā)送測(cè)試。寫在最后敏感數(shù)據(jù)功能的關(guān)鍵不是“加密了”三個(gè)字而是明確誰(shuí)在什么時(shí)刻授權(quán)、事務(wù)中重新檢查什么、保存哪個(gè)時(shí)間點(diǎn)的值以及秘密最早在哪一層被裁剪。當(dāng)結(jié)果模板從一開始就只拿到對(duì)方微信號(hào)后續(xù)頁(yè)面改版也更難意外泄露另一份數(shù)據(jù)。好的隱私設(shè)計(jì)往往同時(shí)讓代碼職責(zé)更清晰。下一篇進(jìn)入 K5雙方回微信溝通后怎樣分別反饋結(jié)果為什么單方確認(rèn)不能占座以及如何用數(shù)據(jù)庫(kù)鎖保證最后一個(gè)座位不會(huì)同時(shí)分給兩位乘客。關(guān)鍵代碼與操作下面的簡(jiǎn)化測(cè)試從雙方視角讀取同一次交換證明每個(gè)人只能得到對(duì)方的微信號(hào)deftest_one_actor_discloses_both_contacts(candidate,driver,passenger):exchangeexchange_contact(candidate_idcandidate.pk,actor_iddriver.pk,nowtimezone.now(),)assertget_disclosed_contact(exchangeexchange,viewerdriver)passenger-wechatassertget_disclosed_contact(exchangeexchange,viewerpassenger)driver-wechatassertBusinessEvent.objects.filter(event_typecontact.exchanged).count()1驗(yàn)證命令make bugfix TESTtests/matching/test_contact_exchange.py::test_one_actor_discloses_both_contacts_and_creates_one_event示例只表達(dá)權(quán)限方向真實(shí)測(cè)試還會(huì)覆蓋重復(fù)點(diǎn)擊、資料修改、外部成員和過(guò)期候選。本篇驗(yàn)證摘要雙方必須在同一個(gè)有效候選中明確同意互相披露系統(tǒng)才創(chuàng)建交換交換保存當(dāng)時(shí)雙方微信號(hào)的加密快照之后修改資料不會(huì)悄悄改寫歷史結(jié)果唯一約束和冪等服務(wù)保證重復(fù)點(diǎn)擊不會(huì)創(chuàng)建第二次交換查詢服務(wù)只向每位參與者返回對(duì)方的聯(lián)系方式頁(yè)面拿不到自己的解密值移動(dòng)瀏覽器已驗(yàn)證雙方確認(rèn)、查看和復(fù)制流程提醒碼始終不會(huì)進(jìn)入頁(yè)面。附錄相關(guān)工具與倉(cāng)庫(kù)gstack倉(cāng)庫(kù)garrytan/gstack地址https://github.com/garrytan/gstackdev-harness倉(cāng)庫(kù)Dev-Wiki/dev-harness地址https://github.com/Dev-Wiki/dev-harnessUI UX Pro Max Skill倉(cāng)庫(kù)nextlevelbuilder/ui-ux-pro-max-skill地址https://github.com/nextlevelbuilder/ui-ux-pro-max-skill