服务端接口(登录).v1.1更新时间:2026-03-11 10:35:00

1、登录token校验

  • 功能介绍
    • 小米服务端对游戏客户端访问unionLogin接口获得的serviceToken,进行登录鉴权
    • 到小米服务端上获取玩家的实名状态(是否实名、年龄、性别)和手机号等信息
  • 接口原型

承载协议HTTPS POST
接口方向游戏服务端 -> 小米服务端
接口URLhttps://union-account-gu.s.migames.com/union-account/loginValidate
数据格式请求:Content-Type: application/json
响应:Content-Type: application/json

  • 请求参数
参数类型是否必选描述获取方式
uidStringY传入小米客户端返回的uid客户端unionLogin接口返回的uid
sessionStringY登录会话,颁发过期时间12小时客户端unionLogin接口返回的serviceToken
appIdStringY应用ID小米开平创建游戏时颁发的AppId
nonceStringY随机字符串随机字符串
signatureStringY签名值签名算法以AppSerect作为秘钥,AppSerect由小米开平创建游戏时颁发,详见文档5.1部分,signature本身不参与计算签名
  • 请求示例
curl --request POST \
--url https://union-account-gu.s.migames.com/union-account/loginValidate \
--header 'Content-Type: application/json' \
--header 'User-Agent: insomnia/8.5.1' \
--data '{
"uid": "U2025060311000221",
"session": "B9G584msGAmGzozr",
"appId": "2882303761520416500",
"nonce": "ywb7dsllz4o1",
"signature": "71987ecb773356ed905ba63d0fce702d13015f43"
}'

  • 响应参数

参数类型是否必选描述
retCodeintY业务返回码:200成功。失败请参考 7.服务端错误码
errMsgStringY返回消息
dataStringN加密后的响应结果(使用AppSerect进行加解密,见底下data结果解密)

  • data解密后字段

参数类型是否必选描述
realNameTypeintN用户实名标识
406 非身份证实名方式(港澳台)
407 实名认证通过,已成年
408 实名认证通过,未成年
409 未进行实名认证
ageintN用户年龄
isBellaintN是否女性:
1: 女性
0: 男性
nickNameStringN昵称
avatarUrlStringN头像
phoneNumStringN用户手机号
注意:手机号不可作为账号唯一标识使用,具体可以参考6“服务接入参考指南”部分
uidStringNuid
signatureStringN对data解密后的参数进行签名验证,签名算法以AppSerect作为秘钥,signature本身不参与计算签名,详见文档第四部分

  • 响应示例
{
"retCode":200,
"errMsg":"成功",
"data":"to+uiX2LC7usOEKNjmwYsFuOnOCU+CShGnMw+V2ds2KPLkyrIFMirF9y+uAxNbMZHGyYI
BmeXUgtCsDYQG4E4hj1/XPk/BXevG5IJfqMXtt0cYQe6Yyl3Ca9CCQRV3EP5qDduQaVPC
uEwSXrmfPfh6ZQWXd2JFrMRyOZrYJB32tsYqXB6PgL1HruIXJce8HN6MwLJoVbcPsN+NE
1SlN/kbbSFubQsGc7NOkNfPQc5ZUZbLAc8N38X/iV/AIjbW0lGDW2tDyMvYecOECGsXhx
whW87XkfWGO38jdyWr/q5m+PBF18Mbs9Ywb7Jd6Gku7Wd2gbuYF//w4CghkpYVFTcu/o2
0OL548I0hzrjMB3pyIDZPUO66IzevuLfKOTaHCUDv4OaZtnqdz5zgYg7WutnQ=="
}

*****data解密后********
{
"realNameType":407,
"age":33,
"isBella":1,
"nickName":"手机尾号91的小米账号",
"avatarUrl":"1.jpg",
"phoneNum":"12312312391",
"uid":"U2025060311000221",
"signature":"40d3352df588a2dd5cd45fcb3c464eb91ac9f89b"
}

  • data结果解密

加密/解密算法以AppSerect作为key,使用AesEcbBase64带密钥(secret)的算法对响应结果进行解密

java示例

/**
* aes加密(ECB)
*
* @param sSrc
* @param sKey
* @return
* @throws Exception
*/
public static String encryptAesEcbBase64(String sSrc, String sKey) {
try {
if (StringUtils.isBlank(sKey)) {
return null;
}
byte[] keyBytes = sKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, AES);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(sSrc.getBytes());
return (new Base64()).encodeToString(encrypted);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}

