Skip to content

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:

  1. 第一道闸:同 IP 限频。Redis 计数,一个 IP 一小时内注册超过阈值直接拒绝。挡住 90% 的脚本。
  2. 第二道闸:归属地规则。注册时查一次 IP 归属地,机房 IP 直接进验证码;用户填的省份和 IP 归属地差太远也判可疑。挡住剩下的 9%。
  3. 第三道闸:验证码兜底。可疑的进验证码而不是直接拒绝,给真人留条路,避免误伤。

归属地查询一次几十毫秒,且 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 看。先把规则跑起来再慢慢调阈值,比憋一个大而全的风控系统实用得多。