給一個電商開發的系統排查,發現漏洞很多。很多經驗不夠的開發者很容易忽視的邏輯錯誤陷阱。在給一個項目做二次開發時候,檢測到的相關經典案例。這里整理支付和產品相關的邏輯,方便后續查看。,這里進行一些簡單的邏輯漏洞梳理與修復。
這里基本可以看出經驗少的程序員和有經驗的程序員的差距,這種差距與算法能力無關,只與類似的系統設計經驗有關,所以如果不是算法崗,盲目追逐算法,并不是最佳解。如果僅僅是以完成功能和對外展示流程走通為核心目標,代碼邏輯質量看不出來,但是到真正運行的時候,有經驗和無經驗寫的代碼差距巨大。
1.原來邏輯與想法::
根據優惠券id,讀取根據id狀態下未使用的優惠券,進入下一步邏輯。
查詢條件 id = $coupon_id + status = 0 讀取一個未使用的優惠券。因為優惠券id是唯一的,再讀取未使用的優惠券,倆個狀態都符合,肯定表示該優惠券是可以使用的。
診斷風險:,讀取優惠券之前,必須加上身份限定user_id 也就是需要三方條件成立,這張查詢條件才是有效的。雖然大部分正常情況下,倆者查詢結果一致,但是如果有惡意攻擊者,從A賬戶,獲取到了B賬戶的優惠券,這樣就可以根據B的優惠券id 放入A賬戶使用。很顯然,id = $coupon_id? and? user_id? and status =0? 查詢安全性上明顯高一截,當然另外的寫法是全部讀取出來,然后用業務代碼判斷,這會增加業務邏輯代碼復雜度。
2.原始寫這個系統人代碼邏輯:
讀取一個product_id 然后進行一次檢查,又讀取另外一個package_id 又進行一次檢查,如果沒有檢測到相關信息,報錯。每次讀取一個參數,判斷一次,不行,退出報錯,沒有問題。方便理解。
代碼風險: 不是統一先驗證必填參數的空參,就直接先讀取數據庫,如果后面又必填的參數是空參,會導致前面的查詢都是浪費。成熟的寫法,都是先對所有的必填參數進行驗證,出現必填的空參,直接報錯,讓客戶端傳遞完整參數。可以節約性能。
3.原始寫法:API接口直接完成全部邏輯,就是一個接口邏輯從頭寫到尾,除了使用了內部Model封裝的CRUD小方法,其他邏輯全部堆積到一個主方法里面,導致下單方法長度超過300行。
診斷風險: 大方法里面包含 優惠券使用邏輯,拼單邏輯,產品判斷邏輯,下單的判斷邏輯,產品的規則邏輯,要測試里面任意邏輯,都只能跑全部的方法,導致修改調試邏輯,只能跑整個方法,而整個方法,又因為參數過多,導致調試的效率大幅度降低。
改進方法: 將優惠券邏輯 結算邏輯? 產品判斷邏輯,規則邏輯 ,權限判斷邏輯,全部拆開到model里面或者子方法里面,調試的時候,只需要填入我們要調試的基礎邏輯即可,不需要跑其他已經正確的邏輯。
4.API 沒有任何防刷,面對并發沒有任何防御措施。一旦出現需要連續快速下單的情況,會造成用戶可以使用倆次優惠券。查詢的接口對防刷沒有太高要求,但是對于數據有改動,賬戶金額有變動/優惠券有數量限制的 ,沒有并發,基本就會被灰產使用。寫該方法的人,應該沒有被灰產攻擊的經驗,沒有代碼上有這種防御接口。
改進方法:其實主要還是防止用戶網絡不暢,連續點了倆次的情況出現。新增一個訂單鎖,確定該訂單下單邏輯全部走完之后,才能下第二單。也就是增加一個流程鎖,每個用戶在一個時間只能下一單,防止用戶的優惠和余額關鍵信息被同時改動,導致數據失效。一般普通流程鎖,使用redis的set即可完成,如果是存在更高級的并發的,需要使用到setnx 。流程鎖需要注意的問題是,在執行完成全部邏輯后,需要進行對應解鎖。所以最好的執行方法就是,controllerl里面只操作參數判斷+鎖流程+主流程,而流程放到logic層或者model層。
5.當前的寫法:沒有產品和店鋪的salesnum字段,需要的時候,直接采用 count的方法讀取數據庫,簡單方便,實時讀取數據庫。很多新手設計表的時候,特別喜歡使用count來替代統計功能,方便好用,也容易理解。
風險警示:一旦遇到諸如需要后臺統計每個產品的銷量還有,每個店鋪的銷售量的時候,只能循環使用count,對系統的性能是噩夢,特別是當訂單數量達到一百萬的時候,會發現刷下后臺,就會導致整個系統卡死。
改進方法:對全部需要統計的東西,盡量額外增加一個count_num字段,在進行改動的時候對該字段進行+1? 或者-1? 如果對精確度要求不高,就不使用事物。否則就需要事物來控制準確度。