0%

游戏实名制接入,这套流程够用了



一、前言

国内游戏上线,有一个环节绕不过去:实名认证与防沉迷。这不是你选不选的问题——2021 年文件下来之后,所有网络游戏都得接国家新闻出版署的实名系统,不接就是违规。

技术链路其实不复杂:客户端收信息 → 服务端调国家接口校验 → 根据结果拦人或放行。差别在于每家公司的接入方式、数据字段和回调处理细节不一样。

下面直接走流程,从接口设计到数据表,一次过完。

二、整体流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
用户输入姓名+身份证

┌──────────────┐
│ 游戏客户端 │ 提交认证请求
└──────┬───────┘

┌──────────────┐
│ 游戏服务端 │ 转发到实名认证服务
└──────┬───────┘

┌──────────────────────┐
│ 实名认证服务(业务层)│ 缓存查重→调接口
└──────┬───────────────┘

┌──────────────────────┐
│ 国家实名认证接口 │ 返回实名结果
└──────────────────────┘

┌──────────────┐
│ 游戏服务端 │ 根据结果拦截/放行
└──────────────┘

三个关键节点:客户端提交服务端调接口游戏端执行策略

三、数据表设计

3.1 用户实名表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `user_realname` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '游戏用户 ID',
`real_name` VARCHAR(64) NOT NULL COMMENT '真实姓名',
`id_card` VARCHAR(32) NOT NULL COMMENT '身份证号(SHA256 存储)',
`id_card_md5` CHAR(32) NOT NULL COMMENT '身份证号 MD5,用于判重',
`gender` TINYINT DEFAULT 0 COMMENT '性别 0未知 1男 2女',
`birthday` DATE DEFAULT NULL COMMENT '出生日期,用于算年龄',
`auth_channel` VARCHAR(16) NOT NULL COMMENT '认证渠道:nation / alipay / wechat',
`auth_time` DATETIME NOT NULL COMMENT '认证时间',
`auth_result` TINYINT NOT NULL DEFAULT 0 COMMENT '0未认证 1通过 2不通过',
`id_card_sha256` CHAR(64) NOT NULL COMMENT '身份证号 SHA256',
INDEX `idx_user` (`user_id`),
INDEX `idx_md5` (`id_card_md5`),
UNIQUE KEY `uk_md5` (`id_card_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户实名信息表';

注意:身份证号不能明文存储。合规要求至少存储为 SHA256 摘要。id_card_md5 仅用于判重(一个身份证只能认证一个账号),不要暴露给前端。

3.2 防沉迷策略表

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `user_play_policy` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT UNSIGNED NOT NULL,
`user_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1成年人 2未成年人',
`play_daily_max` INT NOT NULL DEFAULT 0 COMMENT '每日最大时长(分钟),0不限制',
`cur_play_today` INT NOT NULL DEFAULT 0 COMMENT '今日已玩时长(分钟)',
`ban_night` TINYINT NOT NULL DEFAULT 0 COMMENT '是否夜间禁玩(22-次日8点)',
`recharge_limit` DECIMAL(10,2) DEFAULT NULL COMMENT '月充值限额',
`update_date` DATE NOT NULL COMMENT '数据所属日期',
INDEX `idx_user_date` (`user_id`, `update_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户游戏策略表';

四、实名认证接口

4.1 国家实名认证接口

主流的接法是调国家新闻出版署提供的实名接口,走 HTTP 就行:

1
2
3
4
5
6
7
8
9
POST https://api.wlc.nppa.gov.cn/idcard/authentication
Content-Type: application/json
Authorization: Bearer <access_token>

{
"ai": "<游戏标识>",
"name": "张三",
"idNum": "110101199001011234"
}

返回结果:

1
2
3
4
5
6
7
8
9
10
11
{
"errcode": 0,
"errmsg": "OK",
"data": {
"status": 1,
"result": {
"gender": 1,
"birthday": "1990-01-01"
}
}
}
status 含义 对游戏端影响
0 认证中 暂不限制,异步等待回调
1 认证通过(成年人) 无限制
2 认证通过(未成年人) 执行防沉迷策略
3 认证不通过(信息不符) 拦截,引导用户重新提交

4.2 第三方辅助渠道

很多游戏还会接支付宝或微信的实名作为辅助方案——用户在这些平台上已经实名过了,不需要再输一遍身份证

1
2
3
4
5
6
7
# 支付宝实名授权伪代码
def alipay_auth(auth_code: str) -> dict:
resp = alipay_client.request("alipay.user.info.share", {
"auth_code": auth_code,
})
# 支付宝返回加密后的用户实名信息
return decrypt_user_info(resp["user_info"])

五、服务端实现

5.1 认证请求处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import hashlib
import httpx
from datetime import datetime

class RealNameService:
NPPA_URL = "https://api.wlc.nppa.gov.cn/idcard/authentication"

async def authenticate(self, user_id: int, name: str, id_card: str) -> dict:
id_card_sha = hashlib.sha256(id_card.encode()).hexdigest()
id_card_md5 = hashlib.md5(id_card.encode()).hexdigest()

# 1. 查缓存——已经认证过的直接返回历史结果
cached = await self._check_cache(id_card_md5)
if cached:
return cached

# 2. 调国家接口
async with httpx.AsyncClient() as client:
resp = await client.post(
self.NPPA_URL,
json={"ai": APP_ID, "name": name, "idNum": id_card},
headers={"Authorization": f"Bearer {self._get_token()}"},
)
data = resp.json()

# 3. 写数据库
result = self._parse_result(data, name, id_card_sha, id_card_md5)
await self._save_realname(user_id, result)

# 4. 写入策略表
await self._apply_policy(user_id, result)

return result

def _parse_result(self, data: dict, name: str, sha: str, md5: str) -> dict:
return {
"real_name": name,
"id_card_sha256": sha,
"id_card_md5": md5,
"auth_result": data["data"]["status"],
"birthday": data["data"]["result"]["birthday"],
"gender": data["data"]["result"]["gender"],
"auth_time": datetime.now(),
}

5.2 策略执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from datetime import time, datetime, date

class PlayPolicy:
NIGHT_BAN_START = time(22, 0)
NIGHT_BAN_END = time(8, 0)

def should_block(self, user_type: int, cur_played: int, now: datetime) -> str:
"""返回 None 放行,返回字符串表示拦截原因"""

if user_type == 1: # 成年人
return None

# 未成年人:夜间禁玩
if self.NIGHT_BAN_START <= now.time() or now.time() < self.NIGHT_BAN_END:
return "夜间时段(22:00-08:00)无法游戏"

# 未成年人:每日 90 分钟限时
if cur_played >= 90:
return "今日游戏时长已用尽"

return None

def should_limit_recharge(self, user_type: int, monthly_amount: float) -> bool:
if user_type == 1:
return False # 成年人无充值限制
# 未成年人:8-15岁单月不超过200,16-17岁不超过400
return monthly_amount > 200 # 简化处理

5.3 定时重置

防沉迷策略按日重置,cron 任务每天 00:05 执行:

1
2
-- 重置今日已玩时长
UPDATE user_play_policy SET cur_play_today = 0 WHERE update_date < CURDATE();

六、客户端对接

6.1 认证弹窗触发时机

三个触发时机:

场景 操作
首次进入游戏 弹实名认证弹窗,输入姓名+身份证
累计在线 1 小时 弹未成年人提醒,确认继续需二次认证
付费行为触发 检查实名状态,未实名则强制认证

6.2 限时提示

客户端需要定时从服务端拉取策略状态,在倒计时 5 分钟时开始弹提醒:

1
2
3
4
5
6
// 伪代码
func OnPlayTimeWarning(minutesLeft int) {
if minutesLeft <= 5 {
showToast("您今日游戏时长剩余 %d 分钟", minutesLeft)
}
}

七、常见问题

7.1 一个身份证绑多个号

按照合规要求,一个身份证只能绑定一个游戏账号。判重逻辑通过 id_card_md5 唯一索引实现:

1
2
3
4
5
6
async def _check_duplicate(self, id_card_md5: str) -> bool:
existing = await db.fetch_one(
"SELECT user_id FROM user_realname WHERE id_card_md5 = ?",
id_card_md5,
)
return existing is not None

如有特殊情况需要解绑,走人工工单流程,不能开放自助解绑。

7.2 接口超时降级

国家实名接口偶尔会超时或返回繁忙,不能因此让用户无法游戏:

1
2
3
4
5
6
7
8
9
10
async def authenticate_with_timeout(self, user_id, name, id_card):
try:
return await asyncio.wait_for(
self.authenticate(user_id, name, id_card),
timeout=5.0,
)
except (asyncio.TimeoutError, httpx.HTTPStatusError):
# 超时降级:标记为"认证中",暂不限制
await self._save_temp_policy(user_id, temp_pass=True)
return {"auth_result": 0, "message": "认证处理中,请稍后"} # status = 0

注意:降级只是临时措施,后续仍需要通过异步回调或定时任务补偿认证。

7.3 防破解思路

  • 身份证号存摘要,但摘要不可逆,不能用来校验用户输入格式
  • 客户端校验只做格式正则,真实校验全部在服务端
  • 策略执行逻辑必须放服务端,客户端只负责展示和提醒
  • 客户端时间不可信,限时判断依据服务端累计时长

八、总结

步骤 做什么 关键点
收集 客户端弹窗采集姓名+身份证 身份证不存明文,存 SHA256
认证 调国家实名接口或第三方渠道 超时降级 + 异步补偿
判定 根据返回 status 判断成年人/未成年人 判重:一证一号
执行 服务端按策略拦截/放行 限时、禁玩、限充都在服务端
重置 每日 00:00 重置时长 cron 定期刷策略表
提醒 客户端弹窗倒计时 提前 5 分钟提醒

实名接入本身技术难度不大,真正的活儿都在边界情况上:接口超时怎么办、用户想换绑怎么处理、跨天时长怎么重置、黑产怎么防。数据表设计稳了,降级策略想明白了,剩下的就是填代码。

记住一句话:游戏合规不是为了拦用户,是让你的产品能合规地活下去。