接口限流
限流维度
- 分钟、小时
告警:
接口唯一标识
流程图
A[三方应用发起请求]
B[认证中间件会处理请求并验证用户身份]
C[在控制器或方法执行之前执行]
C1[记录埋点信息]
C2[本地缓存,是否已经超出限制]
C3[获取和apiRoute名称一致的限流配置]
C4[检查是否被阻止]
C5[超出80%调用预警]
C6[本地缓存,校验是否已经超出限制]
F[]数据表
- 接口路由 (路由名称、路由唯一标识)
- 接口用户(用户名称、访问token、负责人)
- 接口用户路由(用户ID、路由ID、分钟限制、小时限制)
附录
Mysql表
sql
CREATE TABLE `sys_api_route` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`route_name` varchar(50) NOT NULL DEFAULT '' COMMENT '路由名称',
`route_key` varchar(50) NOT NULL DEFAULT '' COMMENT '路由标识',
`remark` varchar(50) NOT NULL DEFAULT '' COMMENT '备注',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0 无效, 1 有效',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除: 0 否, 1 是',
`create_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人标识',
`create_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人姓名',
`create_time` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '创建时间',
`update_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人标识',
`update_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人姓名',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API路由';
CREATE TABLE `sys_api_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '中文名称',
`access_token` varchar(50) NOT NULL DEFAULT '' COMMENT '访问token',
`owner` varchar(50) NOT NULL DEFAULT '' COMMENT '持有人',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0 无效, 1 有效',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除: 0 否, 1 是',
`create_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人标识',
`create_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人姓名',
`create_time` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '创建时间',
`update_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人标识',
`update_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人姓名',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API用户';
CREATE TABLE `sys_api_user_route` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
`route_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '路由ID',
`minute_limit` int(11) NOT NULL DEFAULT '0' COMMENT '分钟限制;-1表示不限制调用量',
`hour_limit` int(11) NOT NULL DEFAULT '0' COMMENT '小时限制;-1表示不限制调用量',
`day_limit` int(11) NOT NULL DEFAULT '0' COMMENT '天限制;-1表示不限制调用量',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态: 0 无效, 1 有效',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除: 0 否, 1 是',
`create_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人标识',
`create_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '创建人姓名',
`create_time` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '创建时间',
`update_user_id` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人标识',
`update_user_name` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人姓名',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API用户关联路由';.Net代码实现
csharp
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>csharp
public class TokenFilterAttribute : Attribute, IActionFilter
{
/// <summary>
/// action执行后补充操作
/// </summary>
public void OnActionExecuted(ActionExecutedContext context)
{
}
/// <summary>
/// action执行前过滤操作
/// </summary>
public void OnActionExecuting(ActionExecutingContext context)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
if (context == null) return;
var token = context.HttpContext.Request.Headers["token"].ToString() ?? "";
var controllerName = context.RouteData?.Values["controller"]?.ToString()?.ToLower() ?? "";
var actionName = context.RouteData?.Values["action"]?.ToString()?.ToLower() ?? "";
SkynetLogger.Info($"请求的token:{token},访问地址:{controllerName}/{actionName}");
var tokenAuthService = context.HttpContext.RequestServices.GetService<TokenAuthService>();
if (tokenAuthService == null) return;
var authResult = tokenAuthService.Auth(token, controllerName, actionName);
stopwatch.Stop();
SkynetLogger.Info($"token:{token} 验证权限耗时:{stopwatch.ElapsedMilliseconds}ms");
if (!authResult.Result)
{
context.Result = new BadRequestObjectResult(new
{
Success = false,
Msg = authResult.Message,
Code = WeChatErrorType.APIException.ToInt()
});
return;
}
}
}csharp
public class TokenAuthService
{
private readonly IRedisClient _redisClient;
private readonly TokenLimitBll _tokenLimitBll;
private readonly ISysApiPermissionService _sysApiPermissionService;
public TokenAuthService(TokenLimitBll tokenLimitBll, IRedisClientFactory redisClientFactory,
ISysApiPermissionService sysApiPermissionService)
{
_tokenLimitBll = tokenLimitBll;
_sysApiPermissionService = sysApiPermissionService;
_redisClient = redisClientFactory.GetClient("techplatform");
}
public ResultModel Auth(string token, string controller, string action)
{
var nowTime = DateTime.Now;
if (string.IsNullOrEmpty(token))
{
return ResultModel.OnFailure($"请求该接口需要在header中携带token,访问地址:{controller}/{action}");
}
var hourCacheKey = $"{token}-{controller}-{action}-{nowTime:ddHH}";
var minuteCacheKey = $"{token}-{controller}-{action}-{nowTime:ddHHmm}";
// 本地缓存,校验是否已经超出限制
if (_tokenLimitBll.CheckHourKey(hourCacheKey) || _tokenLimitBll.CheckMinuteKey(minuteCacheKey))
{
SkynetLogger.Warn($"TOKEN:{token},请求次数超限制(缓存判断)");
return ResultModel.OnFailure($"TOKEN:{token},请求{controller}/{action} 次数超限制");
}
// 获取Token用户的接口调用阈值
var accessLimit = GetTokenLimit(token, controller, action, nowTime, 0);
if (accessLimit.minuteLimit == 0 || accessLimit.hourLimit == 0)
{
SkynetLogger.Warn($"TOKEN:{token},没有接口请求次数配置");
return ResultModel.OnFailure($"TOKEN:{token},请求{controller}/{action} 没有配置接口请求次数");
}
var minuteCount = _redisClient.String.Incr(minuteCacheKey);
// 增加过期时间
if (minuteCount == 1)
{
for (int i = 0; i < 10; i++)
{
if (_redisClient.Key.Expire(minuteCacheKey, 60 * 60)) break;
}
}
if (accessLimit.minuteLimit > 0 && minuteCount > accessLimit.minuteLimit) // 每分钟限制调用次数
{
_tokenLimitBll.AddMinuteKey(minuteCacheKey, nowTime);
SkynetLogger.Warn($"TOKEN:{token},请求次数超限制,限制次数{accessLimit.minuteLimit}次/min");
return ResultModel.OnFailure($"请求次数超限制,限制次数{accessLimit.minuteLimit}次/min");
}
// 调用次数增加,并获取添加后的次数数据
var hourCount = _redisClient.String.Incr(hourCacheKey);
// 增加过期时间
if (hourCount == 1)
{
for (int i = 0; i < 10; i++)
{
if (_redisClient.Key.Expire(hourCacheKey, 60 * 60 * 3)) break;
}
}
if (accessLimit.hourLimit > 0 && hourCount > accessLimit.hourLimit) // 每小时限制调用次数
{
_tokenLimitBll.AddHourKey(hourCacheKey, nowTime);
SkynetLogger.Warn($"TOKEN:{token},请求次数超限制,限制次数{accessLimit.hourLimit}次/h");
return ResultModel.OnFailure($"请求次数超限制,限制次数{accessLimit.hourLimit}次/h");
}
SkynetLogger.Info(
$"TOKEN:{token},使用次数分钟:{minuteCount}/{accessLimit.minuteLimit},小时:{hourCount}/{accessLimit.hourLimit}");
return ResultModel.OnSuccess();
}
private (int minuteLimit, int hourLimit) GetTokenLimit(string token, string controller, string action,
DateTime nowTime, int loop)
{
if (loop > 2)
{
return (-1, -1);
}
var userInfoCacheKey = $"{token}-{controller}-{action}-{nowTime:ddHH}-user";
var keyMutex = $"{userInfoCacheKey}_mutex";
var cacheData = _redisClient.String.Get(userInfoCacheKey);
var tempArray = cacheData?.Split('-');
if (tempArray == null || tempArray.Length < 2)
{
if (_redisClient.String.SetNx(keyMutex, "1"))
{
(int minuteLimit, int hourLimit) result = (0, 0);
var apiPermission = _sysApiPermissionService.GetModel(token, controller, action);
if (apiPermission == null)
{
SkynetLogger.Warn($"Token权限校验失败,token:{token},访问地址:{controller}/{action}");
return result;
}
result.minuteLimit = apiPermission.MinuteLimit;
result.hourLimit = apiPermission.HourLimit;
// 1小时缓存
_redisClient.String.Set(userInfoCacheKey, $"{result.minuteLimit}-{result.hourLimit}", EX: 60 * 60);
_redisClient.Key.Del(keyMutex);
return result;
}
else
{
_redisClient.Key.Del(keyMutex);
}
Thread.Sleep(50);
//重试
return GetTokenLimit(token, controller, action, nowTime, loop + 1);
}
return (tempArray[0].ToInt(), tempArray[1].ToInt());
}
}