PHP+MYSQL 程式被攻擊,求應對方法

來源:互聯網
上載者:User
關鍵字 php mysql
類似購物的程式,程式上的流程是這樣的:

1、使用者發起請求,下單
2、檢查各種參數是否齊全、有效
3、檢查使用者餘額是否足夠
4、寫入訂單表
5、寫入使用者表,將使用者餘額減少
6、寫入記錄表,記錄使用者下單買的啥,以及花了多少錢

今天發現一個神奇的使用者,他在1秒鐘之內下了20單!至於是不是1秒鐘無從查起,因為資料庫只精確到秒。
更奇怪的是:

1、明明沒有足夠的餘額,卻繼續進入了後續的步驟
2、寫入訂單表成功、寫入記錄表成功,但是就是沒有扣餘額

我想來想去也沒弄明白這是怎麼回事兒,各位遇到過嗎?有何應對方法?

** 其他使用者是完全正常的,只有這個瞬間下很多單的不正常。

    public function orderCreate(Request $request, Response $response) {                if(!$user = session('wechat.oauth_user')){            return response()->json([                'error' => '身份驗證失敗,請重新打開頁面再試'            ]);        }        if(is_null($request->input('object', NULL))        || is_null($request->input('stake', NULL))        || is_null($request->input('time', NULL))        || is_null($request->input('direction', NULL))){            return response()->json([                'error' => '參數提交不全,請重新打開頁面再試'            ]);        }        if($request->input('stake') != 20        && $request->input('stake') != 50        && $request->input('stake') != 100        && $request->input('stake') != 200        && $request->input('stake') != 500        && $request->input('stake') != 1000        && $request->input('stake') != 2000        && $request->input('stake') != 3000){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }        if($request->input('time') != 60        && $request->input('time') != 120        && $request->input('time') != 180        && $request->input('time') != 240        && $request->input('time') != 300){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }        if($request->input('direction') != 1        && $request->input('direction') != 0){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }        if(!$object = Object::find($request->input('object'))){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }                $object_latestPrice = Price::where('id_object', $object->id)->orderBy('created_at', 'desc')->first();        if((strtotime($object_latestPrice->body_price_time) + 300) < time()){            return response()->json([                'error' => '休市期間無法進行交易'            ]);        }                if(!$user = User::where('id_wechat', $user->id)->first()){            return response()->json([                'error' => '身份驗證失敗,請重新打開頁面再試'            ]);        }                if(floatval($user->body_balance) < $request->input('stake')){            return response()->json([                'error' => '帳戶可用餘額不足,請先儲值後再交易'            ]);        }        if($user->is_disabled > 0){            return response()->json([                'error' => '帳戶已被封鎖,無法進行交易'            ]);        }        $order = new Order;        $order->id_user = $user->id;        $order->id_object = $object->id;        $order->body_price_buying = $object_latestPrice->body_price;        $order->body_stake = $request->input('stake');        $order->body_bonus = $object->body_profit * $request->input('stake');        $order->body_direction = $request->input('direction');        $order->body_time = $request->input('time');        $order->save();        $user->body_balance = floatval($user->body_balance) - floatval($order->body_stake);        $user->body_transactions = floatval($user->body_transactions) + floatval($order->body_stake);        $user->save();        $record = new Record;        $record->id_user = $user->id;        $record->id_order = $order->id;        $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌';        $record->body_direction = 0;        $record->body_stake = $order->body_stake;        $record->save();        return response()->json([            'result' => $order->toArray()        ]);    }

UPDATE:
現在在一大堆的條件判斷之後,希望改成事物來處理這件事,但是 Laravel 的事務這麼寫正確嗎?或者說我這麼寫的話能夠起到我想要的作用嗎?有點懵 - -

        DB::beginTransaction();        $user->body_balance = floatval($user->body_balance) - $request->input('stake');        $user->body_transactions = floatval($user->body_transactions) + $request->input('stake');        $user->save();        if($user->body_balance < 0) {            DB::rollback();        } else {            $order = new Order;            $order->id_user = $user->id;            $order->id_object = $object->id;            $order->body_price_buying = $object_latestPrice->body_price;            $order->body_stake = $request->input('stake');            $order->body_bonus = $object->body_profit * $request->input('stake');            $order->body_direction = $request->input('direction');            $order->body_time = $request->input('time');            $order->save();            $record = new Record;            $record->id_user = $user->id;            $record->id_order = $order->id;            $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌';            $record->body_direction = 0;            $record->body_stake = $order->body_stake;            $record->save();            $this->computeNetwork($user, $order);            if($order->body_time == 60) $this->computePrice($user, $order, $object);                    }        DB::commit();

回複內容:

類似購物的程式,程式上的流程是這樣的:

1、使用者發起請求,下單
2、檢查各種參數是否齊全、有效
3、檢查使用者餘額是否足夠
4、寫入訂單表
5、寫入使用者表,將使用者餘額減少
6、寫入記錄表,記錄使用者下單買的啥,以及花了多少錢

今天發現一個神奇的使用者,他在1秒鐘之內下了20單!至於是不是1秒鐘無從查起,因為資料庫只精確到秒。
更奇怪的是:

1、明明沒有足夠的餘額,卻繼續進入了後續的步驟
2、寫入訂單表成功、寫入記錄表成功,但是就是沒有扣餘額

我想來想去也沒弄明白這是怎麼回事兒,各位遇到過嗎?有何應對方法?

** 其他使用者是完全正常的,只有這個瞬間下很多單的不正常。

    public function orderCreate(Request $request, Response $response) {                if(!$user = session('wechat.oauth_user')){            return response()->json([                'error' => '身份驗證失敗,請重新打開頁面再試'            ]);        }        if(is_null($request->input('object', NULL))        || is_null($request->input('stake', NULL))        || is_null($request->input('time', NULL))        || is_null($request->input('direction', NULL))){            return response()->json([                'error' => '參數提交不全,請重新打開頁面再試'            ]);        }        if($request->input('stake') != 20        && $request->input('stake') != 50        && $request->input('stake') != 100        && $request->input('stake') != 200        && $request->input('stake') != 500        && $request->input('stake') != 1000        && $request->input('stake') != 2000        && $request->input('stake') != 3000){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }        if($request->input('time') != 60        && $request->input('time') != 120        && $request->input('time') != 180        && $request->input('time') != 240        && $request->input('time') != 300){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }        if($request->input('direction') != 1        && $request->input('direction') != 0){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }        if(!$object = Object::find($request->input('object'))){            return response()->json([                'error' => '參數提交錯誤,請重新打開頁面再試'            ]);        }                $object_latestPrice = Price::where('id_object', $object->id)->orderBy('created_at', 'desc')->first();        if((strtotime($object_latestPrice->body_price_time) + 300) < time()){            return response()->json([                'error' => '休市期間無法進行交易'            ]);        }                if(!$user = User::where('id_wechat', $user->id)->first()){            return response()->json([                'error' => '身份驗證失敗,請重新打開頁面再試'            ]);        }                if(floatval($user->body_balance) < $request->input('stake')){            return response()->json([                'error' => '帳戶可用餘額不足,請先儲值後再交易'            ]);        }        if($user->is_disabled > 0){            return response()->json([                'error' => '帳戶已被封鎖,無法進行交易'            ]);        }        $order = new Order;        $order->id_user = $user->id;        $order->id_object = $object->id;        $order->body_price_buying = $object_latestPrice->body_price;        $order->body_stake = $request->input('stake');        $order->body_bonus = $object->body_profit * $request->input('stake');        $order->body_direction = $request->input('direction');        $order->body_time = $request->input('time');        $order->save();        $user->body_balance = floatval($user->body_balance) - floatval($order->body_stake);        $user->body_transactions = floatval($user->body_transactions) + floatval($order->body_stake);        $user->save();        $record = new Record;        $record->id_user = $user->id;        $record->id_order = $order->id;        $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌';        $record->body_direction = 0;        $record->body_stake = $order->body_stake;        $record->save();        return response()->json([            'result' => $order->toArray()        ]);    }

UPDATE:
現在在一大堆的條件判斷之後,希望改成事物來處理這件事,但是 Laravel 的事務這麼寫正確嗎?或者說我這麼寫的話能夠起到我想要的作用嗎?有點懵 - -

        DB::beginTransaction();        $user->body_balance = floatval($user->body_balance) - $request->input('stake');        $user->body_transactions = floatval($user->body_transactions) + $request->input('stake');        $user->save();        if($user->body_balance < 0) {            DB::rollback();        } else {            $order = new Order;            $order->id_user = $user->id;            $order->id_object = $object->id;            $order->body_price_buying = $object_latestPrice->body_price;            $order->body_stake = $request->input('stake');            $order->body_bonus = $object->body_profit * $request->input('stake');            $order->body_direction = $request->input('direction');            $order->body_time = $request->input('time');            $order->save();            $record = new Record;            $record->id_user = $user->id;            $record->id_order = $order->id;            $record->body_name = $request->input('direction') == 1? '買入看漲' : '買入看跌';            $record->body_direction = 0;            $record->body_stake = $order->body_stake;            $record->save();            $this->computeNetwork($user, $order);            if($order->body_time == 60) $this->computePrice($user, $order, $object);                    }        DB::commit();

沒見過涉及金錢交易不開事務就執行的,請用事務解決此類問題。

更新一下:
有人回答先扣錢就行,答案是否定的,在MySQL中不用事務一定完成不了這個操作。
舉個不用事務先扣錢的例子,

  1. 收到請求A,進行餘額查詢,餘額足夠,

  2. 這時候請求B闖入,也進行了餘額查詢,餘額足夠,

  3. 請求A開始更新喻額,然後進行了其他動作,

  4. 請求B也開始更新喻額,進行其他動作。

如此一樣解決不了並發的問題。

事務加一,而且優先判斷金額等重要條件

沒看懂你代碼具體的實現,但是我猜你可能取到的髒資料。
你可以試試如下方案
trans begin
sql:update xxx set 帳戶餘額 = 帳戶餘額 - 消費金額(計費操作)
sql:select 帳戶餘額 from xxx (擷取完成計費後的餘額)
if(帳戶餘額 < 0) rollback
else commit

20單並發,每單在判斷餘額的時候應該都是足夠的,然後之後寫表操作,第一次扣餘額成功,接下來19單扣餘額失敗,但是你的代碼中沒有任何處理,就導致了建立了訂單,但是沒有扣餘額的情況
解決方案樓上都說了,用事務提交,一開始先update餘額欄位,然後再做餘下操作,這樣能保證並發的時候在餘額這裡有一個鎖,其它請求都要等到這個請求被commit或者rollback以後才能執行

首先,樓主最後貼的代碼還是有問題的。

總的來說,這個,需要用到事務和鎖,同時避免一些坑。

第一,檢查mysql的事務層級,我們要在 可重複讀的 層級下。
第二,確認線上資料庫結構,確保讀寫都使用一個資料庫連接(尤其是讀寫分離的情況下)。
第三,首先開啟事務。
第四,開事務後,第一條就是用select for update查詢出使用者的餘額(避免一致性非鎖定讀)。
第五,進行資金判斷和扣減,注意php計算的話,使用bcmath來處理。
第六,所有資金操作都應該有日誌記錄,所有的資料異常或者代碼錯誤都應該記錄日誌。
第七,業務操作後提交事務。

把賬戶餘額計費放在前面,目前的邏輯執行了,但在計費的過程出錯了而已,如金額欄位不能小於0。放在前面計費的話,可以判斷是否執行成功,否則提示錯誤!

問題出在3,4,5這裡,這種邏輯在出現類似並發的集中請求的時候就會出問題。正確邏輯是
3-update table set 餘額 = 餘額 - 金額 where user_id = ? & 餘額 > 金額,檢查本次修改所影響的行數,如果為0表示根本沒更新,就是餘額已經不足了
4-寫訂單
就沒有5了

原始邏輯的問題就是3查詢的時候餘額確實是足夠的,但是等到第5步扣除餘額的時候就不一定了。

嗯,補充一下,有明說的沒錯,就算修改了邏輯涉及重要資料的地方也最好使用事務。

同上,涉及金錢或者類似的,一定要開啟事務。

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.