limiter.py 4.8 KB

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