limiter.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on 2022-06-15
  4. ---------
  5. @summary: 限制器
  6. ---------
  7. @author: Dzr
  8. """
  9. import inspect
  10. from fastapi import FastAPI
  11. from slowapi import Limiter, _rate_limit_exceeded_handler
  12. from slowapi.errors import RateLimitExceeded
  13. from slowapi.util import get_remote_address
  14. import db.redisdb as redis
  15. import setting
  16. from services.robot import send_msg
  17. from services.utils import current_date
  18. LIMITER_DATE = "lm_date" # 当前日期
  19. LIMITER_FORECAST_WARNING = "lm_forecast_warning" # 上限预告
  20. LIMITER_EXCEEDED_WARNING = "lm_exceeded_warning" # 超限告警
  21. PENV = setting.PLATFORM_ENVIRONMENT # 当前运行环境
  22. redis_db = redis.RedisDB() # redis实例
  23. def register_limiter(app: FastAPI):
  24. if not redis_db.exists_key(setting.LIMITER_REDIS_KEY):
  25. # 服务首次启动时初始化参数
  26. init_datas = [
  27. (LIMITER_DATE, current_date()),
  28. (LIMITER_FORECAST_WARNING, 1),
  29. (LIMITER_EXCEEDED_WARNING, 1)
  30. ]
  31. redis_db.hset_batch(setting.LIMITER_REDIS_KEY, init_datas)
  32. app.state.limiter = limiter
  33. app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
  34. class PayCaptchaLimiter(Limiter):
  35. def reset_warn(self):
  36. self.logger.debug("告警重置")
  37. # 开启上限预告
  38. redis_db.hset(setting.LIMITER_REDIS_KEY, LIMITER_FORECAST_WARNING, 1)
  39. # 开启超限告警
  40. redis_db.hset(setting.LIMITER_REDIS_KEY, LIMITER_EXCEEDED_WARNING, 1)
  41. def send_pre_warning(self, item, *identifiers):
  42. table = setting.LIMITER_REDIS_KEY
  43. limit_date = redis_db.hget(table, LIMITER_DATE)
  44. curr_date = current_date()
  45. if curr_date != limit_date:
  46. redis_db.hset(table, LIMITER_DATE, curr_date)
  47. self.reset_warn()
  48. self.reset()
  49. self.limiter.hit(item, *identifiers)
  50. usages_count = self._storage.get(item.key_for(*identifiers))
  51. val = '{:.2f}'.format(usages_count / item.amount * 100)
  52. if float(val) > 80 and int(redis_db.hget(table, LIMITER_FORECAST_WARNING)):
  53. tips = "".join([f"[{PENV}]", f"使用次数已超过{val}%"])
  54. send_msg("超级鹰", item.amount, usages_count, tips)
  55. redis_db.hset(table, LIMITER_FORECAST_WARNING, 0)
  56. def send_finished(self, limit):
  57. max_limit = setting.LIMITER_MAX_LIMIT # 最大访问次数
  58. amount = limit.limit.amount
  59. self.logger.debug(f"今日接口调用次数 {amount} 已达上限!")
  60. if int(redis_db.hget(setting.LIMITER_REDIS_KEY, LIMITER_EXCEEDED_WARNING)):
  61. tips = "".join([f"[{PENV}]", "今日接口调用次数已达上限!\n 继续使用请点击"])
  62. send_msg("超级鹰", max_limit, amount, tips, allow_reset=True)
  63. # 关闭超限告警
  64. redis_db.hset(setting.LIMITER_REDIS_KEY, LIMITER_EXCEEDED_WARNING, 0)
  65. def __evaluate_limits(self, request, endpoint: str, limits) -> None:
  66. failed_limit = None
  67. limit_for_header = None
  68. for lim in limits:
  69. limit_scope = lim.scope or endpoint
  70. if lim.is_exempt:
  71. continue
  72. if lim.methods is not None and request.method.lower() not in lim.methods:
  73. continue
  74. if lim.per_method:
  75. limit_scope += ":%s" % request.method
  76. if "request" in inspect.signature(lim.key_func).parameters.keys():
  77. limit_key = lim.key_func(request)
  78. else:
  79. limit_key = lim.key_func()
  80. args = [limit_key, limit_scope]
  81. if all(args):
  82. if self._key_prefix:
  83. args = [self._key_prefix] + args
  84. if not limit_for_header or lim.limit < limit_for_header[0]:
  85. limit_for_header = (lim.limit, args)
  86. self.send_pre_warning(lim.limit, *args) # 发送预警&自动重置
  87. if not self.limiter.hit(lim.limit, *args):
  88. self.logger.warning(
  89. "ratelimit %s (%s) exceeded at endpoint: %s",
  90. lim.limit,
  91. limit_key,
  92. limit_scope,
  93. )
  94. failed_limit = lim
  95. limit_for_header = (lim.limit, args)
  96. break
  97. else:
  98. self.logger.error(
  99. "Skipping limit: %s. Empty value found in parameters.",
  100. lim.limit
  101. )
  102. continue
  103. request.state.view_rate_limit = limit_for_header
  104. if failed_limit:
  105. self.send_finished(failed_limit) # 发送重置消息
  106. raise RateLimitExceeded(failed_limit)
  107. # 覆盖父类私有方法(不建议尝试做法,此处覆盖仅为了插入告警)
  108. _Limiter__evaluate_limits = __evaluate_limits
  109. limiter = PayCaptchaLimiter(
  110. key_func=get_remote_address,
  111. storage_uri=setting.LIMITER_STORAGE_URI,
  112. ) # 付费验证码限制器实例对象