跳到主要内容
版本:v3

TapTap OAuth 接口

概述

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

开发者接入 SDK 单纯 TapTap 用户认证方式,在用户授权你的应用程序后,将会生成访问令牌(Access Token)。这个访问令牌在加密后会生成 Mac Token 字符串。Access Token 长期有效,只有在用户更新其账户安全信息或解除对当前应用的授权时才会失效。开发人员应妥善管理 Access Token 在其服务器上,用作与 TapTap 服务器进行后续通讯的标识。

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

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

流程

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

    public String kid;
    public String access_token;
    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

注意:当前实际返回的 kidaccess_token 值相等,建议使用 kid

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.
access_tokenstring该字段暂无作用
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

# 客户端 ID
$client_id = "请替换为控制台的 Client ID";
# 替换成移动端 SDK 获取的 access_token
$access_token="1/hC0vtMo7ke0Hkd-iI8-zcAwy7vKds9si93l7qBmNFxJkylWEOYEzGqa7k_9iw_bb3vizf-3CHc6U8hs-5a74bMFzkkz7qC2HdifBEHsW9wxOBn4OsF9vz4Cc6CWijkomnOHdwt8Km6TywOX5cxyQv0fnQQ9fEHbptkIJagCd33eBXg76grKmKsIR-YUZd1oVHu0aZ6BR7tpYYsCLl-LM6ilf8LZpahxQ28n2c-y33d-20YRY5NW1SnR7BorFbd00ZP97N9kwDncoM1GvSZ7n90_0ZWj4a12x1rfAWLuKEimw1oMGl574L0wE5mGoshPa-CYASaQmBDo3Q69XbjTsKQ";
# 替换成移动端 SDK 获取的 mac_key
$mac_key = "mSUQNYUGRBPXyRyW";
# 接口请求地址
$request_url = "https://open.tapapis.cn/account/profile/v1?client_id=".$client_id;
$ts = time();
$nonce = "== XinDong_Lee-JiEun ==";//随机数,正式上线请替换
//方式一
//$params = [$ts,$nonce,"GET","/account/profile/v1?client_id=".$client_id,"open.tapapis.cn", 443,"\n"];
//$signStr = implode("\n",$params);
//var_dump($signStr);
//方式二
$signStr = sprintf("%s\n%s\n%s\n%s\n%s\n443\n\n",$ts,$nonce,"GET","/account/profile/v1?client_id=".$client_id,"open.tapapis.cn");
//MAC TOKEN 加密
$mac = base64_encode(hash_hmac('sha1', $signStr, $mac_key,true));
$auth = sprintf('MAC id="%s",ts="%s",nonce="%s",mac="%s"',$access_token,$ts,$nonce,$mac);
$headers = array(
"Authorization:".$auth
);
echo "Authorization:".$auth.PHP_EOL.PHP_EOL;

$curl = curl_init($request_url);
// 设置请求方式
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "GET");
// Header 头信息设置
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$result = curl_exec($curl);
curl_close($curl);

?>
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 返回的 access_token
accessToken := "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=" + "\"" + accessToken + "\"" + "," + "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))
}
脚本请求示例

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

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

#!/usr/bin/env bash

# 客户端 ID
CLIENT_ID="请替换为控制台的 `Client ID`"
# SDK 获取的 access_token
ACCESS_TOKEN="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"' "${ACCESS_TOKEN}" "${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 次,如一直失败,则中断并告知用户