/**
* aes解密(ECB)
*
* @param sSrc
* @param sKey
* @return
*/
public static String decryptAesEcbBase64(String sSrc, String sKey) {
try {
if (StringUtils.isBlank(sKey)) {
return null;
}
byte[] raw = sKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] original = cipher.doFinal((new Base64()).decode(sSrc.getBytes()));
return new String(original);
} catch (Exception ex) {
log.error("decrypt err: " + ex.getMessage(), ex);
return null;
}
}

2、实名信息获取

  • 功能介绍
    • 通过用户授权码换取小米用户实名信息
  • 场景描述
    • 登录完成后,游戏调用游戏服务实名信息授权接口,获取授权码(authCode),通过客户端传递给游戏服务端。游戏服务端调用实名信息获取接口,使用授权码获取实名信息。小米服务端返回加密的实名信息,游戏服务端解密,获取用户真实姓名和身份证号。如果实名信息获取失败,游戏侧需自行定义兜底处理逻辑。
  • 接口原型
承载协议HTTPS POST
接口方向游戏服务端 -> 小米服务端
接口URLhttps://union-account-gu.s.migames.com/union-account/realnameInfo
数据格式请求:Content-Type: application/json
响应:Content-Type: application/json

  • 请求参数

参数类型是否必选描述获取方式
authCodeStringY授权令牌从客户端实名授权接口获取,有效期五分钟,过期需要重新调用端上实名授权接口获取
gameAccountIdStringY游戏侧账号user_id(游戏侧账号主键,要求唯一)游戏侧生成
timestampLongY当前时间戳(防重放)系统生成,13位时间戳
appIdStringY开发者站appId小米开平创建游戏时颁发的AppId
nonceStringY随机字符串随机生成
signatureStringY签名值实名信息涉及用户隐私,故加密强度较高。此处基于ECDH算法,基于小米公钥和游戏侧私钥协商的密钥进行签名,详见5.2

  • 小米公钥
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk4QTqDRudHKt8MdkWjpq3OGzU3rONp+FpGmmEWMbjna1tKJ0N8vb/zp1jhPda56gfYypzauZ9CjYeKk0vxrWJA==

  • 请求示例
{
"authCode": "44dbb3b3fc774898a788f1484de36422",
"appId": "2892303761520167821",
"timestamp": "1769516021347",
"nonce": "bf99f8f295ce4bf8",
"signature": "2297c0d3b22427fec44309e7b6ac49ef4aed5a86",
"gameAccountId": "game_user_001"
}

  • 响应参数

参数类型是否必选描述
retCodeintY返回码,200成功,失败请参考 7.服务端错误码
errMsgStringY错误信息
dataStringN响应结果,基于ECDH加密算法

  • data解密后内容

参数类型是否必选描述
realNameStringN姓名
identityInfoStringN身份证号

  • 响应示例(解密过程参考5.3)
{
"retCode":200,
"errMsg":"成功",
"data":"to+uiX2LC7usOEKNjmwYsFuOnOCU+CShGnMw+V2ds2KPLkyrIFMirF9y+uAxNbMZHGyYI
BmeXUgtCsDYQG4E4hj1/XPk/BXevG5IJfqMXtt0cYQe6Yyl3Ca9CCQRV3EP5qDduQaVPC
uEwSXrmfPfh6ZQWXd2JFrMRyOZrYJB32tsYqXB6PgL1HruIXJce8HN6MwLJoVbcPsN+NE
1SlN/kbbSFubQsGc7NOkNfPQc5ZUZbLAc8N38X/iV/AIjbW0lGDW2tDyMvYecOECGsXhx
whW87XkfWGO38jdyWr/q5m+PBF18Mbs9Ywb7Jd6Gku7Wd2gbuYF//w4CghkpYVFTcu/o2
0OL548I0hzrjMB3pyIDZPUO66IzevuLfKOTaHCUDv4OaZtnqdz5zgYg7WutnQ=="
}

*****data解密后********
{
"realName":"李三",
"identityInfo":"341224199901252456"
}

