个人做的小网站也需要警惕恶意流量,我们需要有应对恶意流量的方法。比如,我们的服务端API接口需要防止被别人爬虫,避免造成我们的损失。
前天看到我的网站流量一下从之前的100增加到4k多,一看来源网站,排名第一的:
刷了4k多访问。再看我的服务端API接口成本(因为我的接口每调用一次都会产生成本),也增加了不少。于是紧急开始增加网站防护。
但实际上,最理想的方式还是提前做好基础的防护。这次我积累了一些经验,都是免费的方式,但却能极大的增强网站防护。
监控
首先,我们要有监控的手段,比如,我服务端的API,调用一次是会扣减我在底层平台上的一些额度的。因此当出现了异常情况的时候,首先,我们可以及时发现并且止损。止损办法,就是临时先关闭API,系统维护。
使用Django提供的csrf_token
我服务端使用的是Django框架,它可以在每次下发前端网页的时候,在页面中带一个csrf_token参数,然后在前端发起服务端请求的时候,将这个csrf_token传给服务端,服务端会默认进行校验,校验不通过直接报错。
前端代码如下:
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (
!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) &&
!this.crossDomain
) {
xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}");
}
},
});
这种方式可以增加爬虫的难度,使得服务端API接口至少不至于那么容易被爬虫。当然,稍微有耐心一点的爬虫工程师便可以通过先获取页面上的csrf_token,然后伪造请求。所以我们还需要更多的保护措施。
使用阿里云防火墙
说实话,阿里云的这些服务都挺贵的,但是比较好的一点是它的防火墙也提供了免费版,有总比没有好。我们首先可以搜索到“Web应用防火墙”产品,然后购买一个免费版的套餐。
之后在“接入管理”里面,在“CNAME”里将我们的域名添加进去,然后在“云产品接入”里将我们的服务器实例添加进去。
添加进去之后,默认规则就会开始生效了,我们基本不用手动去配置防护规则。
使用人机验证
但是,即使接完了防火墙能够拦截掉一些恶意流量,我们服务端的API还是很容易被爬虫。因此,我们还需要一种能够保护服务端API的办法,而这个办法就是接入人机验证
这里我折腾了半天,首先我去阅读了阿里云的人机验证文档,发现确实好用,后来才发现太贵。然后一通搜索又搜到了:https://www.vaptcha.com/,并且一步步按照文档接入了我的页面。不过后来觉得有两点不足:
一个是每次验证都要画曲线太麻烦,不太友好;另一个是我遇到了总是验证不过的情况,不太稳定。
然后我想起了之前进入一些网站的时候看到过的如下页面:
于是试着搜了搜cloudflare人机验证,没想到cloudflare居然提供了免费的人机验证服务,而且看起来在国内打开也还算快,这就很完美了。
它的文档链接:https://developers.cloudflare.com/turnstile/get-started/client-side-rendering
这种人机验证的大致流程是:前端页面需要初始化一个Cloudflare提供的组件,当需要验证的时候,比如用户点击登录的时候,前端可以调用这个组件的验证/渲染方法,然后会唤起验证。当组件验证通过之后,前端可以获取到一个token,之后,带着这个token请求服务端,服务端需要用这个token,结合IP地址,去请求cloudflare的验证接口,来最终确认人机验证是否通过。其中,这个token只能用一次,第二次去请求验证接口将会验证失败。
人机验证流程
最后,分享一下我的人机验证从前端到服务端的流程。
因为我觉得每次发起请求之前都去验证一次,体验不太好,我的目的是防止爬虫请求,所以只要有这一层拦截就行了,不需要每次都拦截;但是,验证成功之后,也需要过一段时间重新验证一次,避免爬虫工程师通过复制cookie的方式来请求我的接口。
所以,我准备了一个独立的人机验证页面,这个页面就做一件事:加载Cloudflare的验证组件,组件初始化完毕之后。调用服务端实现的一个验证接口进行验证
当服务端的验证接口验证成功之后,会使用session写入一个“已验证”的key,过期时间两小时,这样两小时内就不用重复验证。
同时,其余的所有服务端API接口,在真正执行之前都会先校验一次session中的这个“已验证”的key,如果key已经失效,则会返回一个特定的“未验证”的error,前端收到这个error之后,就会跳转到我准备的独立的人机验证页面。
服务端验证接口的代码如下:
from django.shortcuts import render
from django.http import HttpResponse,JsonResponse
from django.views.decorators.csrf import csrf_exempt
from v1.util.token_store import cf_key
import json
import requests
@csrf_exempt
def cf_captcha(request):
if not cf_captcha_verify(request):
return JsonResponse({"err": "verify failed"})
request.session["has_verified"] = 1 # 在session中设置一个key
request.session.set_expiry(7200) # expires 2 hours
print("set verified")
return JsonResponse({"err": ""})
def cf_captcha_verify(request):
url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
token = request.POST.get("verify_token")
client_ip = request.META.get('REMOTE_ADDR') # 获取请求的IP地址
form_data = {
"secret": cf_key,
"response": token,
"remoteip": client_ip,
}
resp = requests.post(url, json=form_data)
if resp.status_code != 200:
print("response err, verify failed")
return False
resp_json = json.loads(resp.content)
if resp_json["success"]:
return True
else:
print("verify failed:", resp_json)
return False
验证页面的前端代码如下:
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="strict-origin-when-cross-origin" />
<title>人机验证</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback"
async
defer
></script>
<script>
window.onloadTurnstileCallback = function () {
turnstile.render("#cfts", {
sitekey: "XXXXXX",
callback: function (token) {
console.log(`Challenge Success ${token}`);
$.ajax({
url: "/verify/",
type: "POST",
dataType: "json",
data: {
verify_token: token,
},
success: function (response) {
if ("err" in response && response["err"] != "") {
alert(response["err"]);
turnstile.reset("#cfts");
return;
};
window.close();
},
error: function (xhr, status, error) {
console.error("Ajax请求失败:", error);
turnstile.reset("#cfts");
},
});
},
});
};
</script>
</head>
<body>
<h2>Verifying</h2>
<div id="cfts"></div>
</body>
</html>