为什么要开细节类

因为有很多东西,很细微,但又很实用。就会放在细节类。该类的目的是做个记录,既然简单。

限制ip接口案例

场景

需要针对相同ip用户每分访问接口次数的做限制,防止用户恶意刷。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 接口 ip 限制
* @param $miniutes 分钟数
* @param $times 次数
* @param $api 接口唯一标识
* @return bool
*/
public function apiIpLimit($miniutes, $times, $api)
{
$ip = Request::getClientIP(); //获取用户ip

if ($ip) {
$key = $api . "_" . $ip;

$num = Yii::$app->cookie->get($key);
$num = $num ? $num + 1 : 1; //请求次数加1
if ($num > $times) {
Yii::getLogger()->log($key . " exceed limit", Logger::LEVEL_ERROR);
return false; //超过限制次数直接返回,不重置过期时间
}

Yii::$app->cookie->set($key, $num, $miniutes * 60); //未超过限制次数,更新访问次数,并重置过期时间为一分钟
}

return true;
}

解析

情景

假设每分钟限制8次请求

代码逻辑是否符合需求

上面的代码案例并不是完全依照每分钟限制8次请求逻辑处理,因为有重置过期时间。

假设第6,7,8次请求都间隔了40s。按逻辑,第6次和第8次中间间隔了80s,超过一分钟了,但因为访问接口,未超过限制次数时会重置过期时间为60s的因素,第6次访问和第8次访问被累加在同一个次数累计中。

代码合理性

从合理性上,我觉得是合理的。但逻辑解释应该是相同ip距离上一次访问时间不超过一分钟,计入次数累计,当次数累计超过限制次数将会返回错误,时间1min

当然,如果要按目前逻辑实现也很简单,可以存储值num_day(累计次数+当天日期(最小单位:天)),然后重置的时间设置为当前时间戳-第一次的时间戳

优化点

首先ip这块用户可以通过vpn等手段切换不同ip。

其次,限制次数是设置在cookie用户是可以删除的。

建议:限制用户每天可以请求多少次,记录在redis之类的服务端缓存。

限制用户每天可以请求多少次

检验

1
2
3
4
// 邀请邮件 校验: 每天(utc时区)限制50封
if ($this->getApiRequestLimitByDay(50, 'bizInviteEmail') === false) {
Response::echoResult(ErrCode::$ACCOUNT_LIMIT_ERR);
}

累计次数

1
$this->setApiRequestLimitByDay('bizInviteEmail', $accountId); //发送邮件后更新用户当天发邮件次数

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 获取接口是否超出每日访问次数限制
* @param $times 次数
* @param $api 限制内容唯一标识
* @param string $accountId 用户主账号id
* @return bool
*/
public function getApiRequestLimitByDay($times, $api, $accountId = "")
{
if(empty($accountId)) {
$accountId = \Yii::$app->bizUser->accountId;
}

if ($accountId) {
$day = date('Y_m_d',time());
$key = $api . "_" . $accountId . "_" . $day;

$redis = Factory::getUserRedisClient();
$num = $redis->get($key);
$num = $num ? $num + 1 : 1;

if ($num > $times) {
Yii::getLogger()->log($key . " exceed limit", Logger::LEVEL_ERROR);
return false;
}
}

return true;
}

/**
* 更新(累加)接口每日访问次数
* @param $api 限制内容唯一标识
* @param string $accountId 用户主账号id
*/
public function setApiRequestLimitByDay($api, $accountId = "")
{
if(empty($accountId)) {
$accountId = \Yii::$app->bizUser->accountId;
}

if ($accountId) {
$day = date('Y_m_d',time());
$key = $api . "_" . $accountId . "_" . $day;

$redis = Factory::getUserRedisClient();
$num = $redis->get($key);
$num = $num ? $num + 1 : 1;

try {
$redis->setex($key, 86400, $num); //保存一天后过期
} catch (\Exception $e) {
LogService::logError("set $ownerId $key exception : " . $e->getMessage());
}
}
}