Skip to content

游戏防沉迷:平台怎么识别未成年人异地登录?(Java 实现) ​

未成年人账号限玩,是 2021 年之后游戏行业的常态:每周只能玩三小时,晚上十点到次日八点禁玩。真按这个规则执行的家长,孩子确实玩不了多少。但搜「防沉迷 解除」依然能搜出一堆生意——租号平台、代过人脸、拿家长身份证重实名。未成年人拿大人的账号玩,平台光靠实名信息根本看不出来,这就是防沉迷绕开的最后一环。

腾讯这类头部厂商的做法是双层:实名 + 人脸抽查。人脸成本高、体验差,不能每次登录都弹。中间缺一层「低成本、不打扰」的初筛——登录时的 IP 归属地核验就是这一层:一个 12 岁小孩的账号深夜在异地城市上线,或一个实名成人的账号长期在中学宿舍区活跃,先打个问号,再决定要不要上人脸。这篇用 Java 写一个这样的辅助核验服务。

一、未成年人绕防沉迷的典型路径 ​

归纳一下常见绕法,每一条都会在 IP 上留痕迹:

  • 拿家长账号玩:账号实名是 40 岁成年男性,但登录行为高度集中在晚间、周末和寒暑假,且长期固定在某几个 IP——问家属账号常用地址一核对就露馅;
  • 租号:租号平台账号池有限,几十上百个买家共用少量 IP,一个 IP 在深夜高频关联多个账号;
  • 异地代练/托管:账号登录 IP 突然从城市 A 跳到城市 B,和账号常用地、绑定的手机归属地都对不上,且常常持续到凌晨。

平台不可能每次都弹人脸,但可以把这几类「疑点」做成规则:IP 归属地 + 活跃时段 + 常用地比对,命中就打标,攒够置信度再触发二次验证。

二、方案设计:登录事件 -> 归属地 -> 规则引擎 ​

服务分三层:

  1. 接入层:登录接口 / 会话心跳里上报 IP(注意取真实客户端 IP,别被 X-Forwarded-For 伪造骗过去,直接信任 CDN 透传的头字段要校验);
  2. 特征层:写一个 GeoService,把 IP 送 IP9 接口换归属地(国家/省市/运营商),结果缓存起来,同一 IP 当天不重复查询;
  3. 规则层:对每个账号维护「常用地画像」(过去 30 天出现过的省市集合),新登录时比对。规则:不在常用地 + 时段敏感(深夜/上课时间)+ 账号年龄画像异常,三选二即触发核验队列。

下面代码就是这个服务的骨架:Spring Boot 风格,用 RestClient 调 IP9,ConcurrentHashMap 做缓存,规则判断收敛在一个方法里,方便调阈值。

三、Java 实现:登录核验服务 ​

java
import java.time.LocalTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.web.client.RestClient;

public class LoginGuardService {

    // IP9 免费接口:GET https://ip9.com.cn/get?ip=<ip>
    private static final String GEO_API = "https://ip9.com.cn/get?ip=";
    private final RestClient client = RestClient.create();
    // 缓存: ip -> "prov|city",当天有效,省免费额度
    private final Map<String, String> geoCache = new ConcurrentHashMap<>();

    record Geo(String prov, String city, String isp) {}

    /** 查询归属地,失败返回 null,调用方按放行处理(宁可放过,不可误伤) */
    private Geo lookup(String ip) {
        String key = geoCache.get(ip);
        if (key != null) {
            String[] p = key.split("\\|");
            return new Geo(p[0], p[1], p[2]);
        }
        try {
            Map<?, ?> body = client.get()
                    .uri(GEO_API + ip)
                    .retrieve()
                    .body(Map.class);
            if (body == null || !Integer.valueOf(200).equals(body.get("ret"))) {
                return null; // 非法 IP 或限流,按放行
            }
            Map<?, ?> data = (Map<?, ?>) body.get("data");
            Geo geo = new Geo(String.valueOf(data.get("prov")),
                    String.valueOf(data.get("city")),
                    String.valueOf(data.get("isp")));
            geoCache.put(ip, geo.prov() + "|" + geo.city() + "|" + geo.isp());
            return geo;
        } catch (Exception e) {
            return null; // 网络异常一律放行,风控不能阻塞正常登录
        }
    }

