上一次我们学习了10秒之内的频率反爬,原理是缓存中保存前来访问的ip地址10秒,一旦该ip地址再次访问我们网站,我们的服务器会先检查缓存中是否保存了你的ip地址,如果查找到,说明是10秒之内来访问我们的网站,我们就拒绝访问,从而达到了频率反爬。
频率请求限制,频率反爬
现在我们用新的方法来进行频率反爬,做到60秒内限制访问10次。
实现的方法就是,基于当前访问的时间往前推1分钟,我们查看1分钟之内有多少条访问记录,如果超过10条我们就拒绝访问。
只要我们这个value检测到超过10条我们就拒绝该ip地址访问
下面我们用代码来实现这个功能
class HelloMiddle(MiddlewareMixin): # Mixin 是使混合的意思
def process_request(self, request):
print(request.META.get('REMOTE_ADDR'))
ip = request.META.get('REMOTE_ADDR')
requests = cache.get(ip, []) # 从缓存中读取以往存储的ip,并且以[]列表的形式赋值给requests
while requests and time.time() - requests[-1] > 60: # 判断有记录存在,且当前时间减去最早记录的时间大于60(也就是时间过期了)
requests.pop() # 清洗,把过期的记录清空。这个循环的作用是把过期的时间记录清楚
if len(requests) > 10: # 如果有10条没有过期的访问记录,说明访问频率太高,需要拒绝访问
return HttpResponse('请求次数过于频繁,辣鸡小爬虫回家睡觉吧')
requests.insert(0, time.time()) # 如果没有满足10次访问,就把当前的访问时间戳插入到列表中的开头
cache.set(ip, requests, timeout=60) # 将requests保存到缓存中,并且设置过期时间为60
当我们访问第十一次的时候
这样就可以限制访问次数,注意,被提示访问频繁时只要最早的那次访问记录被清洗掉,我们就又可以访问了,而不是等待60秒才能访问。
假如爬虫依旧在我们警告过后还访问的话,我们可以建立黑名单,对于60秒内频率超过30次的,施行黑名单,直接拉黑ip地址,操作如下。
class HelloMiddle(MiddlewareMixin): # Mixin 是使混合的意思
def process_request(self, request):
print(request.META.get('REMOTE_ADDR'))
ip = request.META.get('REMOTE_ADDR')
black_list = cache.get('black', [])
if ip in black_list:
return HttpResponse('黑名单,凉凉了')
requests = cache.get(ip, []) # 从缓存中读取以往存储的ip,并且以[]列表的形式赋值给requests
while requests and time.time() - requests[-1] > 60: # 判断有记录存在,且当前时间减去最早记录的时间大于60(也就是时间过期了)
requests.pop() # 清洗,把过期的记录清空。这个循环的作用是把过期的时间记录清楚
requests.insert(0, time.time()) # 如果没有满足10次访问,就把当前的访问时间戳插入到列表中的开头
cache.set(ip, requests, timeout=60) # 将requests保存到缓存中,并且设置过期时间为60
if len(requests) > 30: # 如果60s内有30条,拉黑名单
black_list.append(ip)
cache.set('black', black_list, timeout=60*60*24)
return HttpResponse('辣鸡小爬虫,拉你进黑名单')
if len(requests) > 10: # 如果有10条没有过期的访问记录,说明访问频率太高,需要拒绝访问
return HttpResponse('请求次数过于频繁,辣鸡小爬虫回家睡觉吧')
实验:自定义中间件 process_exception
应用交互友好化(提高服务器容错率)
Debug模式下,当我们出现服务器在被访问时出错时,会返回错误码,而关闭debug则会返回500错误,这个错误对于程序员来说是一件很丢脸的事情,所以我们可以利用下面的中间件来提高容错。
process_exception(self,request,exception):
当视图抛出异常时调用,不主动进行返回或返回HttpResponse对象- 视图出错就会报出5xx服务器内部错误,这种错误是程序员最丢人的错误,通过这个切面我们可以全局捕获5xx错误,提高容错率。
我们先制造一个错误的视图,这样可以验证中间件发挥作用
新建urls
url('^clac/', views.clac, name='clac'),
新建views
def clac(request):
a = 1
b = 1
result = (a+b)/0 # 这行会报错
return HttpResponse(result)
可以看到访问时
然后我们在中间件MiddleWare.py添加
def process_exception(self, request, exception):
print(request, exception) # 打印出请求类型,和报错原因,用来收集报错信息
return redirect(reverse('App:index')) # 发生错误我们就重定向到主页
这样我们只要访问clac这个路由,就会直接重定向到index了,从而将服务器内部错误隐藏了起来,提高了网站的逼格。
同时也会将报错原因打印出来
研究Django自带中间件的原理
我们这里研究的是其中一个Csrf对的中间件
我们ctrl+a进入中间件查看
可以看到这个是继承了我们之前使用过的MiddlewareMixin,我们再点入MiddlewareMIxin
class MiddlewareMixin:
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__() # 调用的supper的init
def __call__(self, request):
response = None
if hasattr(self, 'process_request'):
response = self.process_request(request)
if not response:
response = self.get_response(request)
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
return response # 返回response
可以看到第六行这是call是类装饰器
接着我们回到csrf的中间件代码中
当一个文件的代码很多的时候,我们可以从最左侧的structure来查看当前文件结构
V指的是变量 f开头指的是函数fuction m是方法
accept接收请求
reject拒绝请求
def _reject(self, request, reason):
logger.warning(
'Forbidden (%s): %s', reason, request.path,
extra={
'status_code': 403, # 拒绝访问时返回的代码为403
'request': request,
}
)
return _get_failure_view()(request, reason=reason)
get_token获取认证方式
set_token设置认证方式
- 在response返回的时候会设置token
- 客户端get请求来的时候设置token,post请求来的时候验证token
还有很多以__开头的方法,这种方法称为魔术方法
中间件的切点函数/方法
process_request
def process_request(self, request):
csrf_token = self._get_token(request) # 利用get_token,获取token
if csrf_token is not None: # 如果获取到的token不为空
# Use same token next time.
request.META['CSRF_COOKIE'] = csrf_token
get_token
def _get_token(self, request):
if settings.CSRF_USE_SESSIONS: # 先检查是否用的sessions
try:
return request.session.get(CSRF_SESSION_KEY)
except AttributeError:
raise ImproperlyConfigured(
'CSRF_USE_SESSIONS is enabled, but request.session is not '
'set. SessionMiddleware must appear before CsrfViewMiddleware '
'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
)
else: # 假如没有sessions,就试着获取cookies
try:
cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
except KeyError:
return None
csrf_token = _sanitize_token(cookie_token) # 序列化
if csrf_token != cookie_token:
# Cookie token needed to be replaced;
# the cookie needs to be reset.
request.csrf_cookie_needs_reset = True # 需要重置
return csrf_token
process_view
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False): # 如果完成了csrf的认证,就直接返回None
return None # 返回None相当于这一层的中间件结束
if getattr(callback, 'csrf_exempt', False): #exempt是豁免的意思,这里获取csrf_exempt这个属性,默认是False,如果这个属性获取到了就return None
return None #也就是说如果有csrf豁免权的话也就不需要验证csrf了
# 从上面两个判断可以知道,如果我们有csrf_processing_done或者csrf_exempt两者其一的属性,我们就可以跳过csrf的认证
# Assume that anything not defined as 'safe' by RFC7231 needs protection
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): # 也就是说如果是'GET','HEAD','OPTIONS','TRACE'这些请求,就不用csrf认证
if getattr(request, '_dont_enforce_csrf_checks', False): # 如果有不强制csrf检查,就返回_accept
# Mechanism to turn off CSRF checks for test suite.
# It comes after the creation of CSRF cookies, so that
# everything else continues to work exactly the same
# (e.g. cookies are sent, etc.), but before any
# branches that call reject().
return self._accept(request)
if request.is_secure(): # 如果是安全的就逐步进行检查
# Suppose user visits http://example.com/
# An active network attacker (man-in-the-middle, MITM) sends a
# POST form that targets https://example.com/detonate-bomb/ and
# submits it via JavaScript.
#
# The attacker will need to provide a CSRF cookie and token, but
# that's no problem for a MITM and the session-independent
# secret we're using. So the MITM can circumvent the CSRF
# protection. This is true for any HTTP connection, but anyone
# using HTTPS expects better! For this reason, for
# https://example.com/ we need additional protection that treats
# http://example.com/ as completely untrusted. Under HTTPS,
# Barth et al. found that the Referer header is missing for
# same-domain requests in only about 0.2% of cases or less, so
# we can use strict Referer checking.
referer = request.META.get('HTTP_REFERER') # 这里的refer指的是上一个页面,当你请求服务器的时候,服务器可以获取到你上一个请求的服务器在哪里
if referer is None: # 如果refer为空就直接返回reject,也就是直接拒绝访问( REASON_NO_REFERER拒绝原因是没有上一个页面)
return self._reject(request, REASON_NO_REFERER)
referer = urlparse(referer) # 如果有refer就转换一下
# Make sure we have a valid URL for Referer.
if '' in (referer.scheme, referer.netloc): # REASON_MALFORMED_REFERER拒绝原因上一页格式有问题
return self._reject(request, REASON_MALFORMED_REFERER)
# Ensure that our Referer is also secure.
if referer.scheme != 'https': # 如果不是https协议,也拒绝访问,因为csrf是安全的请求
return self._reject(request, REASON_INSECURE_REFERER)
# If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
# match on host:port. If not, obey the cookie rules (or those
# for the session cookie, if CSRF_USE_SESSIONS).
good_referer = ( # 这里新建一个good_referer
settings.SESSION_COOKIE_DOMAIN
if settings.CSRF_USE_SESSIONS # 从sessions里获取
else settings.CSRF_COOKIE_DOMAIN # 否则从cookie中获取
)
if good_referer is not None: # 如果刚刚创建的不是空的
server_port = request.get_port() # 获取端口
if server_port not in ('443', '80'): # 如果获取到的端口不是443或者80这种默认端口,那么就用自己上一步获取到的端口
good_referer = '%s:%s' % (good_referer, server_port)
else: # 如果good_referer是空的,就直接获取host赋值给good_referer
# request.get_host() includes the port.
good_referer = request.get_host()
# Here we generate a list of all acceptable HTTP referers,
# including the current host since that has been validated
# upstream.
good_hosts = list(settings.CSRF_TRUSTED_ORIGINS) # 转换成列表
good_hosts.append(good_referer) # 追加了我们刚刚创建的referer进入good_hosts
if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
# 意思是如果 any()为False,就拒绝访问,也就是如果good_gosts中的元素都为假False,就拒绝访问
reason = REASON_BAD_REFERER % referer.geturl()
return self._reject(request, reason)
csrf_token = request.META.get('CSRF_COOKIE') # 令csrf_token获取CSRF_COOKIE
if csrf_token is None: # 如果获取到的为空,就拒绝访问
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
# and in this way we can avoid all CSRF attacks, including login
# CSRF.
return self._reject(request, REASON_NO_CSRF_COOKIE)
# Check non-cookie token for match.
request_csrf_token = "" # 如果csrf_token不为空的
if request.method == "POST": # 如果请求是post
try: # 从form表单中获取到csrfmiddlewaretoken,如果没有获取到就赋值空字符串
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
except IOError:
# Handle a broken connection before we've completed reading
# the POST data. process_view shouldn't raise any
# exceptions, so we'll ignore and serve the user a 403
# (assuming they're still listening, which they probably
# aren't because of the error).
pass
if request_csrf_token == "": # 判断如果为空
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE.
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '') # 从settings中去获取并赋值给request_csrf_token
request_csrf_token = _sanitize_token(request_csrf_token) # 转换一下tonken
if not _compare_salted_tokens(request_csrf_token, csrf_token): # 检验的token是否加盐,如果没有匹配上token,就拒绝访问
return self._reject(request, REASON_BAD_TOKEN) # 也就是这里验证的csrf
return self._accept(request) # 如果通过验证的token就返回接受,可以正常访问
csrf实际上实在process_view中验证的
解释:
第76行使用了any()函数:
- any(iterable) 函数用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False,如果有一个为 True,则返回 True。
- iterable指的是元组或者列表
- 详细见https://www.runoob.com/python/python-func-any.html
第76行的
is_same_domain(referer.netloc, host) for host in good_hosts
使用的是列表推导式:- https://blog.csdn.net/liukai2918/article/details/80428441详细请看链接
- 列表推导式的结果还是列表,所以和上一点的解释搭配起来使用
如何让特定请求跳过csrf验证
假如我们的项目开发完了,有一个函数,我们只想给某一个请求,去除scrf验证,该怎么实现?
首先建立一个模板login.html,用来充当我们登陆的界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="{% url 'app:login' %}" method="post">
<span>用户名</span><input type="text" name="username" placeholder="嘤嘤嘤">
<br>
<button>嘤嘤嘤嘤</button>
</form>
</body>
</html>
再建立一个url
url('^login/', views.login, name='login')
views:
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
elif request.method == 'POST':
return HttpResponse('post请求成功')
解决csrf禁止访问的办法
我们出现了这个csrf禁止访问,在之前我们通过:
- 添加csrf标签,
- 注释掉csrf验证
来解决csrf验证失败,现在我们学习第三种方法来解决这个问题
- 第三种方法:添加豁免属性
我们在views中添加一个豁免属性的装饰器
@csrf_exempt
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
elif request.method == 'POST':
return HttpResponse('post请求成功')
添加装饰器之后就可以免除csrf验证
再次访问:成功访问
这个方法的原理就是在process_view中
有两种可以跳过csrf验证的判断,第一种我们没见过,它的方法不是装饰器在这里就不能使用了,但是第二种是csrf豁免,只要有这个属性我们就可以豁免了。
第一种其实也是验证csrf_processing_done这个属性,我们可以自己写一个中间件加一个属性,等于True,我们也可以实现。
此处评论已关闭