3、解除账号关联(可选)

  • 功能介绍

  调用此接口可以将游戏官方账号与小米的uid解绑结果上报给小米服务端。

  • 场景描述

  玩家选择小米账号登录游戏时,游戏需要将游戏官方账号与小米uid绑定,并将绑定结果上报给小米游戏服务端。若需将游戏官方账号与小米uid解绑,需将解绑结果上报给小米,游戏服务端则可通过本接口实现。

  • 接口原型
承载协议HTTPS POST
接口方向游戏服务端 -> 小米服务端
接口URLhttps://union-account-gu.s.migames.com/union-account/unbindAccount
数据格式请求:Content-Type: application/json
响应:Content-Type: application/json
  • 请求参数

参数类型是否必选描述获取方式
unionIdStringY传入小米客户端返回的uid客户端unionLogin接口返回的uid
appIdStringY开发者站appId小米开平创建游戏时颁发的AppId
fromStringY来源(固定):content_provider
nonceStringY随机字符串随机字符串
signatureStringY签名值签名算法以AppSerect作为秘钥,AppSerect为小米开平创建游戏时颁发,详见文档5.1部分,signature本身不参与计算签名

  • 请求示例
curl --request POST \
--url https://union-account-gu.s.migames.com/union-account/unbindAccount \
--header 'Content-Type: application/json' \
--data '{
"unionId": "U2025060311000221",
"appId": "2882303761520416500",
"from": "content_provider",
"nonce": "md8s12kxsrnk",
"signature": "800ad5db2a35ed4273d09f8fe0469b9ba07cd693"
}'

  • 响应参数

参数类型是否必选描述
retCodeintY业务返回码:200成功
errMsgStringY返回消息

  • 响应示例
{
"retCode":200,
"errMsg":"成功"
}

4、解除账号关联通知(可选)

  • 功能介绍

  小米游戏服务端调用此接口向游戏服务端发送关键事件通知,游戏收到通知需解绑账号关联关系。

  • 场景描述

  在小米开发者站配置回调地址后,小米服务端将在玩家解除小米账号关联后提醒游戏解绑账号。若游戏服务端返回结果为非成功响应,小米服务端将周期性(最多3次)发送本次关键事件的通知,建议游戏服务端在收到通知后立即返回成功响应,避免堆积通知消息。

  • 接口约束
    • 不允许游戏服务端设置IP白名单,用于限制小米侧的出口IP地址。IP白名单本身并不能提高安全性且会给业务发展带来约束,因不遵守该约定而导致的后果将由游戏自行承担。
    • 必须提前在小米开发者站中配置游戏服务端的回调地址,地址要求必须支持HTTPS协议,且具有合法商用证书。
  • 接口原型

承载协议HTTPS POST
接口方向小米服务端 -> 游戏服务端
最大响应时间< 1s
接口URL在小米开发者站中配置游戏服务端的回调地址URL
数据格式请求:Content-Type: application/json
响应:Content-Type: application/json

  • 请求参数

参数类型是否必选描述获取方式
unionIdStringN传入小米客户端返回的uid小米客户端unionLogin接口返回的uid
appIdStringY开发者站appId小米开平创建游戏时颁发的AppId
nonceStringY随机字符串随机字符串
signatureStringY签名值签名算法以AppSerect作为秘钥,AppSerect为小米开平创建游戏时颁发,详见文档5.1部分,signature本身不参与计算签名

  • 请求示例
curl --request POST \
--url {接口URL} \
--header 'Content-Type: application/json' \
--data '{
"unionId": "U2025060311000221",
"appId": "2882303761520416500",
"nonce": "md8s12kxsrnk",
"signature": "800ad5db2a35ed4273d09f8fe0469b9ba07cd693"
}'

  • 响应参数

参数类型是否必选描述
retCodeintY业务返回码:200成功
errMsgStringY返回消息

  • 响应示例
{
"retCode":200,
"errMsg":"成功"
}

5、签名生成与验证

5.1 AppSerect验签机制

验签步骤说明

步骤一:生成待签名字符串

  • 各请求参数按字母顺序排序(不包含signature),若首字母相同,按后一个字母排序,依次类推(值为null的参数请不要参与签名)
  • 排序后拼接成par1=val1&par2=val2&par3=val3的格式,所生成的字符串即为待签名的字符串

