[国家新闻出版署]网络游戏防沉迷实名认证系统接口对接实例(Java版)

最近又有游戏要对接网络游戏防沉迷实名认证系统接口,又又搞了我三天两夜才把接口对接完毕,确实难受的一批.其实之前对接过,无奈没有保留代码,导致痛苦的事情又经历一遍,这次总结经验,于是有了这篇文章.

首先记录下(备忘)官方网站地址:网络游戏防沉迷实名认证系统 (nppa.gov.cn)

接着上正戏,因为本人从事的是Java,所有只有Java的实现,不是同行可以先走一步了,本人得意声明:所有代码都是我自己写的,如有雷同不胜荣幸!

1.先看看我的代码目录结构

接着就是每一个类的内容展示(顺序从上至下):

2.接口回调的所有编码,我做成枚举类

java 复制代码
package com.xxx.xxx.api.authentication.enumData;

public enum AuthResponseCode {

    OK(0, "OK", "请求成功"),
    SYS_ERROR(1001, "SYS ERROR", "系统错误"),
    SYS_REQ_RESOURCE_NOT_EXIST(1002, "SYS REQ RESOURCE NOT EXIST", "接口请求的资源不存在"),
    SYS_REQ_METHOD_ERROR(1003, "SYS REQ METHOD ERROR", "接口请求方式错误"),
    SYS_REQ_HEADER_MISS_ERROR(1004, "SYS REQ HEADER MISS ERROR", "接口请求核心参数缺失"),
    SYS_REQ_IP_ERROR(1005, "SYS REQ IP ERROR", "接口请求IP地址非法"),
    SYS_REQ_BUSY_ERROR(1006, "SYS REQ BUSY ERROR", "接口请求超出流量限制"),
    SYS_REQ_EXPIRE_ERROR(1007, "SYS REQ EXPIRE ERROR", "接口请求过期"),
    SYS_REQ_PARTNER_ERROR(1008, "SYS REQ PARTNER ERROR", "接口请求方身份非法"),
    SYS_REQ_PARTNER_AUTH_DISABLE(1009, "SYS REQ PARTNER AUTH DISABLE", "接口请求方权限未启用"),
    SYS_REQ_AUTH_ERROR(1010, "SYS REQ AUTH ERROR", "接口请求方无该接口权限"),
    SYS_REQ_PARTNER_AUTH_ERROR(1011, "SYS REQ PARTNER AUTH ERROR", "接口请求方身份核验错误"),
    SYS_REQ_PARAM_CHECK_ERROR(1012, "SYS REQ PARAM CHECK ERROR", "接口请求报文核验失败");

    private final int code;
    private final String message;
    private final String description;

    AuthResponseCode(int code, String message, String description) {
        this.code = code;
        this.message = message;
        this.description = description;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public String getDescription() {
        return description;
    }

    public static AuthResponseCode fromCode(int code) {
        for (AuthResponseCode responseCode : AuthResponseCode.values()) {
            if (responseCode.getCode() == code) {
                return responseCode;
            }
        }
        throw new IllegalArgumentException("Unknown error code: " + code);
    }
}

3.实体DO类

java 复制代码
package com.xxx.xxx.api.authentication.vo;

import lombok.Data;

@Data
public class AuthResponse<T> {
    private Integer errcode;
    private String errmsg;
    private T data;

}
java 复制代码
package com.xxx.xxx.api.authentication.vo;

public class AuthResponseData {
    private CheckQueryResult result;

    public CheckQueryResult getResult() {
        return result;
    }

    public void setResult(CheckQueryResult result) {
        this.result = result;
    }

    public static class CheckQueryResult {
        private int status;
        private String pi;

        public int getStatus() {
            return status;
        }

        public void setStatus(int status) {
            this.status = status;
        }

        public String getPi() {
            return pi;
        }

        public void setPi(String pi) {
            this.pi = pi;
        }
    }
}
java 复制代码
package com.xxx.xxx.api.authentication.vo;

import com.alibaba.fastjson.annotation.JSONType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@JSONType(orders = {"no", "si", "bt", "ot", "ct", "di", "pi"})
public class LoginoutDO {
    private Integer no;       // 条目编码
    private String si;        // 游戏内部会话标识
    private Integer bt;       // 用户行为类型
    private Long ot;          // 行为发生时间戳,单位秒
    private Integer ct;       // 上报类型
    private String di;        // 设备标识(游客模式下必填)
    private String pi;        // 用户唯一标识(已认证通过用户必填)

