跳到主要内容

服务端接入指南

  • 海外域名:https://xdsdk-intnl-6.xd.com
  • 国内域名:https://xdsdk-6.xd.cn

REST API 请求的 Base URL(下文 curl 示例中用 {{host}} 表示)。

URLMETHOD功能
/api/account/v1/user/profileGET登录验证

登录验证

游戏服务端使用客户端获取的 token ,按照下面的方式获取用户信息。

请求方式

请求参数

nameDateTypeOptionsDescription
clientIdstringrequireXD Client ID

请求头

nameDateTypeOptionsDescription
Authorizationstringrequire签名

Authorization 字段的签算过程见 Mac Token 签算 小节。

请求示例

curl --location --request GET '{{host}}/api/account/v1/user/profile?clientId=hn5RcJei2JxCYlS0' \
--header 'Authorization: MAC id="jyUij5PM9iPtJbFS3qteCuA-6kqG6gSA5yDcQ4eZbT8j5LPuwI7lCHYY9d1SoiEtNa6K6YAVhOupEimlMXgYSxl_16950jfvu7O0THSdeOBaE7ugZMsiMXYPh3kJ1UmSJ7uNFmh1Wyul4ZscO7zE_rEPk1Zx_MmQUYUdssDIjqA",ts="1653842818",nonce="gPQVk",mac="GrEgzCPqspvk05K3tPR0mlemlE4="'

返回内容(JSON)