    /** 账号画像:常用省市集合(正常现网从 DB/Redis 读) */
    private Map<String, Integer> usualCities(String userId) {
        // 简化:真实实现查「过去30天登录省市」聚合
        return Map.of("广东|广州", 80, "广东|深圳", 15);
    }

    /** 核验入口:返回 1=放行 2=二次核验(人脸/短信) 3=拦截+人工 */
    public int verify(String userId, String ip, LocalTime now) {
        Geo geo = lookup(ip);
        if (geo == null) {
            return 1;
        }
        String city = geo.prov() + "|" + geo.city();
        Map<String, Integer> usual = usualCities(userId);
        if (usual.containsKey(city)) {
            return 1; // 常用地直接放行,不打扰
        }

        int hits = 0;
        // 信号1:不在常用地
        hits++;
        // 信号2:深夜登录(22:00-08:00 未成年人禁玩时段)
        boolean night = now.isBefore(LocalTime.of(8, 0))
                || !now.isBefore(LocalTime.of(22, 0));
        if (night) {
            hits++;
        }
        // 信号3:机房特征,租号/托管常用 IDC 出口(VIP 字段 ip_type=IDC)
        if (geo.isp().contains("IDC") || geo.isp().contains("数据中心")) {
            hits++;
        }
        // 信号4:账号画像年龄异常(实名成年但活跃时段全在夜间)——现网由画像服务提供
        return hits >= 3 ? 3 : (hits >= 2 ? 2 : 1);
    }
}

拆开看几个关键点。

缓存与容错:geoCache 保证同一 IP 一天只查一次,几十万日活也就几万次查询;接口异常、超时全部按「放行」处理——防沉迷的风控可以漏,不能把正常玩家挡在门外。返回码语义:ret=400 非法 IP、ret=429 限流,这两类都走 null 分支。

规则收敛:命中计数是刻意做得「看得懂」的。生产环境会把 usualCities 换成真实的 30 天画像服务,把 night 时段和实名年龄做交叉(成年账号深夜常态登录本身就有问题),再加设备指纹、人脸置信度这些外部信号一起打分。

别直接在控制器里调:这个服务应当放在登录链路的事件异步通道上,登录先放行,异步核验命中高危再召回弹人脸,不然每次登录都同步等 HTTP 查询,接口延迟会很难看。

四、落地注意事项 ​

  • 数据合规是硬约束:IP 归属地涉及个人信息,用途限定「防沉迷与账号安全」,建议只存省市级结果,不存原始 IP 或存了加密+定期清理,隐私政策里写清楚;
  • 误伤兜底:异地登录最常见的原因是「寒暑假回老家」「住校生周末回家」,这两种恰好是未成年人真实场景。所以第一次异地命中只打标不拦截,二次命中且时段敏感才上人脸,申诉渠道必须畅通;
  • 租号是猫鼠游戏:租号平台会用住宅代理换 IP,规则 3 会失效,但「一个 IP 高频关联多账号」这类聚集特征还在,配合账号侧的设备指纹一起看;
  • 阈值要灰度:先跑两周「只打标不处置」,统计正常未成年用户(寒暑假轨迹)的误报率,再决定哪些信号加权。

总结 ​

防沉迷的最后一公里是「识别账号背后的真实使用者」,IP 归属地核验是成本最低的初筛层:常用地画像 + 时段敏感 + 机房特征三条规则,就能把「拿家长账号深夜在异地登录」这类典型行为筛出来送二次核验。示例里用的 https://ip9.com.cn/get?ip=xxx,免费版 60 次/分钟够小体量平台做缓存后的核验;要 ip_type(ISP/BUS/IDC)精确区分家庭宽带与机房出口,看官网 https://www.ip9.com.cn 的 VIP 说明。把这段逻辑接进登录事件流,先灰度打标两周,用真实数据校准阈值,再决定什么时候升级成人脸核验——未成年人保护和玩家体验不是二选一。