步骤二:生成签名串

  • 通过开平获取签名需要的AppSerect,作为密钥
  • 使用hmac-sha1带密钥(secret)的哈希算法对待签字符串进行签名计算。

JAVA代码示例

package com.xiaomi.migc.union.utils;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;


public class EncryptUtil1 {

private static final String MAC_NAME = "HmacSHA1";
private static final String ENCODING = "UTF-8";

/**
* 使用 HMAC-SHA1 签名方法对对encryptText进行签名
*
* @param encryptText 被签名的字符串
* @param encryptKey 密钥
* @return 返回被加密后的字符串
* @throws Exception
*/
public static String hmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception {
byte[] data = encryptKey.getBytes(ENCODING);
// 根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称
SecretKey secretKey = new SecretKeySpec(data, MAC_NAME);
// 生成一个指定 Mac 算法 的 Mac 对象
Mac mac = Mac.getInstance(MAC_NAME);
// 用给定密钥初始化 Mac 对象
mac.init(secretKey);
byte[] text = encryptText.getBytes(ENCODING);
// 完成 Mac 操作
byte[] digest = mac.doFinal(text);
StringBuilder sBuilder = bytesToHexString(digest);
return sBuilder.toString();
}

/**
* 转换成Hex
* @param bytesArray
*/
private static StringBuilder bytesToHexString(byte[] bytesArray) {
if (bytesArray == null) {
return null;
}
StringBuilder sBuilder = new StringBuilder();
for (byte b : bytesArray) {
String hv = String.format("%02x", b);
// String hv = Integer.toHexString( b & 0xFF );
sBuilder.append(hv);
}
return sBuilder;
}

public static void main(String []args) throws Exception {
// 请求参数排序appId=2882303761520419939&nonce=PM3ENXm5canqCoCtC8fa4VYLVeSiySwF&session=G0jFAavU7ZsP00hN&uid=2025060707269734
String hmac = hmacSHA1Encrypt("原文", "你的AppSecret");
System.out.println(hmac);
assert hmac.equals("3d80601ef8764dbaef2854d412ff0c193dddb9be");
}
}

5.2 实名信息获取接口ECDH签名机制

红色部分有别于AppSerect验签,其他部分相同
验签步骤说明

步骤一:生成待签名字符串

  • 各请求参数按字母顺序排序(不包含signature),若首字母相同,按后一个字母排序,依次类推(值为null的参数请不要参与签名)
  • 排序后拼接成par1=val1&par2=val2&par3=val3的格式,所生成的字符串即为待签名的字符串

步骤二:生成签名串

  • 使用自身私钥和小米提供的公钥使用ECDHUtil协商共享密钥sharedSecret
  • 使用共享密钥的 Base64 字符串作为签名密钥
  • 使用hmac-sha1带密钥(secret)的哈希算法对待签字符串进行签名计算

JAVA代码示例

  import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;

