Skip to content

游戏账号交易怎么防洗号?用 IP 归属地做异地登录核验(Go 实现) ​

在游戏账号交易平台、租号平台上,最典型的纠纷长这样:卖家把号挂出去,买家付钱收货,隔天发现密码被改、装备被清——卖家在官方渠道申诉「账号被盗」把号找回,钱货两空。圈内管这叫「洗号」。平台被投诉得多了,还会背上「帮骗子背书」的名声。

这类纠纷的难点在于:账号找回是官方规则允许的(防止盗号),平台没法一刀切禁止申诉。但交易前多问一句「这个号现在是谁在登、平时在哪登」,大部分洗号案就藏不住了。IP 归属地就是这「一问」里成本最低的答案。

一、洗号在 IP 上留下的痕迹 ​

把账号交易流程拆开看,风险点在两端:

  • 卖家端:号主平时在杭州登录,卖号当天登录 IP 却出现在千里之外的机房,或者一个 IP 上同时挂着几十个待售账号——这类号大概率是批量注册的料号,卖出去必然找回;
  • 买家端:买家收货后第一次登录,IP 属地跟卖家描述的常用地(比如「浙江自用号」)完全对不上,且是代理/机房特征,那这单交易可能是用来洗号的跳板。

所以防线就一条:交易发生时,把当前登录 IP 的归属地跟账号历史登录地交叉比对。对不上就加验(设备、实名),两个以上信号异常直接拦下转人工。

二、方案设计 ​

平台本来就有登录事件表(账号、IP、时间戳、操作类型),核验服务要做的事:

  1. 按账号拉出近 30 天的 IP 集合,逐个查归属地,得到「常用登录省份」集合;
  2. 交易请求进来时,查当前 IP 归属地,计算三个特征:是否出现在常用省份、运营商是否机房(ip_type 为 IDC)、该 IP 近期是否关联过多账号;
  3. 特征打分:0 分放行,1 分要求二次核验(滑块/设备指纹),2 分及以上转人工。

查询接口用 https://ip9.com.cn/get?ip=xxx(免费版 60 次/分钟/IP),批量场景循环调用加缓存。Go 写核验服务合适:交易高峰并发高,goroutine 天然适合「一次核验查多个 IP」的并行 fan-out。

三、Go 实现 ​

go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"
)

type Geo struct {
	IP     string `json:"ip"`
	Prov   string `json:"prov"`
	City   string `json:"city"`
	ISP    string `json:"isp"`
	IPType string `json:"ip_type"` // VIP 字段:ISP 家庭 / BUS 企业 / IDC 机房
}

var (
	client = &http.Client{Timeout: 5 * time.Second}
	cache  sync.Map              // 内存缓存,避免同一 IP 反复查
	tokens = make(chan struct{}, 60) // 并发上限,别一次打爆免费额度
)

func queryGeo(ip string) (*Geo, error) {
	if v, ok := cache.Load(ip); ok {
		return v.(*Geo), nil
	}
	tokens <- struct{}{}
	defer func() { <-tokens }()
	r, err := client.Get("https://ip9.com.cn/get?ip=" + ip)
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()
	body, _ := io.ReadAll(r.Body)
	var resp struct {
		Ret  int  `json:"ret"`
		Data *Geo `json:"data"`
	}
	if json.Unmarshal(body, &resp) != nil || resp.Ret != 200 {
		return nil, fmt.Errorf("查询失败 ret=%d", resp.Ret)
	}
	cache.Store(ip, resp.Data)
	return resp.Data, nil
}

// checkTrade 对一次账号交易做核验。
// history:账号近 30 天常用登录省份集合;curIP:卖家当前登录 IP。
func checkTrade(curIP string, history []string, usersOnIP int) (int, string) {
	geo, err := queryGeo(curIP)
	if err != nil {
		// 接口异常时降级:不放行高风险,也拦住正常交易,转人工最快
		return 2, "归属地查询暂不可用,转人工核验"
	}
	score := 0
	reason := []string{}
	if !contains(history, geo.Prov) {
		score++
		reason = append(reason, "当前省份不在历史常用地")
	}
	if geo.IPType == "IDC" || strings.Contains(geo.ISP, "IDC") || strings.Contains(geo.ISP, "数据中心") {
		score++
		reason = append(reason, "机房/代理特征")
	}
	if usersOnIP >= 10 {
		score++
		reason = append(reason, fmt.Sprintf("该IP近1小时关联%d个账号", usersOnIP))
	}
	switch {
	case score >= 2:
		return score, "拦截:" + strings.Join(reason, ",")
	case score == 1:
		return score, "二次核验:" + strings.Join(reason, ",")
	default:
		return 0, "放行"
	}
}

func contains(set []string, s string) bool {
	for _, v := range set {
		if v == s {
			return true
		}
	}
	return false
}

func main() {
	// 示例:账号 A 历史常用地是浙江/上海,现在从山东的 IP 发起卖号
	score, verdict := checkTrade("202.102.70.1",
		[]string{"浙江", "上海"}, 12)
	fmt.Printf("score=%d verdict=%s\n", score, verdict)

	// 并行核验一批交易(数据量大时开 goroutine + WaitGroup 即可)
	// 注意免费版 60 次/分钟额度,token 通道已在 queryGeo 里限住并发。
	_ = time.Now
}

几处值得说的写法:queryGeo 里 cache 先行,sync.Map 对高并发读安全;token 通道把并发压在 60 以内,配合缓存不会撞上免费版额度;接口抛错时返回「转人工」而不是放行——卖号场景宁可多拦一笔,不能漏放一个料号。生产环境里 history 应该从登录事件表离线算好(比如每天跑定时任务更新),别在交易热路径上现查。

四、落地注意事项 ​

  • 误伤大头是「号主真的搬家/出差」:只用「省份不在常用地」这一个信号定罪会误伤,所以打分制里单独一个信号最多触发二次核验,两个以上才拦截;二次核验环节给设备指纹和实名留口子。
  • 共享出口别当机房:公司、校园出口是 BUS 特征,ip_type 字段能区分 ISP/BUS/IDC,别拿「非家庭宽带」一刀切。
  • 历史地要排除异常段:算常用省份时先剔除交易当天的记录,否则洗号者登过一次就能把自己的 IP 洗进常用地。窗口取 30 天、剔除最近 24 小时是常见做法。
  • 合规:账号、IP、登录时间都属于个人信息,处理前在隐私政策里写明用途(账号交易安全核验),留存期限和申诉渠道都要有;卖号被拦的提示里别泄露具体判断逻辑,防止被批量试探。

总结 ​

洗号案的本质是「人和号对不上」——交易双方的真实身份、注册归属、登录习惯三条线凑不齐。IP 归属地核验给平台一个低成本的三线交叉点:查 https://ip9.com.cn/get?ip=卖家IP,跟账号历史常用省份比对,叠加机房特征和 IP 关联账号数,打分分级处置。原型里 Go 的并发查询和缓存可以直接抄进线上服务,接口免费版 60 次/分钟够中小平台跑,量大再看官网 https://www.ip9.com.cn 的 VIP 与私有化方案。先接登录事件表跑一周「只记录不拦截」,用真实交易数据校准权重,再逐步上拦截——这套规则引擎权衡的从来不是技术复杂度,而是误伤和漏放的生意账。