nameDateTypeOptionsDescription
appIdstringnon-nullable应用 id
userIdstringnon-nullable平台用户唯一 id
userCodestringnon-nullable用户唯一 id 编码
usernamestringnon-nullable用户名称
nickNamestringnullable当前登录渠道用户昵称
avatarstringnullable当前登录渠道用户头像
loginTypenumbernon-nullable当前登录渠道
registTimestringnon-nullable注册时间
registIpstringnon-nullable注册 ip
sourcenumbernon-nullable来源(普通用户 :0 迁移用户 :1
loginListarraynon-nullable绑定的登录方式
isGuestbooleannon-nullable是否为游客账户
providerbooleannon-nullable绑定的登录方式
openIdstringnon-nullable当前登录方式对应的openId
userRegionstringnon-nullable用户归属地区
unionIdstringnullable当前登录方式对应的unionId(非必要字段,需要的游戏联系平台添加)

返回示例

{
"appId": "1234",
"userId": "264450023964905472",
"userCode": "eHw+lbhp00",
"username": "Guest4v3LSg",
"nickName": "Guest4v3LSg",
"avatar": "",
"loginType": 0,
"registTime": "2021-09-06T09:49:59.000+00:00",
"registIp": "172.26.132.148",
"source": 0,
"loginList": [
"guest"
],
"isGuest": true,
"provider": [
"guest"
],
"openId":"OsWUscczqGuW3qf5==",
"userRegion":"CN",
"unionId":""
}

登录方式枚举(loginType 字段)

codenamedescription
0guest游客登录
2apple苹果登录
3google谷歌登录
4facebookFacebook 登录
5taptapTap 登录
6lineLine 登录
7twitter推特登录
9twitchTWITCH 登录
10steamSteam 登录
11phone手机号登录

用户归属地区说明(userRegion 字段)

目前仅海外有值,值基于标准的 ISO-3166-1 alpha-2,取值来自cloudflare,cloudflare没给的情况下会设置为DF,相关文档https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-ipcountry。

异常返回

有错误的请求,API 将返回带有相应错误代码的 json 返回。

nameDateTypeOptionsDescription
codenumbernon-nullable异常码
msgstringnon-nullable异常信息
datastringnullable附加信息

示例 :

{
"code": 40300,
"msg": "非法 Access Token",
"data": ""
}

通过 OpenId 查询 XDID

如果游戏自行维护的充值网页、社群登录使用了与 xd sdk 使用了相同的三方社交账户登录(登录参数必须一致),此时可以通过社交账户返回的 open id 查询对应的 xd id。

访问控制

只有白名单 IP 才能访问该接口,使用该接口请前请将调用的服务器出口 IP 提供给 XD SDK 服务端同学。

请求方式

请求参数

nameDateTypeOptionsDescription
clientIdstringrequireXD Client ID
loginTypestringrequire登录方式枚举 (google/apple/facebook)
openIdstringrequire社交账户 openid

请求示例

curl --location 'https://{{host}}/api/account/v1/xd_id?clientId=hn5RcJei2JxCYlS0&loginType=google&openId=109881973448496369394'

返回示例

正常返回

{
"code": 200,
"msg": "ok",
"detail": "",
"data": {
"xdid": "440576949883305984"
}
}

异常返回

{
"code": 40103,
"msg": "Illegal User ID",
"detail": "UNKNOWN: message 40103 user does not exist",
"data": ""
}

详细错误码见 API 错误码

TapTap 绑定邮箱未验证

背景见 用户未验证-taptap-邮箱导致登录失败。该逻辑仅针对用海外 TapTap 登录新建心动账号的玩家。

返回示例:

{
"code":40021,
"msg":"TapTap 绑定邮箱未验证",
"detail":"UNKNOWN: message 40021 error.user-email-not-verified",
"data":{
"email":"unverified@gmail.com",
"loginType":"TapTap"
}
}

第三方登录时邮箱已经存在

该心动账号已绑定用户当前的登录方式,背景见现有心动账号已绑定用户当前的登录方式

返回示例:

{
"code":40902,
"msg":"Current login account's email has already bind current login type",
"detail":"UNKNOWN: message 40902 error.user-email-login-type-conflict",
"data":{
"loginType":"Google",
"conflicts":[
{
"loginType":"Apple",
"userId":"382602976365400064"
},
{
"loginType":"TapTap",
"userId":"382602976365400064"
},
{
"loginType":"Twitter",
"userId":"382602976365400064"
}
],
"email":"1969910954@qq.com"
}
}

多个账号绑定同一个邮箱

背景见 多个心动账号绑定同一邮箱

返回示例:

{
"code":40901,
"msg":"Email conflict can not auto bind by email",
"detail":"UNKNOWN: message 40901 error.user-email-conflict",
"data":{
"loginType":"Google",
"conflicts":[
{
"loginType":"Apple",
"userId":"382602976365400064"
},
{
"loginType":"Taptap",
"userId":"382602976365400065"
},
{
"loginType":"Twitter",
"userId":"382602976365400064"
}
],
"email":"1969910954@qq.com"
}
}

Mac Token 签算步骤

字段说明

字段说明类型示例
kid客户端完成登录操作后可以获取stringXoP8DuN6rr2jTqvYxc2RocBv423***_6FFeVRLtfSBfMKk
mac key客户端完成登录操作后可以获取stringEkKMnZr4y
nonce建议至少 5 位,必须每次随机生成(比如可以从英文字母表及 0-9 随机取几个)string3X0JE
tsunix 时间戳(秒)number1653840089
uri取除去域名以外的完整链接string例如: 请求地址 https://xdsdk-intnl-6.xd.com/api/account/v1/user/profile?clientId=hn5RcJei2JxCYlS0 uri 应为 /api/account/v1/user/profile?clientId=hn5RcJei2JxCYlS0
method请求方式stringPOST 或 GET
host请求域名stringxdsdk-intnl-6.xd.com
port请求端口number443

签算

Step1: 按照上面的说明给各个字段赋值。

Step2: 拼接待签名字符串得到 signBase

signBase = ts + "\n" + nonce + "\n" + method + "\n" + uri + "\n" + host + "\n" + port + "\n"
备注

拼接待签名字符串时,每个字段后面都要跟上换行符。

signBase 示例:

1653841859
Ujbl6K
GET
/api/account/v1/user/profile?clientId=hn5RcJei2JxCYlS0
xdsdk-intnl-6.xd.com
443

Step3: 使用 HmacSHA1signBase 签名 key 即为 mac key, 签名后的二进制使用 Base64 编码得到 mac 字符串。

Step4: 设置请求头 authorization 为:

MAC id={kid},ts={ts},nonce={nonce},mac={mac}

Authorization 字段示例:

Authorization: MAC id="ui8A9zrEFWzGXXOI4t7EFxwwe-r2Jykx6vT13TBBFFdBd9y2aoyxyyOJYa5i9KiQ-CQkufDXckHLPnsStc_hpDvggdSO0Y0BlnlTYX-CebnmCIGde-8sCf2fx0iWHOl3Uruxo-i6-kB82Z5lGrO7PbNSTs_TDD9kLOSSPDth-rY",ts="1653840090",nonce="KjaHSG",mac="aIRcQewElQeP5FhXIGJQTH+eAMg="

签算代码示例

Shell 脚本请求示例
可用此脚本验证直接替换参数,用来验证自己服务端签算的 mac token 是否正确。
CLIENT_ID 替换为心动平台获取的 `XD Client ID`,ACCESS_TOKEN 和 MAC_KEY 为客户端登录成功后的 `token``macKey`

#!/usr/bin/env bash

# XD Client ID
CLIENT_ID="FwFdCIr6u71WQDQwQN"
# SDK 获取的 access_token
ACCESS_TOKEN="ibseFPqcXD1k5PzInDz3E2imQ0I8bXnEFIFkhzkaO8aSrgxvf7V2WJNyWRFpONE5-s5CuFsnGsJ8_u471wch6YJJoS8RiIsm9ftk263_NO5sTASMmIUHzs0a0dTc3oT07FgifwVYQFqnqvHb_z8ILug2Z6vvDYF33OKCXc3sFbU";
# SDK 获取的 mac_key
MAC_KEY="p7xfKpS4x"

# 随机数,正式上线请替换
NONCE="abcleejieun"
# 当前时间戳
TS=$(date +%s)

# 请求方法
METHOD="GET"

# 中国大陆地区请求域名
REQUEST_HOST="xdsdk-6.xd.cn"

#海外请求域名
# REQUEST_HOST=xdsdk-intnl-6.xd.com

# 请求地址 (带 query string)
REQUEST_URI="/api/account/v1/user/profile?clientId=$CLIENT_ID"

MAC=$(printf "%s\n%s\n%s\n%s\n%s\n443\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}" | openssl dgst -binary -sha1 -hmac ${MAC_KEY} | base64)

signBase=$(printf "%s\n%s\n%s\n%s\n%s\n443\n" "${TS}" "${NONCE}" "${METHOD}" "${REQUEST_URI}" "${REQUEST_HOST}")
echo $signBase

echo $MAC

AUTHORIZATION=$(printf 'MAC id="%s",ts="%s",nonce="%s",mac="%s"' "${ACCESS_TOKEN}" "${TS}" "${NONCE}" "${MAC}")

echo $AUTHORIZATION

curl -s -H "Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}"
POSTMAN Pre-request Script
var sdk = require('postman-collection')
var kid = "jWyusMLXFBDp-JLFuE5dpJ8Joqm04N1vtL8q6B7HcPR2FmQsUaaCH2Bi8oca5h0NheXGSfAviVw8t0OYSs5aFEY0NzQWwsF8nImNrDpY6gfcnJm-njISxn5XQe-Bsyx5SF44NxoUH6j3cVntKjU4WOg04op_sl7HJFF00nDkNAk"
var key = "66tmTc6Kh"
var nonce = "3X0JE"
var ts = Date.now() / 1000 | 0
var url = pm.request.url
var method = pm.request.method
var host = url.getHost()
var port = 443
var uri = url.toString().replace("http://", "").replace("https://", "").replace(":", "").substring(host.length)
var signBase = ts + "\n" + nonce + "\n" + method + "\n" + uri + "\n" + host + "\n" + port + "\n"
var authorization =
"MAC id=\"" + kid + "\",ts=\"" + ts + "\",nonce=\"" + nonce + "\",mac=\"" + CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA1(signBase, key)) + "\"";
pm.globals.set("authorization", authorization);
C++ 请求示例
CMAKE
cmake_minimum_required(VERSION 3.17)
project(untitled)

