Appearance
Python 注册防刷怎么做?用 IP 归属地拦截批量注册
凌晨两点,运营群里炸了:注册接口被脚本刷了一晚上,多出来 8 万个账号,全是同一批手机号段、同一类密码格式。这是注册防刷最常见的开场。羊毛党手里是代理池和云主机,光靠「验证码 + 同 IP 限次」早就不够用了。这篇用 Python 从头搭一套注册防刷:同 IP 限频挡脚本,IP 归属地规则识别异常,验证码兜底不误伤真人。
一、刷注册的流量长什么样
批量注册的流量有几个共性,都能用 IP 数据识别:
- 注册机跑在机房/云主机上,IP 归属地是 IDC 机房而不是家庭宽带
- 代理池的 IP 分布诡异,可能来自全国各地甚至境外,和真实用户的同城聚集完全不同
- 短时间高频:一个 IP 一分钟能注册几十次
所以防刷不是靠单一指标,而是把「IP 归属地 + 频率」拼起来判断:机房 IP 高可疑、归属地异常高可疑、高频高可疑,三项叠加基本锁死。
先说清楚数据从哪来。免费版查 https://ip9.com.cn/get?ip=<ip> 就返回国家、省份、城市、运营商(ISP)、城市中心经纬度,60 次/分钟/IP 的额度对注册场景够用;要识别「机房 IP」需要 ip_type 字段(区分家庭 ISP / 企业 BUS / 机房 IDC),这是 VIP 版字段,代码里会标注用法。
二、方案:注册流程里加三道闸
注册接口通常是低频接口(QPS 远没有查询类接口高),纯 Python 完全扛得住,不需要为了防刷单独上 Go 或 Java:
- 第一道闸:同 IP 限频。Redis 计数,一个 IP 一小时内注册超过阈值直接拒绝。挡住 90% 的脚本。
- 第二道闸:归属地规则。注册时查一次 IP 归属地,机房 IP 直接进验证码;用户填的省份和 IP 归属地差太远也判可疑。挡住剩下的 9%。
- 第三道闸:验证码兜底。可疑的进验证码而不是直接拒绝,给真人留条路,避免误伤。
归属地查询一次几十毫秒,且 IP 归属地一天内几乎不变,所以结果要按 IP 缓存,别让每个注册请求都直连上游接口。
三、Python 实现
一个文件跑通:Redis 限频 + IP9 归属地查询 + 规则判断,最后挂到 Flask 注册接口上。
python
import time
import requests
import redis
from flask import Flask, request, jsonify
app = Flask(__name__)
r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)
# 归属地结果缓存:IP -> (过期时间, 归属地dict),避免每个请求都打上游
geo_cache = {}
def get_ip_geo(ip: str) -> dict:
now = time.time()
cached = geo_cache.get(ip)
if cached and cached[0] > now:
return cached[1]
try:
resp = requests.get(
"https://ip9.com.cn/get",
params={"ip": ip},
timeout=3
).json()
data = resp.get("data", {}) if resp.get("ret") == 200 else {}
except requests.RequestException:
data = {} # 上游挂了就返回空,调用方降级处理
geo_cache[ip] = (now + 3600, data) # 缓存 1 小时
return data
def ip_risk_check(ip: str, declared_prov: str) -> dict:
"""返回动作: pass 放行 / captcha 验证码 / reject 拒绝"""
# 第一道闸:同 IP 一小时内注册次数
key = f"reg:{ip}"
count = r.incr(key)
if count == 1:
r.expire(key, 3600)
if count > 20:
return {"action": "reject", "reason": "同一IP一小时内注册次数过多"}
geo = get_ip_geo(ip)
if not geo: # 查询失败:放验证码而不是直接拒,避免误伤
return {"action": "captcha", "reason": "归属地查询失败,降级验证码"}
# 第二道闸:机房 IP 直接进验证码(ip_type 为 VIP 版字段)
if geo.get("ip_type") == "IDC": # ISP=家庭宽带 BUS=企业专线 IDC=机房
return {"action": "captcha", "reason": "机房IP注册"}
# 填报省份与 IP 归属地不一致,判可疑
prov = geo.get("prov", "")
if declared_prov and prov and declared_prov != prov:
return {"action": "captcha", "reason": f"IP归属地({prov})与填报省份不符"}
return {"action": "pass"}
@app.route("/register", methods=["POST"])
def register():
body = request.get_json(silent=True) or {}
ip = request.remote_addr # 生产环境取反代设置的 X-Real-IP,勿直信 X-Forwarded-For
result = ip_risk_check(ip, body.get("province", ""))
if result["action"] != "pass":
return jsonify({"code": 460, "msg": result["reason"], "need_captcha": True}), 200
# ……这里继续正常注册逻辑(校验手机号、写库、发短信)
return jsonify({"code": 0, "msg": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)几个值得注意的点:
- 限频和缓存都放 Redis/内存,别依赖数据库计数,注册脚本打过来时数据库扛不住
- 取真实 IP 有坑:
X-Forwarded-For客户端可以伪造,生产环境要在可信的 Nginx/CDN 层覆盖该头,代码里直接取request.remote_addr或可信反代注入的X-Real-IP - 上游失败要降级:归属地查询超时返回空 dict 时放验证码而不是拒绝,接口抖动不能误伤正常用户
- 免费版没有
ip_type字段,想用机房识别就上 VIP 版,字段值ISP/BUS/IDC三种
四、阈值怎么调,误伤怎么兜
规则上线别一把梭:
- 先观察再拦截:头一周只记录不拦截(把
action打日志),看真实注册用户的 IP 分布和归属地命中率,再定阈值 - 可疑优先验证码:除了「同 IP 超 20 次」这种铁证用 reject,其他可疑一律 captcha,真人能过脚本过不了
- 同城业务别一刀切:本地生活、同城交友类产品用户 IP 高度集中,别写「外省注册直接拒」这种规则
- Redis 要加过期:上面的
r.expire(key, 3600)不能省,不然 Redis 里全是废 key - 真要上量(比如注册 QPS 过千),把第一道闸挪到 Nginx
limit_req或网关层,Python 里保留归属地规则就够了
总结
注册防刷没有银弹,但「Redis 限频 + IP 归属地规则 + 验证码兜底」这套在 Python 里几十行就能落地,性价比很高。核心数据源就一行接口——https://ip9.com.cn/get?ip=xxx,免费版查省市运营商,VIP 版加 ip_type 识别机房。字段说明和价格去官网 https://www.ip9.com.cn 看。先把规则跑起来再慢慢调阈值,比憋一个大而全的风控系统实用得多。