問題情況
公司項目在9.26號的時候被攻擊了, 正常情況下一天的提現金額是一百多,但是26號一天提現彙總金額到2700多,是平時的幾十倍, 明顯是受到惡意網絡攻擊了:
問題1: 被攻擊那幾天每日產生的星能異常的高
正常一天系統就產生1萬多的星能, 26號一天產生了416萬多的星能, 是平日的幾百倍
從上圖可以看出26號,27號產生了大量異常的星能
問題2: 後來查詢發現一個ip一天簽到了6979次
除了ip一樣外,還能見到同一個用户在一天多次簽到的情況, 簽到記錄寫入時間也幾乎一致, 如果正常通過程序簽到的話, 一個用户一天只能簽到一次不會存在這種多次簽到的情況
問題3: 一個用户在同一秒內能獲取多次視頻獎勵
用户看視頻獎勵每個視頻要看至少30s, 並且一天只有看的前10個視頻有獎勵,後面看的視頻正常就沒有獎勵了, 但從下圖可以看出一個用户在同一時刻有多條看視頻獎勵記錄
從上面數據庫中的數據推測系統應該是被人短時間內發動了大量惡意攻擊,導致MySQL數據庫寫入了很多不應該寫入的數據
處理辦法
1. 給接口加訪問流速控制
- 在基礎api接口的構造方法(__construct)中對ip訪問接口做總的限制
public function __construct(Request $request = null)
{
... ...
//限制ip的訪問頻率
RateLimiterUtil::ipCheck(300, 60);
// 接口簽名驗證
$this->checkSign();
... ...
}
-
針對特定接口加ip流速限制
public function withdraw() { //提現頻率限制,1分鐘個ip只能提2次 RateLimiterUtil::ipCheck(2, 60, 'withdraw'); ...... // 提現邏輯 }
2. 對接口訪問增加簽名校驗
接口訪問的時候統一多加一個time秒級時間戳跟一個sign簽名字段, 後端接收到請求後根據time時間戳先進行sign簽名驗證
//接口簽名驗證
protected function checkSign()
{
$platform = $this->request->header('platform', '');
$token = $this->request->header('token', '');
$env = Env::get('app.env');
if ( in_array($env, ['dev', 'production'])) {
$time = $this->request->param('time');//傳遞時間戳
if (!$time) {
$this->error('時間戳不能為空', [], 0);
}
$sign = $this->request->param('sign');
if (!$sign) {
$this->error('訪問簽名不能為空', [], 0);
}
$signServer = md5('跟前台約定好的字符串' . $time);
if ($signServer != $sign) {
$this->error('訪問簽名錯誤', [], 0);
}
}
}
通過增加上面兩種防護手段後, 又觀察了幾天沒有再出現MySQL數據庫中短時間寫入大量數據的情況
附錄:
接口訪問流速控制工具類(RateLimiterUtil)
<?php
namespace fast;
use app\common\service\RedisService;
use think\Env;
use think\Exception;
class RateLimiterUtil
{
/**
* 檢查是否超出限流
* @param string $key 限流標識(IP / 用户ID / 接口名)
* @param int $limit 時間窗口內最大請求次數
* @param int $expire 時間窗口(秒)
* @return bool true=超出限制, false=未超出
*/
public static function isOverLimit($key, $limit = 10, $expire = 60)
{
//獲取redis實例
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$env = Env::get('app.env');
$cacheKey = 'ratelimit:' . $key;
if ($env != 'production') {
$cacheKey = $env . 'ratelimit:' . $key;
}
$count = $redis->get($cacheKey);
if ($count >= $limit) {
return true;
} else {// 原子自增
$count = $redis->incr($cacheKey);
// 第一次設置過期時間
if ($count == 1) {
$redis->expire($cacheKey, $expire);
}
return false;
}
}
/**
* 限流檢查,如果超出則直接拋出異常或返回 JSON
* @param string $key
* @param int $limit
* @param int $expire
* @param bool $returnJson 是否直接返回 JSON
* @throws Exception
*/
public static function check($key, $limit = 10, $expire = 60, $returnJson = true)
{
if (self::isOverLimit($key, $limit, $expire)) {
$msg = "請求過於頻繁,請稍後再試";
if ($returnJson) {
json(['code' => 0, 'msg' => $msg])->send();
exit;
}
throw new Exception($msg, 0);
}
}
/**
* 默認按 IP 限流
* @param int $limit 次數
* @param int $expire 秒
* @param bool $returnJson
* @throws Exception
*/
public static function ipCheck($limit = 10, $expire = 60, $diffKey = '', $returnJson = true)
{
$ip = request()->ip();
$key = 'ip:' . $ip;
if ($diffKey) {
$key .= ':' . $diffKey;
}
self::check($key, $limit, $expire, $returnJson);
}
/**
* 按 IP+用户ID 限流
* @param int $uid 用户ID
* @param int $limit 次數
* @param int $expire 秒
* @param bool $returnJson
* @throws Exception
*/
public static function ipUidCheck($uid = 0, $limit = 10, $expire = 60, $returnJson = true)
{
$ip = request()->ip();
$key = 'ip:' . $ip . ':uid:' . $uid;
self::check($key, $limit, $expire, $returnJson);
}
}