public class RealNameClient {

private static final String API_URL = "https://union-account-gu.s.migames.com/union-account/realnameInfo";
private static final String APP_ID = "你的AppId";
private static final String CP_PRIVATE_KEY = "你的CP端私钥(Base64)";
private static final String SERVER_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEk4QTqDRudHKt8MdkWjpq3OGzU3rONp+FpGmmEWMbjna1tKJ0N8vb/zp1jhPda56gfYypzauZ9CjYeKk0vxrWJA==";

public static void main(String[] args) {
try {
// 从SDK获取的授权码
String authCode = "44dbb3b3fc774898a788f1484de36422";
// 游戏账号ID
String gameAccountId = "game_user_001";

// 1. 准备请求参数
Map<String, String> params = new HashMap<>();
params.put("appId", APP_ID);
params.put("authCode", authCode);
params.put("gameAccountId", gameAccountId);
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
params.put("nonce", UUID.randomUUID().toString().replace("-", ""));

// 2. 生成签名
String signature = generateSignature(params, CP_PRIVATE_KEY, SERVER_PUBLIC_KEY);
params.put("signature", signature);

// 3. 发送HTTP请求
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(API_URL);
httpPost.setHeader("Content-Type", "application/json");
JSONObject jsonParams = new JSONObject(params);
httpPost.setEntity(new StringEntity(jsonParams.toJSONString(), "UTF-8"));

String response = EntityUtils.toString(httpClient.execute(httpPost).getEntity());
httpClient.close();

// 4. 解析响应
JSONObject responseObj = JSONObject.parseObject(response);
int retCode = responseObj.getIntValue("retCode");
String errMsg = responseObj.getString("errMsg");

if (retCode != 200) {
System.out.println("请求失败: " + retCode + " - " + errMsg);
return;
}

// 5. 解密实名信息
String encryptedData = responseObj.getString("data");
JSONObject realNameInfo = RealNameDecryptUtil.decryptRealNameInfo(
encryptedData, CP_PRIVATE_KEY, SERVER_PUBLIC_KEY);

// 6. 使用实名信息
System.out.println("姓名: " + realNameInfo.getString("realName"));
System.out.println("身份证号: " + realNameInfo.getString("identityInfo"));

} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 生成签名
*/
private static String generateSignature(Map<String, String> params,
String cpPrivateKey,
String serverPublicKey) throws Exception {
// 1. 按字典序排序参数
TreeMap<String, String> sortedParams = new TreeMap<>(params);

// 2. 拼接参数字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
String data = sb.toString();

// 3. 协商共享密钥
SecretKey sharedSecret = RealNameDecryptUtil.generateSharedSecret(cpPrivateKey, serverPublicKey);

// 4. 使用共享密钥的Base64字符串作为HMAC密钥
String sharedSecretBase64 = Base64.getEncoder().encodeToString(sharedSecret.getEncoded());

// 5. 使用HMAC-SHA1计算签名
SecretKeySpec signingKey = new SecretKeySpec(
sharedSecretBase64.getBytes("UTF-8"), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes("UTF-8"));

// 6. 转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : rawHmac) {
String hex = String.format("%02x", b);
hexString.append(hex);
}

return hexString.toString();
}
}

5.3 实名信息ECDH解密机制

解密步骤说明:
步骤1:以CP端私钥和小米服务端公钥通过ECDH算法协商出的共享密钥
步骤2:使用AES/ECB/PKCS5Padding带密钥(共享密钥)的算法对响应结果进行解密
JAVA代码示例:

public class RealNameDecryptUtil {
/**
* 解密实名信息
*
* @param encryptedData 加密的实名信息(Base64)
* @param cpPrivateKeyBase64 CP端私钥(Base64)
* @param serverPublicKeyBase64 小米服务端公钥(Base64)
* @return 解密后的实名信息
*/
public static JSONObject decryptRealNameInfo(String encryptedData, String cpPrivateKeyBase64, String serverPublicKeyBase64) throws Exception {
// 1. 协商共享密钥
SecretKey sharedSecret = generateSharedSecret(cpPrivateKeyBase64, serverPublicKeyBase64);
// 2. 使用共享密钥解密数据
String decryptedJson = decrypt(encryptedData, sharedSecret);
// 3. 解析JSON
return JSONObject.parseObject(decryptedJson);
}

/**
* 使用ECDH算法协商共享密钥
*/
private static SecretKey generateSharedSecret(String cpPrivateKeyBase64, String serverPublicKeyBase64) throws Exception {
// 将CP端私钥从Base64转换为PrivateKey对象
byte[] privateKeyBytes = Base64.getDecoder().decode(cpPrivateKeyBase64);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey cpPrivateKey = keyFactory.generatePrivate(privateKeySpec);
// 将服务端公钥从Base64转换为PublicKey对象
byte[] publicKeyBytes = Base64.getDecoder().decode(serverPublicKeyBase64);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
PublicKey serverPublicKey = keyFactory.generatePublic(publicKeySpec);
// 使用ECDH算法协商共享密钥
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
keyAgreement.init(cpPrivateKey);
keyAgreement.doPhase(serverPublicKey, true);
byte[] sharedSecret = keyAgreement.generateSecret();
// 使用SHA-256对共享密钥进行哈希,得到256位的AES密钥
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] aesKeyBytes = sha256.digest(sharedSecret);
// 创建AES密钥
return new SecretKeySpec(aesKeyBytes, "AES");
}

/**
* 使用AES解密数据
*/
private static String decrypt(String encryptedData, SecretKey sharedSecret) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, sharedSecret);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, "UTF-8");
}
}

5.4 工具类代码

AppSerect签名代码:

      
package com.xiaomi.migc.union.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