    /**
     * 枚举类,表示用户行为类型
     */
    public enum BehaviorType {
        /**
         * 下线
         */
        OFFLINE(0),
        /**
         * 上线
         */
        ONLINE(1);

        private final int value;

        BehaviorType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }

    }

    /**
     * 枚举类,表示用户行为数据上报类型
     */
    public enum ReportType {
        /**
         * 已认证通过用户
         */
        CERTIFIED_USER(0),
        /**
         * 游客用户
         */
        GUEST_USER(2);

        private final int value;

        ReportType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }

    }
}

代码都很简单,我就不一一阐述了,下面有请主菜登场(此处应有掌声👏🏻~)

4.主角登场

java 复制代码
package com.xxx.xxx.api.authentication;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.google.common.collect.Maps;
import com.xxx.xxx.api.authentication.vo.AuthResponse;
import com.xxx.xxx.api.authentication.vo.AuthResponseData;
import com.xxx.xxx.api.authentication.vo.LoginoutDO;
import com.xxx.xxx.exception.AuthenticationRunTimeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * 游戏防沉迷系统接口
 */
@Component
@Slf4j
public class GameIdCardAuth {

    private static final String CHECK_URL = "https://api.wlc.nppa.gov.cn/idcard/authentication/check";
    private static final String QUERY_URL = "http://api2.wlc.nppa.gov.cn/idcard/authentication/query";
    private static final String LOGINOUT_URL = "http://api2.wlc.nppa.gov.cn/behavior/collection/loginout";

    @Value("${lib.IdCardAuth.secret_key}")
    private String SECRET_KEY;

    @Value("${lib.IdCardAuth.app_id}")
    private String APP_ID;

    @Value("${lib.IdCardAuth.biz_id}")
    private String BIZ_ID; // 游戏备案码

    @Autowired
    private RestTemplate restTemplate;


