获取用户信息
概述
用户完成 TapTap 登录(SDK 登录或扫码登录)后,接入方服务端可以使用登录返回的 Access Token 调用 TapTap OpenAPI,获取该用户的公开信息,包括:
- openid:用户在单个应用内的唯一标识。同一用户在同一应用中的 openid 恒定不变,在不同应用中的 openid 不同。适合作为游戏内的用户标识。
- unionid:用户在同一开发者账号下所有应用内的唯一标识。同一用户在同一开发者的所有游戏中 unionid 相同,在不同开发者的游戏中不同。适合用于同一厂商多个游戏间的数据互通。
- 昵称、头像:用户的公开资料。
调用分两步:
- 使用 Access Token 中的
kid和mac_key签算出 MAC Token,作为Authorization请求头; - 携带该请求头调用用户信息接口。
准备工作:获取 Access Token
Access Token 是用户授权的凭证,来源取决于登录方式:
- SDK 登录:客户端调用
GetCurrentTapAccount方法获取当前有效的 Access Token,发送给接入方服务端。 - 扫码登录:轮询 token 接口的响应即为 Access Token。
Access Token 包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
kid | string | 本次授权凭证的标识符,签算后作为 Authorization 头中 MAC id="..." 的值 |
token_type | string | 凭证类型,如 mac |
mac_key | string | HMAC-SHA1 签算密钥 |
mac_algorithm | string | 签算算法,如 hmac-sha-1 |
scopes | array | 本次登录实际授权的范围,如 basic_info、public_profile,决定可调用的接口,见接口与授权范围 |
kid 和 mac_key 每次登录授权后都会变化,由登录流程动态下发。mac_key 与开发者控制台中固定不变的 Server Secret 是两个不同的值,请勿混用。
关于有效期:
- 单个 Access Token 的最长有效期为 30 天,但也可能提前失效,例如用户注销账号、在 TapTap 客户端解除授权等。
- SDK 在每次应用启动时会自动刷新 Access Token。
- 不建议在服务端或游戏客户端缓存 Access Token;需要使用时,应重新通过 SDK 获取最新值,避免使用已失效的凭证发起请求。
MAC Token 签算
MAC Token 是使用 Access Token 中的 kid 和 mac_key,按 HMAC-SHA1 算法签算生成的 Authorization 请求头,用于 TapTap OpenAPI 的身份验证。
签算步骤
-
构造待签名字符串,共 6 个有内容的部分,每部分以
\n结尾,并在末尾再追加一个\n(因此最后是连续两个\n):{timestamp}\n{nonce}\n{method}\n{uri}\n{host}\n{port}\n\ntimestamp:当前时间戳(秒级)nonce:随机字符串method:HTTP 请求方法(如GET)uri:请求路径,包含 query string(如/account/profile/v1?client_id=xxx)host:请求域名(如open.tapapis.cn)port:端口号(HTTPS 为443)
-
使用
mac_key对待签名字符串进行 HMAC-SHA1 签算,并将结果 Base64 编码,得到mac值。 -
构造
Authorization头:MAC id="{kid}",ts="{timestamp}",nonce="{nonce}",mac="{mac}"
签算校验样例
签算最常见的错误是待签名字符串拼接不对(尤其是结尾的两个 \n)。建议先用下面这组固定输入验证你的签算实现,输出一致后再替换为真实值。
输入(均为演示数据):
| 参数 | 样例值 |
|---|---|
kid | demo_kid |
mac_key | demo_mac_key |
timestamp | 1618221750 |
nonce | adssd |
method | GET |
uri | /account/basic-info/v1?client_id=demo_client_id |
host | open.tapapis.cn |
port | 443 |
待签名字符串(\n 为换行符,注意结尾连续两个):
1618221750\nadssd\nGET\n/account/basic-info/v1?client_id=demo_client_id\nopen.tapapis.cn\n443\n\n
正确的签算结果:
mac = oYWjWxKzPBbeK2xKY7+ADJKAlmE=
Authorization: MAC id="demo_kid",ts="1618221750",nonce="adssd",mac="oYWjWxKzPBbeK2xKY7+ADJKAlmE="
也可以用命令行快速验证:
printf '1618221750\nadssd\nGET\n/account/basic-info/v1?client_id=demo_client_id\nopen.tapapis.cn\n443\n\n' \
| openssl dgst -sha1 -hmac 'demo_mac_key' -binary | base64
# 输出:oYWjWxKzPBbeK2xKY7+ADJKAlmE=
签算示例代码
Node.js 请求示例
const http = require('http');
const https = require('https');
const crypto = require('crypto');
/**
* 以下是配置项:请按实际情况替换参数后进行授权请求。
*/
const client_id = "请替换为控制台的 Client ID";
const kid = "请替换为客户端 SDK 授权成功后,Access Token 中的 kid";
const macKey = "请替换为客户端 SDK 授权成功后,Access Token 中的 mac_key";
/**
* 获取当前账户详细信息的接口。
* 如果需要获取账户基础信息,请替换为以下 URL:
* https://open.tapapis.cn/account/basic-info/v1
*/
const url = "https://open.tapapis.cn/account/profile/v1";
/**
* 主程序逻辑
*/
// 步骤 1:设置请求方法和 URL
const method = 'GET';
const requestUrl = url + '?client_id=' + client_id;
// 步骤 2:生成时间戳和随机数
const ts = Math.floor(Date.now() / 1000).toString().padStart(10, '0');
const nonce = getRandomString(5);
// 步骤 3:创建待签名字符串并生成签名
const parsedUrl = new URL(requestUrl);
const host = parsedUrl.hostname;
const uri = parsedUrl.pathname + parsedUrl.search;
const port = parsedUrl.port || (parsedUrl.protocol === 'https:' ? '443' : '80');
const signingString = buildSigningString(ts, nonce, method, uri, host, port);
const mac = sign(signingString, macKey);
// 步骤 4:生成 Authorization 头
const authorization = `MAC id="${kid}",ts="${ts}",nonce="${nonce}",mac="${mac}"`;
console.log('Authorization: ' + authorization);
// 步骤 5:执行 HTTP 请求并输出结果
const client = parsedUrl.protocol === 'https:' ? https : http;
const req = client.request({
hostname: host,
port: port,
path: uri,
method: method,
headers: { 'Authorization': authorization }
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => { console.log(data); });
});
req.end();
/**
* 生成随机字符串
* @param {number} length 随机字符串的长度
* @returns {string} 随机生成的字符串(仅含大小写字母和数字)
*/
function getRandomString(length) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const bytes = crypto.randomBytes(length);
let result = '';
for (let i = 0; i < length; i++) {
result += chars[bytes[i] % chars.length];
}
return result;
}
/**
* 构造待签名字符串
* @param {string} ts 时间戳
* @param {string} nonce 随机数
* @param {string} method HTTP 方法
* @param {string} uri 请求路径(含 query string)
* @param {string} host 请求域名
* @param {string} port 端口号
* @returns {string} 待签名字符串
*/
function buildSigningString(ts, nonce, method, uri, host, port) {
return `${ts}\n${nonce}\n${method}\n${uri}\n${host}\n${port}\n\n`;
}
/**
* 使用 HMAC-SHA1 生成签名
* @param {string} signingString 待签名字符串
* @param {string} key MAC 密钥
* @returns {string} Base64 编码的签名值
*/
function sign(signingString, key) {
const hmac = crypto.createHmac('sha1', key);
hmac.update(signingString);
return hmac.digest('base64');
}
Java 请求示例
package com.taptap;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Authorization {
public static void main(String[] args) throws IOException {
/**
* 以下是配置项:请按实际情况替换参数后进行授权请求。
*/
String client_id = "请替换为控制台的 Client ID";
String kid = "请替换为客户端 SDK 授权成功后,Access Token 中的 kid";
String mac_key = "请替换为客户端 SDK 授权成功后,Access Token 中的 mac_key";
/**
* 获取当前账户详细信息的接口。
* 如果需要获取账户基础信息,请替换为以下 URL:
* https://open.tapapis.cn/account/basic-info/v1
*/
String url = "https://open.tapapis.cn/account/profile/v1";
/**
* 主程序逻辑
*/
// 步骤 1:设置请求方法和 URL
String method = "GET";
String request_url = url + "?client_id=" + client_id;
// 步骤 2:生成时间戳和随机数
String ts = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000);
String nonce = getRandomString(5);
// 步骤 3:创建待签名字符串并生成签名
URL parsedUrl = new URL(request_url);
String host = parsedUrl.getHost();
String uri = request_url.substring(request_url.lastIndexOf(host) + host.length());
String port = request_url.startsWith("https") ? "443" : "80";
String signingString = buildSigningString(ts, nonce, method, uri, host, port);
String mac = sign(signingString, mac_key);
// 步骤 4:生成 Authorization 头
String authorization = String.format("MAC id=\"%s\",ts=\"%s\",nonce=\"%s\",mac=\"%s\"",
kid, ts, nonce, mac);
System.out.println("Authorization: " + authorization);
// 步骤 5:执行 HTTP 请求并输出结果
HttpURLConnection conn = (HttpURLConnection) new URL(request_url).openConnection();
conn.setRequestProperty("Authorization", authorization);
conn.setRequestMethod(method);
BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
StringBuilder result = new StringBuilder();
while ((line = rd.readLine()) != null) {
result.append(line);
}
rd.close();
System.out.println(result.toString());
}
/**
* 生成随机字符串
* @param length 随机字符串的长度
* @return 随机生成的字符串(仅含大小写字母和数字)
*/
private static String getRandomString(int length) {
String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(chars.charAt(random.nextInt(chars.length())));
}
return sb.toString();
}
/**
* 构造待签名字符串
* @param ts 时间戳
* @param nonce 随机数
* @param method HTTP 方法
* @param uri 请求路径(含 query string)
* @param host 请求域名
* @param port 端口号
* @return 待签名字符串
*/
private static String buildSigningString(String ts, String nonce, String method,
String uri, String host, String port) {
return ts + "\n" + nonce + "\n" + method + "\n" + uri + "\n" + host + "\n" + port + "\n\n";
}
/**
* 使用 HMAC-SHA1 生成签名
* @param signingString 待签名字符串
* @param key MAC 密钥
* @return Base64 编码的签名值
*/
private static String sign(String signingString, String key) {
try {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(signingString.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(rawHmac);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException(e);
}
}
}
PHP 请求示例
<?php
/**
* 以下是配置项:请按实际情况替换参数后进行授权请求。
*/
$client_id = "请替换为控制台的 Client ID";
$kid = "请替换为客户端 SDK 授权成功后,Access Token 中的 kid";
$mac_key = "请替换为客户端 SDK 授权成功后,Access Token 中的 mac_key";
/**
* 获取当前账户详细信息的接口。
* 如果需要获取账户基础信息,请替换为以下 URL:
* https://open.tapapis.cn/account/basic-info/v1
*/
$url = "https://open.tapapis.cn/account/profile/v1";
/**
* 主程序逻辑
*/
// 步骤 1:设置 $method、$request_url
$method = "GET";
$request_url = $url . "?client_id=" . urlencode($client_id);
// 步骤 2:生成时间戳和随机数
$ts = time(); // 秒级当前时间戳
$nonce = randomString(5); // 随机数,至少5位
// 步骤 3:创建待签名字符串并生成签名
$signing_string = createSigningString($request_url, $ts, $nonce, $method);
$mac = sign($signing_string, $mac_key);
// 步骤 4:生成 Authorization 头部信息
$auth = sprintf('MAC id="%s",ts="%s",nonce="%s",mac="%s"', $kid, $ts, $nonce, $mac);
echo "Authorization: " . $auth . PHP_EOL . PHP_EOL;
// 步骤 5:执行 HTTP 请求并输出结果
$headers = array("Authorization: " . $auth);
$response = executeCurlRequest($request_url, $headers, $method);
echo "HTTP Status Code: " . $response['http_code'] . PHP_EOL . PHP_EOL;
if (isset($response['error'])) {
echo "Error: " . $response['error'] . PHP_EOL . PHP_EOL;
}
if (isset($response['body'])) {
echo "Response Body: " . PHP_EOL . json_encode($response['body'], JSON_PRETTY_PRINT) . PHP_EOL;
}
/**
* 创建待签名字符串
*
* @param string $request_url 请求的 URL
* @param int $ts 时间戳
* @param string $nonce 随机数
* @param string $method HTTP 方法
* @return string 待签名字符串
*/
function createSigningString($request_url, $ts, $nonce, $method)
{
$parsed_url = parse_url($request_url);
$uri = $parsed_url['path'] . '?' . $parsed_url['query'];
$domain = $parsed_url['host'];
$port = 443; // 使用 HTTPS 固定端口
return implode("\n", [
$ts,
$nonce,
$method,
$uri,
$domain,
$port,
""
]) . "\n";
}
/**
* 生成签名值
*
* @param string $signing_string 待签名字符串
* @param string $mac_key MAC 密钥
* @return string 生成的签名值
* @example sign('abc', 'def') -> dYTuFEkwcs2NmuhQ4P8JBTgjD4w=
*/
function sign($signing_string, $mac_key)
{
return base64_encode(hash_hmac('sha1', $signing_string, $mac_key, true));
}
/**
* 执行 cURL 请求并返回结果
*
* @param string $url 请求的 URL
* @param array $headers 请求头
* @param string $method HTTP 方法
* @return array 包含状态码和响应内容或错误信息
*/
function executeCurlRequest($url, $headers, $method)
{
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
// 执行请求并捕获响应
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
// 检查是否发生 cURL 错误
if (curl_errno($curl)) {
$error_message = curl_error($curl);
curl_close($curl);
return array(
'http_code' => 0,
'error' => "cURL 错误: " . $error_message,
'body' => json_decode($response, true)
);
}
// 关闭 cURL 句柄
curl_close($curl);
// 解析响应
$parsed_body = json_decode($response, true);
// 根据 HTTP 状态码返回结果
if ($httpCode >= 200 && $httpCode < 300) {
return array(
'http_code' => $httpCode,
'body' => $parsed_body
);
} else {
return array(
'http_code' => $httpCode,
'error' => "HTTP 请求失败,状态码: $httpCode",
'body' => $parsed_body
);
}
}
/**
* 生成随机字符串
*
* @param int $length 随机字符串的长度
* @return string 随机生成的字符串
*/
function randomString($length = 5)
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomIndex = mt_rand(0, $charactersLength - 1);
$randomString .= $characters[$randomIndex];
}
return $randomString;
}
Python3 请求示例
import base64
import hmac
import random
import string
import time
from hashlib import sha1
from urllib.parse import urlparse
from urllib.request import Request, urlopen
#
# 以下是配置项:请按实际情况替换参数后进行授权请求。
#
client_id = "请替换为控制台的 Client ID"
kid = "请替换为客户端 SDK 授权成功后,Access Token 中的 kid"
mac_key = "请替换为客户端 SDK 授权成功后,Access Token 中的 mac_key"
#
# 获取当前账户详细信息的接口。
# 如果需要获取账户基础信息,请替换为以下 URL:
# https://open.tapapis.cn/account/basic-info/v1
#
url = "https://open.tapapis.cn/account/profile/v1"
#
# 主程序逻辑
#
def main():
# 步骤 1:设置请求方法和 URL
method = 'GET'
request_url = f'{url}?client_id={client_id}'
# 步骤 2:生成时间戳和随机数
ts = str(int(time.time()))
nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=5))
# 步骤 3:创建待签名字符串并生成签名
parsed = urlparse(request_url)
signing_string = build_signing_string(
ts, nonce, method, parsed.path + '?' + parsed.query, parsed.hostname, '443')
mac = sign(signing_string, mac_key)
# 步骤 4:生成 Authorization 头
authorization = f'MAC id="{kid}",ts="{ts}",nonce="{nonce}",mac="{mac}"'
print('Authorization:', authorization)
# 步骤 5:执行 HTTP 请求并输出结果
req = Request(request_url, headers={'Authorization': authorization})
with urlopen(req) as response:
print(response.read().decode('UTF-8'))
def build_signing_string(ts, nonce, method, uri, host, port):
"""构造待签名字符串
Args:
ts: 时间戳
nonce: 随机数
method: HTTP 方法
uri: 请求路径(含 query string)
host: 请求域名
port: 端口号
Returns:
待签名字符串
"""
return '\n'.join([ts, nonce, method, uri, host, port, '']) + '\n'
def sign(signing_string, key):
"""使用 HMAC-SHA1 生成签名
Args:
signing_string: 待签名字符串
key: MAC 密钥
Returns:
Base64 编码的签名值
"""
hmac_code = hmac.new(key.encode('UTF-8'), signing_string.encode('UTF-8'), sha1)
return base64.b64encode(hmac_code.digest()).decode('UTF-8')
if __name__ == '__main__':
main()
Go 请求示例
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
func main() {
/*
* 以下是配置项:请按实际情况替换参数后进行授权请求。
*/
clientId := "请替换为控制台的 Client ID"
kid := "请替换为客户端 SDK 授权成功后,Access Token 中的 kid"
macKey := "请替换为客户端 SDK 授权成功后,Access Token 中的 mac_key"
reqHost := "open.tapapis.cn"
/*
* 获取当前账户详细信息的接口。
* 如果需要获取账户基础信息,请将下方路径替换为:
* /account/basic-info/v1
*/
reqURI := "/account/profile/v1?client_id=" + clientId
reqURL := "https://" + reqHost + reqURI
/*
* 主程序逻辑
*/
// 步骤 1:设置请求方法和 URL
method := "GET"
// 步骤 2:生成时间戳和随机数
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce := randomString(5)
// 步骤 3:创建待签名字符串并生成签名
signingString := buildSigningString(timestamp, nonce, method, reqURI, reqHost, "443")
mac := hmacSha1(signingString, macKey)
// 步骤 4:生成 Authorization 头
authorization := fmt.Sprintf(`MAC id="%s",ts="%s",nonce="%s",mac="%s"`, kid, timestamp, nonce, mac)
fmt.Println("Authorization:", authorization)
// 步骤 5:执行 HTTP 请求并输出结果
client := http.Client{}
req, err := http.NewRequest(method, 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))
}
// buildSigningString 构造待签名字符串
//
// 参数说明:
// - ts: 时间戳
// - nonce: 随机数
// - method: HTTP 方法
// - uri: 请求路径(含 query string)
// - host: 请求域名
// - port: 端口号
func buildSigningString(ts, nonce, method, uri, host, port string) string {
return ts + "\n" + nonce + "\n" + method + "\n" + uri + "\n" + host + "\n" + port + "\n\n"
}
// hmacSha1 使用 HMAC-SHA1 生成签名,返回 Base64 编码的签名值
//
// 参数说明:
// - signingString: 待签名字符串
// - key: MAC 密钥
func hmacSha1(signingString, key string) string {
mac := hmac.New(sha1.New, []byte(key))
mac.Write([]byte(signingString))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
// randomString 生成随机字符串
//
// 参数说明:
// - length: 随机字符串的长度
func randomString(length int) string {
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, length)
rand.Read(b)
for i := range b {
b[i] = chars[b[i]%byte(len(chars))]
}
return string(b)
}
C# 请求示例
using System.Collections;
using UnityEngine;
using System;
using System.Net.Http;
using System.Text;
public class TapLoginOAuth : MonoBehaviour
{
/**
* 以下是配置项:请按实际情况替换参数后进行授权请求。
*/
private string clientId = "请替换为控制台的 Client ID";
private string kid = "请替换为客户端 SDK 授权成功后,Access Token 中的 kid";
private string macKey = "请替换为客户端 SDK 授权成功后,Access Token 中的 mac_key";
/**
* 获取当前账户详细信息的接口。
* 如果需要获取账户基础信息,请替换为以下 URL:
* https://open.tapapis.cn/account/basic-info/v1
*/
private string url = "https://open.tapapis.cn/account/profile/v1";
void Start()
{
StartCoroutine(SendRequest());
}
IEnumerator SendRequest()
{
/**
* 主程序逻辑
*/
// 步骤 1:设置请求方法和 URL
string method = "GET";
string requestUrl = url + "?client_id=" + clientId;
// 步骤 2:生成时间戳和随机数
string ts = ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString();
string nonce = GenerateNonce(5);
// 步骤 3:创建待签名字符串并生成签名
var parsedUrl = new Uri(requestUrl);
string host = parsedUrl.Host;
string uri = parsedUrl.PathAndQuery;
string signingString = BuildSigningString(ts, nonce, method, uri, host, "443");
string mac = Sign(signingString, macKey);
// 步骤 4:生成 Authorization 头
string authorization = string.Format("MAC id=\"{0}\",ts=\"{1}\",nonce=\"{2}\",mac=\"{3}\"",
kid, ts, nonce, mac);
Debug.Log("Authorization: " + authorization);
// 步骤 5:执行 HTTP 请求并输出结果
using (var httpClient = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(requestUrl));
request.Headers.Add("Authorization", authorization);
var response = httpClient.SendAsync(request).Result;
var responseBody = response.Content.ReadAsStringAsync().Result;
Debug.Log("Response: " + responseBody);
}
yield return null;
}
/// <summary>
/// 构造待签名字符串
/// </summary>
/// <param name="ts">时间戳</param>
/// <param name="nonce">随机数</param>
/// <param name="method">HTTP 方法</param>
/// <param name="uri">请求路径(含 query string)</param>
/// <param name="host">请求域名</param>
/// <param name="port">端口号</param>
/// <returns>待签名字符串</returns>
string BuildSigningString(string ts, string nonce, string method, string uri, string host, string port)
{
string[] signArray = { ts, nonce, method, uri, host, port, "" };
return string.Join("\n", signArray) + "\n";
}
/// <summary>
/// 使用 HMAC-SHA1 生成签名
/// </summary>
/// <param name="signingString">待签名字符串</param>
/// <param name="key">MAC 密钥</param>
/// <returns>Base64 编码的签名值</returns>
string Sign(string signingString, string key)
{
using (var hmac = new System.Security.Cryptography.HMACSHA1(Encoding.UTF8.GetBytes(key)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signingString));
return Convert.ToBase64String(hash);
}
}
/// <summary>
/// 生成随机字符串
/// </summary>
/// <param name="length">随机字符串的长度</param>
/// <returns>随机生成的字符串</returns>
string GenerateNonce(int length)
{
const string chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
var random = new System.Random();
var nonce = new char[length];
for (int i = 0; i < length; i++)
{
nonce[i] = chars[random.Next(chars.Length)];
}
return new string(nonce);
}
}
C++ 请求示例
#include <iostream>
#include <string>
#include <sstream>
#include <ctime>
#include <random>
#include <curl/curl.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
/**
* 以下是配置项:请按实际情况替换参数后进行授权请求。
*/
const std::string client_id = "请替换为控制台的 Client ID";
const std::string kid = "请替换为客户端 SDK 授权成功后,Access Token 中的 kid";
const std::string mac_key = "请替换为客户端 SDK 授权成功后,Access Token 中的 mac_key";
/**
* 获取当前账户详细信息的接口。
* 如果需要获取账户基础信息,请替换为以下 URL:
* https://open.tapapis.cn/account/basic-info/v1
*/
const std::string url = "https://open.tapapis.cn/account/profile/v1";
// 函数声明
std::string createSigningString(const std::string& request_url, time_t ts,
const std::string& nonce, const std::string& method);
std::string sign(const std::string& signing_string, const std::string& key);
std::string randomString(int length);
std::string base64Encode(const unsigned char* input, unsigned int length);
size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp);
int main()
{
/**
* 主程序逻辑
*/
// 步骤 1:设置 method、request_url
std::string method = "GET";
std::string request_url = url + "?client_id=" + client_id;
// 步骤 2:生成时间戳和随机数
time_t ts = time(nullptr); // 秒级当前时间戳
std::string nonce = randomString(5); // 随机数,至少5位
// 步骤 3:创建待签名字符串并生成签名
std::string signing_string = createSigningString(request_url, ts, nonce, method);
std::string mac = sign(signing_string, mac_key);
// 步骤 4:生成 Authorization 头部信息
std::ostringstream auth_stream;
auth_stream << "MAC id=\"" << kid << "\",ts=\"" << ts
<< "\",nonce=\"" << nonce << "\",mac=\"" << mac << "\"";
std::string auth = auth_stream.str();
std::cout << "Authorization: " << auth << std::endl << std::endl;
// 步骤 5:执行 HTTP 请求并输出结果
CURL* curl = curl_easy_init();
if (!curl) {
std::cerr << "Error: curl 初始化失败" << std::endl;
return 1;
}
std::string response_body;
struct curl_slist* headers = curl_slist_append(nullptr, ("Authorization: " + auth).c_str());
curl_easy_setopt(curl, CURLOPT_URL, request_url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body);
CURLcode ret = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
std::cout << "HTTP Status Code: " << http_code << std::endl << std::endl;
if (ret != CURLE_OK) {
std::cerr << "Error: cURL 错误: " << curl_easy_strerror(ret) << std::endl;
}
std::cout << "Response Body: " << std::endl << response_body << std::endl;
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return 0;
}
/**
* 创建待签名字符串
*
* @param request_url 请求的 URL
* @param ts 时间戳
* @param nonce 随机数
* @param method HTTP 方法
* @return 待签名字符串
*/
std::string createSigningString(const std::string& request_url, time_t ts,
const std::string& nonce, const std::string& method)
{
// 解析 URL:提取域名、路径和查询参数
size_t scheme_end = request_url.find("://");
size_t host_start = (scheme_end != std::string::npos) ? scheme_end + 3 : 0;
size_t path_start = request_url.find('/', host_start);
std::string domain = request_url.substr(host_start, path_start - host_start);
std::string uri = request_url.substr(path_start);
int port = 443; // 使用 HTTPS 固定端口
std::ostringstream oss;
oss << ts << "\n" << nonce << "\n" << method << "\n"
<< uri << "\n" << domain << "\n" << port << "\n\n";
return oss.str();
}
/**
* 生成签名值
*
* @param signing_string 待签名字符串
* @param key MAC 密钥
* @return 生成的签名值
* @example sign("abc", "def") -> dYTuFEkwcs2NmuhQ4P8JBTgjD4w=
*/
std::string sign(const std::string& signing_string, const std::string& key)
{
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digest_len = 0;
HMAC(EVP_sha1(),
key.c_str(), key.length(),
reinterpret_cast<const unsigned char*>(signing_string.c_str()), signing_string.length(),
digest, &digest_len);
return base64Encode(digest, digest_len);
}
/**
* 生成随机字符串
*
* @param length 随机字符串的长度
* @return 随机生成的字符串
*/
std::string randomString(int length)
{
const std::string characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::default_random_engine generator(static_cast<unsigned long>(time(nullptr)));
std::uniform_int_distribution<int> distribution(0, characters.size() - 1);
std::string result;
for (int i = 0; i < length; ++i) {
result += characters[distribution(generator)];
}
return result;
}
/**
* Base64 编码
*
* @param input 输入数据
* @param length 输入数据长度
* @return Base64 编码后的字符串
*/
std::string base64Encode(const unsigned char* input, unsigned int length)
{
int encoded_size = 4 * ((length + 2) / 3);
std::string output(encoded_size + 1, '\0');
EVP_EncodeBlock(reinterpret_cast<unsigned char*>(&output[0]), input, length);
output.resize(encoded_size);
return output;
}
/**
* cURL 写回调函数
*/
size_t writeCallback(void* contents, size_t size, size_t nmemb, void* userp)
{
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
调用接口
接口与授权范围
可调用的接口由登录时申请的授权范围决定,实际授权结果可查看 Access Token 的 scopes 字段:
| 登录时申请的 scope | 可调用的接口 |
|---|---|
basic_info | 获取当前账户基础信息 |
public_profile | 获取当前账户基础信息、获取当前账户详细信息 |
以下接口均为国内示例。当移动端初始化为海外时,登录即为海外,流程不变,将请求域名 open.tapapis.cn 替换为海外域名 open.tapapis.com 即可。
获取当前账户基础信息
GET https://open.tapapis.cn/account/basic-info/v1?client_id=xxx
Authorization mac token
请求参数
| 字段 | 类型 | 说明 |
|---|---|---|
| client_id | string | 该应用的 Client ID,应与约定相同 |
响应参数
| 字段 | 类型 | 说明 |
|---|---|---|
| openid | string | 用户在本应用内的唯一标识,含义见概述 |
| unionid | string | 用户在本开发者所有应用内的唯一标识,含义见概述 |
获取当前账户详细信息
GET https://open.tapapis.cn/account/profile/v1?client_id=xxx
Authorization mac token
请求参数与获取当前账户基础信息相同。
响应参数
| 字段 | 类型 | 说明 |
|---|---|---|
| name | string | 用户名 |
| avatar | string | 用户头像图片地址 |
| openid | string | 用户在本应用内的唯一标识,含义见概述 |
| unionid | string | 用户在本开发者所有应用内的唯一标识,含义见概述 |
请求示例
将签算得到的 Authorization 头原样放入请求(以基础信息接口为例):
curl -H 'Authorization: MAC id="<kid>",ts="<timestamp>",nonce="<nonce>",mac="<mac>"' \
"https://open.tapapis.cn/account/basic-info/v1?client_id=<Client ID>"
错误码
统一格式
| 字段 | 类型 | 说明 |
|---|---|---|
| code | int | 预留字段,用于以后追踪问题 |
| error | string | 错误码,代码逻辑判断时使用 |
| error_description | string | 错误描述信息,开发的时候用来帮助理解和解决发生的错误 |
错误响应
| 错误码 | 说明 | 建议处理 |
|---|---|---|
| invalid_request | 请求缺少必需参数,包含不支持的参数或参数值,或格式不正确 | 检查请求参数后重试 |
| invalid_time | 签算使用的 ts 时间不合法 | 校准本机时间,或请求服务器时间后重新签算 |
| invalid_client | client_id 参数无效 | 检查 client_id 是否与开发者中心一致 |
| access_denied | 凭证无效或已失效(Token 过期、用户注销账号、解除授权等) | 清除本地登录信息,引导用户重新登录,排查步骤见常见问题 |
| forbidden | 用户没有对当前动作的权限 | 不应重复提交该请求;重新身份验证也无法解决 |
| not_found | 请求的资源在服务器上不存在 | 参数相同的情况下不应重复请求 |
| server_error | 服务器出现异常情况 | 稍后重试,建议最多 3 次;仍失败则中断并告知用户 |
| insufficient_scope | 登录时授权的范围不包含所调接口需要的权限 | 对照接口与授权范围调整,详见常见问题 |
常见问题
调用接口返回 access_denied 错误码
按以下顺序排查:
- 签算是否正确:使用签算校验样例验证你的签算实现,输出不一致说明待签名字符串或算法有误。
- Access Token 是否过期:单个 Token 最长有效期 30 天。SDK 在每次启动时会自动刷新 Token;若玩家 30 天内未登录或不活跃,需要引导用户重新登录。
- 用户是否注销或解除授权:玩家在 TapTap 客户端注销账号或解除授权后,原 Token 立即失效。服务端应在校验失败时清除本地登录信息,引导用户重新登录。
另外请确认没有缓存 Access Token——Token 可能随时失效,每次使用时应通过 SDK 获取最新值,见准备工作。
调用接口返回 insufficient_scope 错误码
登录时授权的范围与所调接口需要的权限不匹配导致,例如登录时只申请了 basic_info,服务端却调用了需要 public_profile 的获取当前账户详细信息接口。
对照接口与授权范围检查:登录时申请的 scope(见不同的授权范围)是否覆盖所调用的接口,或改用 basic_info 即可调用的获取当前账户基础信息接口。