@Slf4j
public class EncryptExampleUtil {

private static final String AES = "AES";

private static final String MAC_NAME = "HmacSHA1";

/**
* 使用 HMAC-SHA1 签名方法对对encryptText进行签名
*
* @param encryptText 被签名的字符串
* @param encryptKey 密钥
* @return 返回被加密后的字符串
* @throws Exception
*/
public static String hmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception {
byte[] data = encryptKey.getBytes(StandardCharsets.UTF_8);
// 根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称
SecretKey secretKey = new SecretKeySpec(data, MAC_NAME);
// 生成一个指定 Mac 算法 的 Mac 对象
Mac mac = Mac.getInstance(MAC_NAME);
// 用给定密钥初始化 Mac 对象
mac.init(secretKey);
byte[] text = encryptText.getBytes(StandardCharsets.UTF_8);
// 完成 Mac 操作
byte[] digest = mac.doFinal(text);
StringBuilder sBuilder = bytesToHexString(digest);
return sBuilder.toString();
}

/**
* 转换成Hex
*
* @param bytesArray
*/
public static StringBuilder bytesToHexString(byte[] bytesArray) {
if (bytesArray == null) {
return null;
}
StringBuilder sBuilder = new StringBuilder();
for (byte b : bytesArray) {
String hv = String.format("%02x", b);
// String hv = Integer.toHexString( b & 0xFF );
sBuilder.append(hv);
}
return sBuilder;
}


/**
* aes加密(ECB)
*
* @param sSrc
* @param sKey
* @return
* @throws Exception
*/
public static String encryptAesEcbBase64(String sSrc, String sKey) {

try {
if (StringUtils.isBlank(sKey)) {
return null;
}
byte[] keyBytes = sKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, AES);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(sSrc.getBytes());
return (new Base64()).encodeToString(encrypted);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}

/**
* aes解密(ECB)
*
* @param sSrc
* @param sKey
* @return
*/
public static String decryptAesEcbBase64(String sSrc, String sKey) {
try {
if (StringUtils.isBlank(sKey)) {
return null;
}
byte[] raw = sKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] original = cipher.doFinal((new Base64()).decode(sSrc.getBytes()));
return new String(original);
} catch (Exception ex) {
log.error("decrypt err: " + ex.getMessage(), ex);
return null;
}
}

}

ECDH公私钥生成代码

      
import java.security.*;
import java.security.spec.ECGenParameterSpec;

public class GenerateCPKeys {
public static void main(String[] args) throws Exception {
// 生成 ECDH 密钥对
KeyPair keyPair = generateKeyPair();

// 获取公钥(Base64)
String publicKey = publicKeyToBase64(keyPair.getPublic());
System.out.println("CP 端公钥(需要提供给小米游戏对接):");
System.out.println(publicKey);
System.out.println();

// 获取私钥(Base64)
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
String privateKey = base64Encode(privateKeyBytes);
System.out.println("CP 端私钥(严格保密,存储在游戏服务端):");
System.out.println(privateKey);
System.out.println();

System.out.println("请将以上密钥配置到相应位置:");
System.out.println("- CP 端公钥:提供给小米游戏对接时使用");
System.out.println("- CP 端私钥:存储在游戏服务端配置文件中,严格保密");
}

/**
* 生成 ECDH 密钥对
*/
private static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
keyPairGenerator.initialize(ecSpec, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}

/**
* 将公钥转换为 Base64 字符串
*/
private static String publicKeyToBase64(PublicKey publicKey) {
byte[] publicKeyBytes = publicKey.getEncoded();
return base64Encode(publicKeyBytes);
}

/**
* Base64 编码
*/
private static String base64Encode(byte[] bytes) {
return java.util.Base64.getEncoder().encodeToString(bytes);
}
}

6、服务接入参考指南

手机号不可作为账号唯一标识使用:因存在用户手机号未绑定、未授权、用户手机号变更等场景,所以无法100%获取到手机号。为了保证接入准确性,小米账号登录时需遵循三方账号授权模式设计,以小米uid作为三方账号ID与游戏账号进行绑定,而非直接使用手机号进行游戏账号创建。

  • 具体参考流程图和数据模型:

小米账号登录场景下,游戏获取到uid和手机号处理流程说明

首次登录场景