    /**
     * 1.实名认证接口(游戏运营单位调用该接口进行用户实名认证工作,本版本仅支持大陆地区的姓名和二代身份证号核实认证)
     * @param ai 你猜啊
     * @param name 身份证名字
     * @param idNum 身份证号码
     */
    public AuthResponse<AuthResponseData> doPostCheck(String ai, String name, String idNum) {
        long nowTime = System.currentTimeMillis();

        Map<String, Object> paramMap = Maps.newHashMap();
        paramMap.put("ai", ai);
        paramMap.put("name", name);
        paramMap.put("idNum", idNum);

        String content = JSONObject.toJSONString(paramMap);
        byte[] keyBytes = hexStringToByte(SECRET_KEY);
        String encryptStr = aesGcmEncrypt(content, keyBytes);

        log.debug("加密后的内容: {}", encryptStr);

        Map<String, String> signMap = createSignMap(nowTime);
        String sign;
        try {
            sign = generateCheckSignature(signMap, "{\"data\":\"" + encryptStr + "\"}");
            log.debug("签名结果:" + sign);
        } catch (NoSuchAlgorithmException e) {
            throw new AuthenticationRunTimeException("防沉迷doPostCheck签名异常:{}", e);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.add("appId", signMap.get("appId"));
        headers.add("bizId", signMap.get("bizId"));
        headers.add("timestamps", signMap.get("timestamps"));
        headers.add("sign", sign);

        HttpEntity<String> request = new HttpEntity<>("{\"data\":\"" + encryptStr + "\"}", headers);
        ResponseEntity<String> response = restTemplate.postForEntity(CHECK_URL, request, String.class);

        log.debug("响应内容: {}", response.getBody());

        // 将响应数据转换为封装类
        return JSONObject.parseObject(response.getBody(), new TypeReference<AuthResponse<AuthResponseData>>() {
        });
    }

    /**
     * 2.实名认证结果查询接口(追加查询未验证的玩家)
     * @param ai 继续猜啊
     */
    public AuthResponse<AuthResponseData> doGetQuery(String ai) {
        long nowTime = System.currentTimeMillis();
        String params = "ai=" + ai;
        Map<String, String> signMap = createSignMap(nowTime);
        String sign = null;
        try {
            sign = generateQuerySignature(signMap, "ai" + ai);
            log.debug("签名结果:" + sign);
        } catch (NoSuchAlgorithmException e) {
            throw new AuthenticationRunTimeException("防沉迷doGetQuery签名异常:{}", e);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.add("appId", signMap.get("appId"));
        headers.add("bizId", signMap.get("bizId"));
        headers.add("timestamps", signMap.get("timestamps"));
        headers.add("sign", sign);

        HttpEntity<String> entity = new HttpEntity<>(headers);
        ResponseEntity<String> response = restTemplate.exchange(QUERY_URL + "?" + params, HttpMethod.GET, entity, String.class);

        log.debug("响应内容: {}", response.getBody());

        // 将响应数据转换为封装类
        return JSONObject.parseObject(response.getBody(), new TypeReference<AuthResponse<AuthResponseData>>() {
        });
    }

    /**
     * 3.游戏用户行为数据上报接口(上报游戏用户上下线行为数据)
     * @param loginoutDOList 玩家行为
     */
    public AuthResponse<String> doPostLoginout(List<LoginoutDO> loginoutDOList) {
        long nowTime = System.currentTimeMillis();

        Map<String, Object> paramMap = Maps.newHashMap();
        paramMap.put("collections", loginoutDOList);

        String content = JSONObject.toJSONString(paramMap);
        byte[] keyBytes = hexStringToByte(SECRET_KEY);
        String encryptStr = aesGcmEncrypt(content, keyBytes);

        log.debug("加密后的内容: {}", encryptStr);

        Map<String, String> signMap = createSignMap(nowTime);
        String sign;
        try {
            sign = generateLoginoutSignature(signMap, "{\"data\":\"" + encryptStr + "\"}");
            log.debug("签名结果:" + sign);
        } catch (NoSuchAlgorithmException e) {
            throw new AuthenticationRunTimeException("防沉迷loginoutDOList签名异常:{}", e);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.add("appId", signMap.get("appId"));
        headers.add("bizId", signMap.get("bizId"));
        headers.add("timestamps", signMap.get("timestamps"));
        headers.add("sign", sign);

        HttpEntity<String> request = new HttpEntity<>("{\"data\":\"" + encryptStr + "\"}", headers);
        ResponseEntity<String> response = restTemplate.postForEntity(LOGINOUT_URL, request, String.class);

        log.debug("响应内容: {}", response.getBody());

        // 将响应数据转换为封装类
        return JSONObject.parseObject(response.getBody(), new TypeReference<AuthResponse<String>>() {});
    }

    private String generateCheckSignature(Map<String, String> signMap, String content) throws NoSuchAlgorithmException {
        StringBuilder sb = new StringBuilder();
        sb.append(SECRET_KEY);
        for (Map.Entry<String, String> entry : signMap.entrySet()) {
            sb.append(entry.getKey()).append(entry.getValue());
        }
        sb.append(content);
        log.debug("待签名字符串:" + sb);
        return sign(sb.toString());
    }

    private String generateQuerySignature(Map<String, String> signMap, String content) throws NoSuchAlgorithmException {
        StringBuilder sb = new StringBuilder();
        sb.append(SECRET_KEY);
        sb.append(content);
        for (Map.Entry<String, String> entry : signMap.entrySet()) {
            sb.append(entry.getKey()).append(entry.getValue());
        }
        log.debug("待签名字符串:" + sb);
        return sign(sb.toString());
    }

    private String generateLoginoutSignature(Map<String, String> signMap, String content) throws NoSuchAlgorithmException {
        StringBuilder sb = new StringBuilder();
        sb.append(SECRET_KEY);
        for (Map.Entry<String, String> entry : signMap.entrySet()) {
            sb.append(entry.getKey()).append(entry.getValue());
        }
        sb.append(content);
        log.debug("待签名字符串:" + sb);
        return sign(sb.toString());
    }

    private Map<String, String> createSignMap(long nowTime) {
        Map<String, String> signMap = new TreeMap<>();
        signMap.put("appId", APP_ID);
        signMap.put("bizId", BIZ_ID);
        signMap.put("timestamps", String.valueOf(nowTime));
        return signMap;
    }

    private String sign(String toBeSignStr) throws NoSuchAlgorithmException {
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        messageDigest.update(toBeSignStr.getBytes(UTF_8));
        return byteToHexString(messageDigest.digest());
    }

    private static String aesGcmEncrypt(String content, byte[] key) {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
            SecretKeySpec skey = new SecretKeySpec(key, "AES");
            cipher.init(Cipher.ENCRYPT_MODE, skey);
            byte[] ivb = cipher.getIV();
            byte[] encodedByteArray = cipher.doFinal(content.getBytes(UTF_8));
            byte[] message = new byte[ivb.length + encodedByteArray.length];
            System.arraycopy(ivb, 0, message, 0, ivb.length);
            System.arraycopy(encodedByteArray, 0, message, ivb.length, encodedByteArray.length);
            return java.util.Base64.getEncoder().encodeToString(message);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException |
                 BadPaddingException e) {
            return null;
        }
    }

    private static String aesGcmDecrypt(String content, byte[] key) {
        try {
            Cipher decryptCipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
            SecretKeySpec skey = new SecretKeySpec(key, "AES");
            byte[] encodedArrayWithIv = java.util.Base64.getDecoder().decode(content);
            GCMParameterSpec decryptSpec = new GCMParameterSpec(128, encodedArrayWithIv, 0, 12);
            decryptCipher.init(Cipher.DECRYPT_MODE, skey, decryptSpec);
            byte[] b = decryptCipher.doFinal(encodedArrayWithIv, 12, encodedArrayWithIv.length - 12);
            return new String(b, UTF_8);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException |
                 BadPaddingException | InvalidAlgorithmParameterException e) {
            return null;
        }
    }

    private static byte[] hexStringToByte(String str) {
        if (str == null || str.equals("")) {
            return null;
        }
        str = str.toUpperCase();
        int length = str.length() / 2;
        char[] hexChars = str.toCharArray();
        byte[] d = new byte[length];
        for (int i = 0; i < length; i++) {
            int pos = i * 2;
            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
        }
        return d;
    }

    private static byte charToByte(char c) {
        return (byte) "0123456789ABCDEF".indexOf(c);
    }

    private static String byteToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            int hex = b & 0xff;
            String hexStr = Integer.toHexString(hex);
            if (hexStr.length() == 1) {
                hexStr = '0' + hexStr;
            }
            sb.append(hexStr);
        }
        return sb.toString();
    }
}

