Appearance
游戏账号交易怎么防洗号?用 IP 归属地做异地登录核验(Go 实现)
在游戏账号交易平台、租号平台上,最典型的纠纷长这样:卖家把号挂出去,买家付钱收货,隔天发现密码被改、装备被清——卖家在官方渠道申诉「账号被盗」把号找回,钱货两空。圈内管这叫「洗号」。平台被投诉得多了,还会背上「帮骗子背书」的名声。
这类纠纷的难点在于:账号找回是官方规则允许的(防止盗号),平台没法一刀切禁止申诉。但交易前多问一句「这个号现在是谁在登、平时在哪登」,大部分洗号案就藏不住了。IP 归属地就是这「一问」里成本最低的答案。
一、洗号在 IP 上留下的痕迹
把账号交易流程拆开看,风险点在两端:
- 卖家端:号主平时在杭州登录,卖号当天登录 IP 却出现在千里之外的机房,或者一个 IP 上同时挂着几十个待售账号——这类号大概率是批量注册的料号,卖出去必然找回;
- 买家端:买家收货后第一次登录,IP 属地跟卖家描述的常用地(比如「浙江自用号」)完全对不上,且是代理/机房特征,那这单交易可能是用来洗号的跳板。
所以防线就一条:交易发生时,把当前登录 IP 的归属地跟账号历史登录地交叉比对。对不上就加验(设备、实名),两个以上信号异常直接拦下转人工。
二、方案设计
平台本来就有登录事件表(账号、IP、时间戳、操作类型),核验服务要做的事:
- 按账号拉出近 30 天的 IP 集合,逐个查归属地,得到「常用登录省份」集合;
- 交易请求进来时,查当前 IP 归属地,计算三个特征:是否出现在常用省份、运营商是否机房(
ip_type为 IDC)、该 IP 近期是否关联过多账号; - 特征打分: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 与私有化方案。先接登录事件表跑一周「只记录不拦截」,用真实交易数据校准权重,再逐步上拦截——这套规则引擎权衡的从来不是技术复杂度,而是误伤和漏放的生意账。