1.基于小米uid创建游戏账号

  • 若存在手机号,则补充到游戏账号
  • 若不存在手机号,则引导用户填写手机号

2. 建立小米uid和游戏user_id的绑定关系

3. 登录成功

第二次登录场景

  1. 通过小米uid和游戏user_id的绑定关系找到游戏账号
  2. 登录成功

  • 数据模型指导

-- 核心账号表:手机号为主键之一,但允许变更历史
CREATE TABLE users (
user_id BIGINT PRIMARY KEY, -- 游戏侧系统内部唯一ID(永不改变)
current_phone VARCHAR(20) UNIQUE, -- 当前生效手机号
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255),status TINYINT DEFAULT 1,
created_at TIMESTAMP);
)


CREATE TABLE user_oauth_bindings (
binding_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL, -- 身份主键
provider VARCHAR(50) NOT NULL, -- 小米
provider_user_id VARCHAR(255) NOT NULL, -- 三方平台的唯一用户ID
provider_phone VARCHAR(20), -- 授权时获取的手机号
bound_at TIMESTAMP,
last_login_at TIMESTAMP,UNIQUE(provider, provider_user_id)
)

7、服务端错误码

7.1 错误处理最佳实践

7.1.1 错误处理建议

  1. 实现完善的日志记录,记录请求参数、错误码、错误信息
  2. 对于频繁出现的错误,应设置监控告警
  3. 对于1560获取实名信息错误,应有兜底方案(如引导用户在游戏内完成实名)
  4. 对于签名验证失败(1015),应检查密钥配置和签名算法实现

7.1.2 重试策略建议

可重试的错误码:

  • 1001(系统错误)
  • 1002(数据库错误)
  • 10001(服务限流)

不可重试的错误码:

  • 1011(参数错误)- 需要修正参数
  • 1015(签名验证失败)- 需要修正签名算法
  • 1556(授权码不存在)- 需要重新获取授权码
  • 1557(授权码已过期)- 需要重新获取授权码
  • 4002(token过期)- 需要重新登录

7.2 错误码表

错误码错误码含义建议的处理方式
通用错误码
1001系统错误系统内部异常,建议稍后重试,持续失败请联系技术支持
1009AES加密错误加密或解密失败,请检查加密密钥配置是否正确
1011参数错误入参格式不正确,请检查请求参数是否符合接口规范
1015签名验证失败签名计算错误,请检查签名算法和密钥是否正确
1546app验证失败app信息验证失败,请检查AppId是否正确
1552联合登录开发者配置未配置开发者配置不存在,请联系技术支持
10001当前服务器繁忙,请稍后再试触发小米侧接口限流,建议稍后重试
登录token校验接口错误码
4002serviceToken过期session已过期(有效期12小时),请引导用户重新登录
实名信息获取错误码
1015签名验证失败ECDH签名验证失败,请检查:
1. CP端私钥是否正确
2. 小米公钥是否使用文档提供的最新版本
3. 签名算法是否按照5.2章节实现
1555未配置实名授权,请联系对接技术支持游戏未开启实名授权功能,请联系技术支持开通
1556授权码不存在authCode不存在或已被使用,请重新调用客户端实名授权接口获取新的授权码
1557授权码已过期authCode已过期(有效期5分钟),请重新调用客户端实名授权接口获取新的授权码
1558实名授权状态错误用户未完成实名授权或授权已取消,请引导用户重新授权
1559实名授权的游戏内ID不一致gameAccountId与授权时的不一致,可能存在越权访问,请检查gameAccountId是否正确
1560获取实名信息错误从实名系统获取用户实名信息失败,可能原因:
1. 用户未完成实名认证
2. 实名系统异常
建议:游戏侧兜底,自行实名
2003请求重复,请勿重复提交检测到重复请求,请避免短时间内重复提交相同请求
2004请求过于频繁,请稍后再试触发频率限制,建议:
1. 降低请求频率
2. 实现客户端缓存机制
3. 避免用户频繁操作
2005防重放校验失败timestamp超出允许的时间窗口(±5秒),请检查:
1. 客户端时间是否准确
2. timestamp是否为13位毫秒时间戳
解除账号关联错误码
2002用户不存在unionId不存在,请检查unionId是否正确

文档内容是否有帮助?
有帮助
无帮助