set(CMAKE_CXX_STANDARD 14)

#设置头文件路径
set(INC_DIR /usr/local/include)

#设置链接库路径
set(LINK_DIR /usr/local/lib)

#引入头文件
include_directories(${INC_DIR})

#引入库文件
link_directories(${LINK_DIR})

add_executable(untitled main.cpp)

target_link_libraries(untitled libcrypto.a libssl.a pthread)


------


#include <iostream>
#include <openssl/hmac.h>
#include <string>

using namespace std;

static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";

std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) {
std::string ret;
int i = 0;
int j = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];

while (in_len--) {
char_array_3[i++] = *(bytes_to_encode++);
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;

for(i = 0; (i <4) ; i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}

if (i)
{
for(j = i; j < 3; j++)
char_array_3[j] = '\0';

char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;

for (j = 0; (j < i + 1); j++)
ret += base64_chars[char_array_4[j]];

while((i++ < 3))
ret += '=';

}

return ret;

}


string buildSignature(string & baseString,string &macKey){

unsigned char digest[EVP_MAX_MD_SIZE] = {'\0'};
unsigned int digest_len = 0;
HMAC(EVP_sha1(), macKey.c_str(), macKey.length(), (unsigned char*)baseString.c_str(), baseString.length(), digest, &digest_len);
return base64_encode(digest, digest_len);

}

