跳到主要内容
版本:v4

TapTap OAuth 接口

概述

TapTap OpenAPI 采用统一的 Mac Token 头部签算来传递用户授权信息。

开发者接入 SDK 登录模块,用户授权应用后,将会生成一个访问令牌(Access Token)。该令牌将用于做 Mac 签算,生成 Authorization 头,用于身份验证和安全通信。单个 Access Token 的最长有效期为 30 天。SDK 在每次应用启动时,会自动刷新 Access Token,确保用户会话的持续有效性。 游戏移动端可通过 GetCurrentTapAccount 方法获取当前有效的 Access Token,该方法始终返回最新且有效的令牌。移动端获取的 Access Token 需上传至游戏服务器,用作与 TapTap 服务器进行后续通讯的标识。

Mac Token 生成算法见文档中的 MAC Token 算法 部分。

以下接口均为国内示例。当移动端初始化为海外时,登录即为海外,以下服务端文档流程不变,将示例中的请求域名 open.tapapis.cn 更换为海外域名 open.tapapis.com 即可。

流程

  1. 移动端用 SDK 的 TapTap 登录,可以 获取 AccessToken,里面包含:

    public String kid;
    public String token_type;
    public String mac_key;
    public String mac_algorithm;
    public Set<String> scopeSet;
  2. 再把移动端获取的参数发到游戏服务器,服务端签算 mac token。

  3. 请求 https://open.tapapis.cn/account/profile/v1 , header 携带 mac token

API

当 SDK 只请求 basic_info 的权限时,请使用基础信息接口,请求 public_profile 时,请使用详细信息接口。

获取当前账户基础信息

GET https://open.tapapis.cn/account/basic-info/v1?client_id=xxx
Authorization mac token

请求参数

字段类型说明
client_idstring该应用的 Client ID,应与约定相同

响应参数

字段类型说明
openidstring授权用户唯一标识,每个玩家在每个游戏中的 openid 都是不一样的,同一游戏获取同一玩家的 openid 总是相同
unionidstring授权用户唯一标识,一个玩家在一个厂商的所有游戏中 unionid 都是一样的,不同厂商 unionid 不同

请求示例

替换其中的 MAC idClient ID 为自己签算的 mac token 和控制台的 Client ID

curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa
gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs
KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://open.tapapis.cn/account/basic-info/v1?client_id=<Client ID>"

获取当前账户详细信息

GET https://open.tapapis.cn/account/profile/v1?client_id=xxx
Authorization mac token

请求参数

字段类型说明
client_idstring该应用的 Client ID,应与约定相同

响应参数

字段类型说明
namestring用户名
avatarstring用户头像图片地址
openidstring授权用户唯一标识,每个玩家在每个游戏中的 openid 都是不一样的,同一游戏获取同一玩家的 openid 总是相同
unionidstring授权用户唯一标识,一个玩家在一个厂商的所有游戏中 unionid 都是一样的,不同厂商 unionid 不同

请求示例

替换其中的 MAC idClient ID 为自己签算的 mac token 和控制台的 Client ID

curl -s -H 'Authorization:MAC id="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJa
gCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTs
KQ",ts="1618221750",nonce="adssd",mac="XWTPmq6A6LzgK8BbNDwj+kE4gzs="' "https://open.tapapis.cn/account/profile/v1?client_id=<Client ID>"

其他

MAC Token 算法

MAC Token 包含以下字段:

字段类型说明
kidstringmac_key id, The key identifier.
token_typestringToken 类型,如 mac
mac_keystringmac 密钥
mac_algorithmstringmac 计算的算法名称 hmac-sha-1

使用 Mac Token 签算一个接口:

Node.js 请求示例
const http = require('http');
const https = require('https');
const crypto = require('crypto');

function getAuthorization(requestUrl, method, keyId, macKey) {
const url = new URL(requestUrl);
const time = Math.floor(Date.now() / 1000).toString().padStart(10, '0');
const randomStr = getRandomString(16);
const host = url.hostname;
const uri = url.pathname + url.search;
const port = url.port || (url.protocol === 'https:' ? '443' : '80');
const other = '';
const sign = signData(mergeData(time, randomStr, method, uri, host, port, other), macKey);

return `MAC id="${keyId}", ts="${time}", nonce="${randomStr}", mac="${sign}"`;
}

function getRandomString(length) {
return crypto.randomBytes(length).toString('base64');
}

