一、前言
国内游戏上线,有一个环节绕不过去:实名认证与防沉迷。这不是你选不选的问题——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() cached = await self._check_cache(id_card_md5) if cached: return cached 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() result = self._parse_result(data, name, id_card_sha, id_card_md5) await self._save_realname(user_id, result) 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)无法游戏" 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 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": "认证处理中,请稍后"}
|
注意:降级只是临时措施,后续仍需要通过异步回调或定时任务补偿认证。
7.3 防破解思路
- 身份证号存摘要,但摘要不可逆,不能用来校验用户输入格式
- 客户端校验只做格式正则,真实校验全部在服务端
- 策略执行逻辑必须放服务端,客户端只负责展示和提醒
- 客户端时间不可信,限时判断依据服务端累计时长
八、总结
| 步骤 |
做什么 |
关键点 |
| 收集 |
客户端弹窗采集姓名+身份证 |
身份证不存明文,存 SHA256 |
| 认证 |
调国家实名接口或第三方渠道 |
超时降级 + 异步补偿 |
| 判定 |
根据返回 status 判断成年人/未成年人 |
判重:一证一号 |
| 执行 |
服务端按策略拦截/放行 |
限时、禁玩、限充都在服务端 |
| 重置 |
每日 00:00 重置时长 |
cron 定期刷策略表 |
| 提醒 |
客户端弹窗倒计时 |
提前 5 分钟提醒 |
实名接入本身技术难度不大,真正的活儿都在边界情况上:接口超时怎么办、用户想换绑怎么处理、跨天时长怎么重置、黑产怎么防。数据表设计稳了,降级策略想明白了,剩下的就是填代码。
记住一句话:游戏合规不是为了拦用户,是让你的产品能合规地活下去。