int main() {

// 从客户端获取的 kid
char kid[] = "Lz9yb9PCDXMwYPbZqUV7Q4TZtftbzvpVB1tN2mTgiX37d1_3OKTGqzO9oYxRnsNHtxwvwXdptvRs_9CVFZUDPxA5RWDrryXL-NZAWMBT4yAWq5dTmnoq6j_p9dxmpcpp6olQjuw_QHAoDkMQvoGcZDN_cKwzP2foAScKbqYDFJM"; // kid
// 从客户端获取的 mac key
string macKey = "AnrGTdc4l"; // mac_key
char nonce[] = "3X0JE"; // 随机字串,建议至少 5 位,必须每次随机生成
time_t ts = time(NULL); // 当前时间戳,秒级
string tsStr = to_string(ts);
string signatureBaseString = "";
signatureBaseString.append(tsStr);
signatureBaseString.append("\n");
signatureBaseString.append(nonce);
signatureBaseString.append("\n");
signatureBaseString.append("GET");
signatureBaseString.append("\n");
signatureBaseString.append("/api/account/v1/user/profile?clientId=hn5RcJei2JxCYlS0");
signatureBaseString.append("\n");
signatureBaseString.append("xdsdk-intnl-6.xd.com");
signatureBaseString.append("\n");
signatureBaseString.append("443");
signatureBaseString.append("\n");

string mac = buildSignature(signatureBaseString, macKey);
cout<<mac<<endl;

string authorization = "";
authorization.append("MAC id=\"").append(kid).append("\",");
authorization.append("ts=\"").append(tsStr).append("\",");
authorization.append("nonce=\"").append(nonce).append("\",");
authorization.append("mac=\"").append(mac).append("\"");

cout<<"Authorization: "<<authorization;

return 0;
}
PHP 请求示例
// 你要请求的资源地址 clientId 为心动平台获取的 XD Client ID
$url = 'http://xdsdk-intnl-6.xd.com/api/account/v1/user/profile?clientId=hn5RcJei2JxCYlS0';

// 从客户端获取的 kid
$kid = '664cee85e5d85fc8645186c64ea11f198370d876'; // kid
// 从客户端获取的 mac key
$mac_key = '744f213aad7ebe0a78fae2e8416d0d1c2323537f'; // mac_key
$nonce = 'aSefW'; // 随机字串,建议至少 5 位,必须每次随机生成
$ts = time(); // 当前时间戳,秒级

$signatureBaseArray = [];
$signatureBaseArray[] = $ts; // 当前时间戳,秒级
$signatureBaseArray[] = $nonce; // 随机字符串
$signatureBaseArray[] = 'GET'; // 请求方式 , GET 或 POST
$signatureBaseArray[] = 'api/account/v1/user/profile?clientId=hn5RcJei2JxCYlS0'; // 完整的 uri
$signatureBaseArray[] = 'xdsdk-intnl-6.xd.com'; // 主机名
$signatureBaseArray[] = 443; // 端口

$signatureBaseString = implode("\n", $signatureBaseArray) . "\n";

$mac = buildSignature($signatureBaseString, $mac_key);

$header = [
"Authorization" => sprintf('MAC id="%s",ts="%d",nonce="%s",mac="%s"', $kid, $ts, $nonce, $mac)
];

$response = Requests::get($url, $header);

/**
* 生成签名
*
* @param $signatureBaseString
* @param $signatureSecret
* @return string
* @example buildSignature('abc', 'def') -> dYTuFEkwcs2NmuhQ4P8JBTgjD4w=
*/
function buildSignature($signatureBaseString, $signatureSecret) {
return base64_encode ( hash_hmac ( 'sha1', $signatureBaseString, $signatureSecret, true ) );
}
Go 请求示例
package main

import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"net/http"
"strconv"
"time"
)

func main() {
// 替换 clientId、kid、macKey 参数
// XD Client ID
clientId := "FwFdCIr6u71WQDQwQN"
// TapTap 登录成功后 XDSDK 返回的 kid
kid := "cgnISKuA8xuUSWV7yJ3D9eeUEt6KOmdtku_RAPMKw-WPFONkTG4zV36eXhHSQZjpqiq9ZncoiNL-gubDo1RCLlMm7V2-Vqgo_PHYl43JzsjK9rtPD8633EXFFyQaAdotLI9NYcsF6f11Xz_5Pn494s8LaSAC3ftBnBXwCHpKtI4"
// TapTap 登录成功后 XDSDK 返回的 macKey
macKey := "kiaB4ol3V"

// 随机数,正式上线请替换
nonce := "abcleejieun"
// 时间戳转换成字符串
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
// 请求 url 相关
reqHost := "xdsdk-6.xd.cn"
reqUri := "/api/account/v1/user/profile?clientId=" + clientId
reqUrl := "https://" + reqHost + reqUri

macStr := timestamp + "\n" + nonce + "\n" + "GET" + "\n" + reqUri + "\n" + reqHost + "\n" + "443" + "\n"
mac := hmacSha(macStr, macKey)
authorization := "MAC id=" + "\"" + kid + "\"" + "," + "ts=" + "\"" + timestamp + "\"" + "," + "nonce=" + "\"" + nonce + "\"" + "," + "mac=" + "\"" + mac + "\""

client := http.Client{}
req, err := http.NewRequest(http.MethodGet, reqUrl, nil)
if err != nil {
fmt.Println(err.Error())
return
}

// 添加请求头
req.Header.Add("Authorization", authorization)
// 发送请求
resp, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
return
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(string(respBody))
}

// HMAC-SHA1 签名
func hmacSha(valStr, keyStr string) string {
key := []byte(keyStr)
mac := hmac.New(sha1.New, key)
mac.Write([]byte(valStr))

// 进行 Base64 编码
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}