function mergeData(time, randomCode, httpType, uri, domain, port, other) {
let prefix =
`${time}\n${randomCode}\n${httpType}\n${uri}\n${domain}\n${port}\n`;

if (!other) {
prefix += '\n';
} else {
prefix += `${other}\n`;
}

return prefix;
}

function signData(signatureBaseString, key) {
const hmac = crypto.createHmac('sha1', key);
hmac.update(signatureBaseString);
return hmac.digest('base64');
}

const client_id = "hskc**********kklm";
const keyId = "1/VLDoiGUhNCIpUq827L**************zAJ-i8hT_w9vuPtPgdaPkWDv6K4eVe_yZnKz************EYep-T4ki5w3kyYACVnM61JJqDEKfpNnHoTZU********************iUArkgPsWEwOpZGxva7FnqbTwmpLT0a28UtiR5gyr4XXutbnE5tb4A-iSqRpqqtgABXBZd34U5Th3iJ1C666iYQFvuQL9uC-Zv7-xKCNjyPonBqU4ZWZnKLFf2mzprU5vJCA8q5by1SZxY63kZBQieHYxFjyOCQdJ-25gDlxiqDbNq08kmSdY6TB1qtQ68V37L6a8nIzyVHooX9uc2Yw";
const macKey = 'VPDalRmxtBqi******************tH937GNKIvj3';
const requestUrl = 'https://open.tapapis.cn/account/profile/v1?client_id='+ client_id ;
const method = 'GET';


const authorization = getAuthorization(requestUrl, method, keyId, macKey);
console.log(authorization);

const options = new URL(requestUrl);
const client = options.protocol === 'https:' ? https : http;

const req = client.request({
hostname: options.hostname,
port: options.port,
path: options.pathname + options.search,
method: 'GET',
headers: {
'Authorization': authorization
}
}, (res) => {
let data = '';

res.on('data', (chunk) => {
data += chunk;
});

res.on('end', () => {
console.log(data);
});
});

req.end();