这里着重讲一下吧,其实接口有四个,有一个玩家消费的接口,在官方文档介绍有提过,但是在接口文档却只有这三个接口,所有就先实现三个吧.

下面就看简单的测试实例:

java 复制代码
package com.xxx.xxx;

import com.zhcj.gums.api.authentication.GameIdCardAuth;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {GumsApplication.class, UnitTest.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UnitTest {

    // @MockBean
    // private NettyServer nettyServer;//排除启动

    @Autowired
    private GameIdCardAuth gameIdCardAuth;

    /**
     * 防沉迷接口测试
     */
    @Test
    public void authentication() {

        System.out.println("实名认证接口");
        System.out.println(gameIdCardAuth.doPostCheck("100000000000000001", "某一一", "110000190101010001"));

        // System.out.println("实名认证结果查询接口");
        // System.out.println(gameIdCardAuth.doGetQuery("100000000000000001"));

        // System.out.println("实名认证接口");
        // LoginoutDO loginoutDO = new LoginoutDO();
        // loginoutDO.setNo(1)
        //         .setSi("1fffbl6st3fbp199i8zh5ggcp84fgo3rj7pn1y")
        //         .setBt(LoginoutDO.BehaviorType.ONLINE.getValue())
        //         .setOt(System.currentTimeMillis() / 1000)
        //         .setCt(LoginoutDO.ReportType.CERTIFIED_USER.getValue())
        //         .setDi("1fffbl6st3fbp199i8zh5ggcp84fgo3rj7pn1y")
        //         .setPi("1fffbl6st3fbp199i8zh5ggcp84fgo3rj7pn1y");
        // System.out.println(gameIdCardAuth.doPostLoginout(Lists.newArrayList(loginoutDO)));
    }
}

嗯,主打一个有手就行~

相关推荐
kinlon.liu5 分钟前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证
王哲晓25 分钟前
Linux通过yum安装Docker
java·linux·docker
java66666888830 分钟前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存31 分钟前
源码分析:LinkedList
java·开发语言
执键行天涯31 分钟前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
Jarlen1 小时前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
蓑 羽1 小时前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
Reese_Cool1 小时前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
严文文-Chris1 小时前
【设计模式-享元】
android·java·设计模式