前文
有时需要针对访问接口实现频率限制,在Django里用的比较多的就是用过中间件的形式来完成。以下介绍Django中间件的五种自定义方法的用途和举个项目实例来简单说明。
Django中间件的五个方法
中间件中可以定义5个方法,分别是:
process_request(self,request)
process_view(self, request, callback, callback_args, callback_kwargs)
process_template_response(self,request,response)
process_exception(self, request, exception)
process_response(self, request, response
这五个方法从名字上就可以看出各自的用途,分别是request:针对请求前,view:针对请求已过到具体的函数之前调用,response:针对请求结束后,返回值发向网关的时候,这三个是最常用的,也是接下来举例的方法。而template/exception相对用的比较少,前者是针对返回是render时的response才回调用,后者则是在发生异常时才调用,用的不多。
另外就是中间件的调用顺序,这在诸多博客里都被反复提及,这边就不多说了,简单理解就是双重装饰器的效果,请求前依次从上往下,请求结束后则从下往上。
利用中间件实现访问接口的频率拦截
这边实现需求的频率拦截,连续请求函数1分钟频率超过10次就拦截,这个思路就是通过记录用户的ip或者用户名,然后在redis中用setex设置1分钟的key,设置value初始值为1,每请求一次,就利用redis.incrby自增一次,一分钟内超过十次,则在中间件进行拦截,一分钟后key消失,则计数重新开始。
中间件5大方法,这边采用的是process_view,先介绍获取用户ip的方法:
#获取远程ip
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
Django自带的request.META就能捕获REMOTE_ADDR,但是只能捕获请求到服务端的ip,因为可能中途会经过代理,所以需要进行判断。这边是通过HTTP_X_FORWARDED_FOR的字段来判断,该字段是利用了HTTP传输数据包的原理,每经过代理,会自动在X_FORWARDED上加数,所以如果能get到值,则说明经过了代理,通过split分割成列表,则取索引0拿到第一个ip就是客户端ip。
接下来定义一个类来进行判断键的存活时间和频率的控制。
import logging
from django.http import HttpResponse
import json
from django_redis import get_redis_connection
from django.utils.deprecation import MiddlewareMixin
from ..constants import *
# 定义记录日志
logger = logging.getLogger('django')
class RequestLimit:
def __init__(self):
self.redis = get_redis_connection('Middle') #定义redis的连接,在setting里配置cache
def check(self, code, user_id, increment):
store_key = 'limit_%s_%s' % (code, user_id)
timers = self.redis.get(store_key)
print('当前的时间次数是:', timers)
timers = int(timers) if timers else 0
if timers:
if timers <= MAX_TIMERS:
timers = self.redis.incrby(store_key, increment) #采用incrby自增,不会影响到过期时间
if timers == 1: # 并发控制, 如果刚好过期, 则要重新设置过期时间
self.redis.expire(store_key, PERIOD)
else:
self.redis.set(store_key, 1, ex=PERIOD) #PERIOD为1分钟,常量定义在constants.py,这边是设置过期1分钟的key,默认从1开始
success = timers <= MAX_TIMERS #判断当前次数是否小于最大次数,返回正负数,MAX_TIMERS是常数,定义在constants.py文件里
if success:
logger.debug('check success, code: %s, key: %s'
% (code, user_id))
else:
logger.info('check fail, code: %s, key: %s'
% (code, user_id))
return success
最后在中间件进行调用就行了:
def check_prevent(user_id, ip, increment): #调用类,返回success进行判断
prevent_client = FrequencyLimitService()
if user_id:
success = prevent_client.check('user_sql_limit', user_id, increment)
else:
success = prevent_client.check('user_sql_limit', ip, increment)
return success
# 对用户查询进行中间件频率控制
class PreventMiddleware(MiddlewareMixin):
def process_request(self, request):
pass
# 这里没有用view_func,直接通过request.path来进行捕获请求(因为有2个路径),如果是请求路径的则进行判断
def process_view(self, request, view_func, view_args, view_kwargs):
data = {}
path = request.path
if path in ('path1', 'path2'): #判断在2个路径的请求都需要经过中间件
# print(request.path)
user_id = request.session.get('user_id') #获取用户id
ip = get_client_ip(request)
increment = 1
write_success = True
write_success = check_prevent(user_id, ip, increment) #调用上方的函数
if not write_success:
data["status"] = 600
data["msg"] = "您在一分钟内查询的过于频繁,请稍后查询"
return HttpResponse(json.dumps(data),
content_type="application/json") #超过次数就报警返回
总结
用Django的中间件来控制接口的频率限制还是很常用的,也比较方便,当然以上代码还有很多要优化的,有兴趣的朋友可再探讨。