Java 请求示例
package com.taptap;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
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 = "0RiAlMny7jiz086FaU";
String kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"; // kid
String mac_key = "mSUQNYUGRBPXyRyW"; // mac_key
String method = "GET";
String request_url = "https://open.tapapis.cn/account/profile/v1?client_id=" + client_id; //
String authorization = getAuthorization(request_url, method, kid, mac_key);
System.out.println(authorization);
URL url = new URL(request_url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// Http
conn.setRequestProperty("Authorization", authorization);
conn.setRequestMethod("GET");
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 request_url
* @param method "GET" or "POST"
* @param key_id key id by OAuth 2.0
* @param mac_key mac key by OAuth 2.0
* @return authorization string
*/
public static String getAuthorization(String request_url, String method, String key_id, String
mac_key) {
try {
URL url = new URL(request_url);
String time = String.format(Locale.US, "%010d", System.currentTimeMillis() / 1000);
String randomStr = getRandomString(16);
String host = url.getHost();
String uri = request_url.substring(request_url.lastIndexOf(host) + host.length());
String port = "80";
if (request_url.startsWith("https")) {
port = "443";
}
String other = "";
String sign = sign(mergeSign(time, randomStr, method, uri, host, port, other), mac_key);
return "MAC " + getAuthorizationParam("id", key_id) + "," + getAuthorizationParam("ts", time)
+ "," + getAuthorizationParam("nonce", randomStr) + "," + getAuthorizationParam("mac",
sign);
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
private static String getRandomString(int length) {
byte[] bytes = new byte[length];
new SecureRandom().nextBytes(bytes);
String base64String = Base64.getEncoder().encodeToString(bytes);
return base64String;
}
private static String mergeSign(String time, String randomCode, String httpType, String uri,
String domain, String port, String other) {
if (time.isEmpty() || randomCode.isEmpty() || httpType.isEmpty() || domain.isEmpty() || port.isEmpty())
{
return null;
}
String prefix =
time + "\n" + randomCode + "\n" + httpType + "\n" + uri + "\n" + domain + "\n" + port
+ "\n";
if (other.isEmpty()) {
prefix += "\n";
} else {
prefix += (other + "\n");
}
return prefix;
}
private static String sign(String signatureBaseString, String key) {
try {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingKey);
byte[] text = signatureBaseString.getBytes(StandardCharsets.UTF_8);
byte[] signatureBytes = mac.doFinal(text);
signatureBytes = Base64.getEncoder().encode(signatureBytes);
return new String(signatureBytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException(e);
}
}
private static String getAuthorizationParam(String key, String value) {
if (key.isEmpty() || value.isEmpty()) {
return null;
}
return key + "=" + "\"" + value + "\"";
}
}
PHP 请求示例
<?php
/**
* 以下是配置项:请按实际情况替换参数后进行授权请求。
*/
$client_id = "请替换为控制台的 Client ID";
$kid = "请替换为客户端 SDK 授权成功后,MAC Token 中 kid 的值";
$mac_key = "请替换为客户端 SDK 授权成功后,MAC 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


def get_mac_token_signature(host, request_url, method, mac_key, kid):
mac_token_pattern = 'MAC id="{kid}",ts="{ts}",nonce="{nonce}",mac="{mac}"'
timestamp = str(int(time.time()))
nonce = ''.join(random.choices(string.ascii_lowercase + string.ascii_uppercase, k=16))
sign_array = [timestamp, nonce, method, request_url, host, '443', '']
seperator = '\n'
sign_input = seperator.join(sign_array) + seperator
hmac_code = hmac.new(mac_key.encode('UTF-8'), sign_input.encode('UTF-8'), sha1)
mac_str = base64.b64encode(hmac_code.digest()).decode('UTF-8')
return mac_token_pattern.format(kid=kid, ts=timestamp, nonce=nonce,
mac=mac_str)

if __name__ == '__main__':
kid = "1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"
mac_key = "mSUQNYUGRBPXyRyW"
client_id = "0RiAlMny7jiz086FaU"
signature = get_mac_token_signature('open.tapapis.cn', '/account/profile/v1?client_id=' + client_id,
'GET', mac_key, kid)
print(signature)
Go 请求示例
package main

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

func main() {
// 替换 clientId、accessToken、macKey 参数
// clientId 参数在 TapDC 后台查看
clientId := "请替换为控制台的 `Client ID`"
// TapTap 登录成功后 TapSDK 返回的 kid
kid := "1/jvsVxFC6-PIUiXvZVVtv1hogX5q9Z1y_rp-AtVjE3iyHikXXfd_2h-i0wLmc9UjLJwhH6fQ8cvGrklONdvy2J5YfoqzV0ewGPMSLkQIkRv_xaLaYPariWbrkP1MtG2b4CzR1KHvuSCJHewCmTFZmsyNGojTJr5t75f5Nc8j-jjCYeDtFO0-XFI_J7kzktswzzsmISt7cx49QVess-VbaQcU31pEDb_OA03I28H5ehIvqQ0CQdf1LieLyONcH97l1IEU39AirioF_KGJccVG64QsgWmzxLPwmfTurw4cwBPo04yuXnas4YI5haE2UxtckNCpagP19drtGW57-HaAdww"
// TapTap 登录成功后 TapSDK 返回的 mac_key
macKey := "fTCuDUDDmNny7a36EWbhUDLaqpoDMQu2hCi9qAJ5"

// 随机数,正式上线请替换
nonce := "8IBTHwOdqNKAWeKl7plt66=="
// 时间戳转换成字符串
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
// 请求 url 相关
reqHost := "open.tapapis.cn"
reqURI := "/account/profile/v1?client_id=" + clientId
reqURL := "https://" + reqHost + reqURI

macStr := timestamp + "\n" + nonce + "\n" + "GET" + "\n" + reqURI + "\n" + reqHost + "\n" + "443" + "\n\n"
mac := hmacSha1(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 hmacSha1(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))
}
C# 请求示例
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;

public class TapLoginOAuth : MonoBehaviour
{

private string host = "open.tapapis.cn";
private string clientId = "应用的 Client id";
private string macKey = "客户端登录返回的 macKey 值";
private string kid = "客户端登录返回的 kid 值";


// Start is called before the first frame update
void Start()
{
// 发起网络请求
StartCoroutine(SendRequest());
}

// Update is called once per frame
void Update()
{

}

IEnumerator SendRequest()
{
// 构建请求 URL
string requestUrl = $"/account/profile/v1?client_id={clientId}";

// 生成签名
string signature = GetMacTokenSignature(host, requestUrl, "GET", macKey, kid);
Debug.Log("Generated Signature: " + signature);

// 发送 GET 请求到服务器
using (var httpClient = new HttpClient())
{
var uri = new Uri($"https://{host}{requestUrl}");
var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Add("Authorization", signature);

// 发送请求并等待响应
var response = httpClient.SendAsync(request).Result;
var responseBody = response.Content.ReadAsStringAsync().Result;

// 输出服务器响应
Debug.Log("Server Response: " + responseBody);
}

yield return null;
}
string GetMacTokenSignature(string host, string requestUrl, string method, string macKey, string kid)
{
string macTokenPattern = "MAC id=\"{0}\",ts=\"{1}\",nonce=\"{2}\",mac=\"{3}\"";
string timestamp = ((int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds).ToString();
string nonce = GenerateNonce(16);
string[] signArray = { timestamp, nonce, method, requestUrl, host, "443", "" };
string separator = "\n";
string signInput = string.Join(separator, signArray) + separator;

using (var hmac = new System.Security.Cryptography.HMACSHA1(Encoding.UTF8.GetBytes(macKey)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signInput));
string macStr = Convert.ToBase64String(hash);
return string.Format(macTokenPattern, kid, timestamp, nonce, macStr);
}
}

string GenerateNonce(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
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);
}
}

脚本请求示例

可用此脚本验证直接替换参数,用来验证自己服务端签算的 mac token 是否正确。

CLIENT_ID 替换为控制台获取的 Client ID,KID 和 MAC_KEY 为客户端登录成功后的 kidmac_key

#!/usr/bin/env bash

# 客户端 ID
CLIENT_ID="请替换为控制台的 `Client ID`"
# SDK 获取的 kid
KID="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ"
# SDK 获取的 mac_key
MAC_KEY="mSUQNYUGRBPXyRyW"

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

# 请求方法
METHOD="GET"
# 请求地址 (带 query string)
REQUEST_URI="/account/profile/v1?client_id=${CLIENT_ID}"
# 请求域名
REQUEST_HOST="open.tapapis.cn"

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

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

curl -s -H"Authorization:${AUTHORIZATION}" "https://${REQUEST_HOST}${REQUEST_URI}"

通用接口错误信息

统一格式

字段类型说明
codeint预留字段,用于以后追踪问题
errorstring错误码,代码逻辑判断时使用
error_descriptionstring错误描述信息,开发的时候用来帮助理解和解决发生的错误

错误响应

错误码详细描述
invalid_request请求缺少某个必需参数,包含一个不支持的参数或参数值,或者格式不正确
invalid_timeMAC Token 算法中,ts 时间不合法,应请求服务器时间重新构造
invalid_clientclient_id 参数无效
access_denied授权服务器拒绝请求 这个状态出现在拿着 token 请求用户资源时,如出现,客户端应退出本地的用户登录信息,引导用户重新登录
forbidden用户没有对当前动作的权限,引导重新身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交
not_found请求失败,请求所希望得到的资源未被在服务器上发现。在参数相同的情况下,不应该重复请求
server_error服务器出现异常情况 可稍等后重新尝试请求,但需有尝试上限,建议最多 3 次,如一直失败,则中断并告知用户
insufficient_scope移动端进行 TapTap 授权使用的授权范围与服务端调用的 OAuth 接口不匹配导致,例如:移动端授权采用 basic_info 权限而服务端调用 获取当前账户基础信息 API 时则会返回该异常

常见问题

调用 TapTap OAuth 接口返回 access_denied 错误码。

1、 检查签算是否正确

请使用 TapTap 官方文档提供的示例脚本,验证签名计算是否正确。如果签算不正确,可能导致认证失败。

2、 Access Token 过期或无效

每个 Access Token 最长有效期为 30 天。SDK 在每次启动时会自动刷新 Token,确保通过 GetCurrentTapAccount 获取的 Token 始终有效。若玩家 30 天内未登录或不活跃,本地可能无法获取新 Token,需要用户重新登录。

3、 玩家主动注销或取消授权

若玩家在 TapTap 客户端内注销账号,或在授权管理中解除授权,后续的请求将返回 access_denied 错误。服务器应在 Token 校验失败时合理处理 access_denied,并引导用户重新登录。

4、 最佳实践:避免缓存 Token

不要对 Access Token 进行本地缓存,因为 Token 可能会随时失效。每次需要使用时,通过 GetCurrentTapAccount 获取最新的有效 Token,避免用户不必要的频繁登录操作。

调用 TapTap OAuth 接口返回 insufficient_scope 错误码。

insufficient_scope 错误通常是由于 移动端授权的权限范围服务端调用的 OAuth API 所需权限 不匹配 导致的。

例如:移动端在进行 TapTap 授权登录时申请的是 basic_info 权限,但服务端调用 获取当前账户详细信息 API 需要更高级别的权限,由于权限不足导致服务器返回该错误码。

权限匹配规则

通过合理配置 OAuth 授权范围,可有效避免 insufficient_scope 错误,确保 API 调用的顺畅性。