抖音開放平臺接入
抖音開放平臺接入
準備工作
注冊抖音開放平臺 https://developer.open-douyin.com/ 并且進行企業(yè)認證。內(nèi)網(wǎng)穿透工具(需支持https),推薦ngrok。
相關(guān)網(wǎng)站
- 抖音開放平臺: https://developer.open-douyin.com/console?type=1
- 官網(wǎng)開發(fā)文檔:https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/list
- 抖音提供的h5發(fā)布視頻演示頁面:https://open.douyin.com/web_apps/h5_share
需要資料
- 應用對應:Client Key 和 Client Secret
- 白名單管理:增加白名單抖音賬號
注意事項
- 抖音權(quán)登錄抖音號需要加入白名單 控制臺-->設(shè)置-->白名單管理
- 授權(quán)登錄地址權(quán)限
scope需要配置已有權(quán)限,就是抖音對應的能力管理 - 授權(quán)回調(diào)和訂閱事件都需要在抖音配置才可以生效,控制臺-->設(shè)置-->開發(fā)配置
授權(quán)登錄
第一步 授權(quán)登錄地址拼接
授權(quán)登錄和微信授權(quán)登錄類似,拼接授權(quán)登錄地址。包含回調(diào)地址(必須https)攜帶code以及自定義參數(shù) state。回調(diào)地址可以是到指定前端頁面可以是接口,我這里是通過接口去接收。
public String access(String uid, String redirectUri) {
List<String> scope = Lists.newArrayList();
scope.add("data.external.item");//視頻數(shù)據(jù)
scope.add("user_info");// 用戶信息
scope.add("h5.share");// 支持H5場景的內(nèi)容可以分享發(fā)布到抖音,且可攜帶指定話題、小程序等內(nèi)容
scope.add("open.get.ticket");// 用于h5鏈接拉起抖音發(fā)布器分享視頻時對開發(fā)者身份進行驗簽
scope.add("trial.whitelist");// 測試應用白名單權(quán)限
scope.add("poi.cps.common");// CPS傭金設(shè)置與查詢
scope.add("open.get.ticket");// 獲取openTicket
URLBuildUtil url = new URLBuildUtil("https://open.douyin.com/platform/oauth/connect/");
url.putParams("client_key", '抖音應用信息 client key');
url.putParams("response_type", "code");
url.putParams("redirect_uri", "回調(diào)地址");
url.putParams("scope", String.join(",", scope));
url.putParams("state", uid);
String build = url.build();
log.debug("url:{}", build);
return build;
}
最終訪問地址
https://open.douyin.com/platform/oauth/connect?client_key=1234563&redirect_uri=https://d2c2-120-238-70-9.ngrok-free.app/tiktok/callBack&response_type=code&scope=data.external.item,user_info,h5.share,open.get.ticket,trial.whitelist,poi.cps.common,open.get.ticket&state=1662039377758420993
直接瀏覽器訪問

注意
1、生成的地址可以直接通過瀏覽器打開,抖音進行掃碼授權(quán)。
2、將上面生成的地址再次作為二維碼內(nèi)容生成一個二維碼,用戶直接抖音掃碼也是可以完成授權(quán)登錄。
第二步 配置授權(quán)回調(diào)
如果是開發(fā)環(huán)境請使用內(nèi)網(wǎng)穿透地址,可以配置多個回調(diào)地址。

第三步 回調(diào)接口處理
授權(quán)成功抖音會重定向到回調(diào)地址,GET請求,并且攜帶 code 和 state 參數(shù)如果有
@ApiOperation(value = "授權(quán)回調(diào)")
@GetMapping(value = "callBack")
public Result callback(String code, String state) {
// 獲取 token 和 open id
Map<String, Object> params = new HashMap<>();
params.put("client_secret","client_secret");
params.put("client_key", "client_key");
params.put("code", code);
params.put("grant_type", "authorization_code");
Map map = HttpClientUtils.getInstance()
.setContentType("application/json")
.putParams(params)
.doPost("https://open.douyin.com/oauth/access_token/").toMap();
//... 自己的業(yè)務邏輯
return Result.success("授權(quán)成功");
}
訂閱事件
第一步 配置Webhooks
抖音是以POST請求進行驗證配置的,后續(xù)所有事件都會發(fā)送到改接口上。和微信公眾號開發(fā)服務器驗證類似。

第二步 訂閱接口處理
該接口接收抖音驗證消息,驗證通過才能配置成功。請求頭類型是 application/json 內(nèi)容是在body中所以我的接口用 map 來接收,消息體有返回openid 抖音用戶唯一標識
抖音事件如下
- create_video 視頻(通過h5分享發(fā)布視頻觸發(fā)該事件)
- unauthorize 取消授權(quán)(設(shè)置->賬號與安全->授權(quán)管理-解綁觸發(fā))
- authorize 授權(quán)(掃碼授權(quán)登錄觸發(fā))
- verify_webhook 服務器驗證(抖音開發(fā)配置添加URL觸發(fā))
注意
抖音簽名是
headers里面獲取,簽名驗證是應用秘鑰+body字符進行sha1加密結(jié)果比對相對表示沒問題。返回給抖音的是content內(nèi)容,響應頭必須是application/json
@ApiOperation("訂閱消息")
@PostMapping(value = "subscribe")
public void subscribeEvent(@RequestBody Map<String, Object> map, HttpServletRequest request, HttpServletResponse response) {
log.info("訂閱消息:{}", JSONUtil.objToStr(map));
// 簽名字符串
String signature = request.getHeader("x-douyin-signature");
log.info("signature:{}", signature);
// 消息id
String msgId = request.getHeader("msg-id");
log.info("msgId:{}", msgId);
// 消息內(nèi)容
Map content = (Map) map.get("content");
// 事件
String event = map.get("event").toString();
// verify_webhook 服務器驗證
if (event.equals("verify_webhook")) {
// 秘鑰+body字符串
String data = "client secret " + JSONUtil.objToStr(map);
// sha1 簽名
String sign = DigestUtils.sha1Hex(data);
log.info("sign:{}", sign);
if (Objects.equals(sign, signature)) {
this.responseText(JSONUtil.objToStr(content), response);
return; // 提前結(jié)束
}
}
// ... 其他業(yè)務邏輯
}
// 處理響應結(jié)果
private void responseText(String text, HttpServletResponse response) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter writer = null;
try {
writer = response.getWriter();
} catch (IOException e) {
e.printStackTrace();
}
assert writer != null;
writer.write(text);
writer.flush();
writer.close();
}
官網(wǎng)代碼示例
import org.apache.commons.codec.digest.DigestUtils; // sha1算法庫
// 獲取消息中body
String str, wholeStr = "";
try{
BufferedReader br = re.getReader();
while((str = br.readLine()) != null){
wholeStr += str;
}
} catch (Exception e){
log.warn("獲取請求內(nèi)容失敗");
}
// 獲取請求頭中的加簽信息
String signature = re.getHeader("X-Douyin-Signature");
String data = appSecret + wholeStr;
String sign = DigestUtils.sha1Hex(data);
if(!sign.equals(signature)){
log.error("驗簽失敗");
}
第三步 事件消息體
消息內(nèi)容做了脫敏處理,實際返回內(nèi)容以接口為準
h5發(fā)布視頻
{
"event": "create_video",
"client_key": "11111",
"from_user_id": "234234234ldoQ2UFeflKR33333eLYTNVTs",
"content": {
"share_id": "111111",
"item_id": "@9VxX1111111zoA+lLFUWbfL+60z333333EBbHec9qLXdMCWaQQYTUnzwg==",
"has_default_hashtag": null,
"video_id": "111111"
},
"log_id": "2023060117095922440FA7ABD95228B8D0",
"event_id": ""
}
取消授權(quán)
{
"event": "unauthorize",
"client_key": "11111",
"from_user_id": "11111dU7BKTbvqPiLPts8KxFUDKS",
"content": {
"scopes": [
"user_info",
"trial.whitelist",
"data.external.item"
],
"code": 1,
"description": "用戶取消授權(quán)"
},
"log_id": "20230601165505000000000000596F91E",
"event_id": ""
}
授權(quán)登錄
{
"event": "authorize",
"client_key": "11111",
"from_user_id": "11111BKTbvqPiLPts8KxFUDKS",
"content": {
"scopes": [
"user_info",
"trial.whitelist",
"data.external.item"
]
},
"log_id": "20230601165602172018000003663DCE3",
"event_id": ""
}
服務器驗證
{
"event": "verify_webhook",
"client_key": "11111",
"from_user_id": "",
"content": {
"challenge": 19655498
},
"log_id": "2023060116524902BF3D205E5EBE86AAE4",
"event_id": ""
}
H5 發(fā)布 Schema 生成示例
https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/h5/share-to-h5
H5 分享是指第三方應用通過接入該功能,讓用戶可以從網(wǎng)頁或者外部應用分享在線視頻或圖片等信息到抖音。
說白就是通過h5可以做視頻批量分發(fā),平臺可以檢測這些視頻數(shù)據(jù)。
步驟:
- 首先獲取
client_token - 其次通過
client_token獲取ticket - 最后 票據(jù)
ticket、隨機字符串nonce_str、 時間戳timestamp進行簽名 share id這個參數(shù)可選,用來跟蹤用戶發(fā)布h5視頻狀態(tài),是否發(fā)布成功,以及后續(xù)業(yè)務處理。
第一步 client_token
Map<String, Object> params = new HashMap<>();
params.put("client_secret", "client_secret");
params.put("client_key", "client_key");
params.put("grant_type", "client_credential");
Map map = HttpClientUtils.getInstance()
.putParams(params)
.doPost("https://open.douyin.com/oauth/client_token/").toMap();
log.debug("clientToken:{}", map);
Map data = (Map) map.get("data");
if (!Objects.equals(data.get("error_code"), 0)) {
throw new RuntimeException(map.get("message").toString());
}
String access_token = data.get("access_token").toString();
第二步 ticket
票據(jù)有效期 2 小時,自行緩存
Map map = HttpClientUtils.getInstance()
.putHeader("access-token", this.clientToken())
.doGet("https://open.douyin.com/open/getticket/").toMap();
log.debug("clientToken:{}", map);
Map data = (Map) map.get("data");
if (!Objects.equals(data.get("error_code"), 0)) {
throw new RuntimeException(data.get("description").toString());
}
String ticket = data.get("ticket").toString();
第三步 簽名
這里使用了 TreeMap 因為官網(wǎng)要求要 ASCII 碼從小到大排序(字典序) 注意時間戳字段是秒不是毫秒
long timestamp = System.currentTimeMillis() / 1000; // 秒
TreeMap<String, String> map = new TreeMap<>();
map.put("nonce_str", '隨機字符串');
map.put("ticket", '票據(jù)');
map.put("timestamp", '時間戳' + "");
log.debug("參數(shù):{}", JSONUtil.objToStr(map));
List<String> list = new ArrayList<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
list.add(String.format("%s=%s", key, value));
}
String join = String.join("&", list);
String signature = Md5Util.md5Encode(join, "UTF-8");
log.debug("簽名:{}", signature);
第四步 share id(可選)
public String shareId() {
Map<String, Object> params = new HashMap<>();
params.put("need_callback", true); //
Map map = HttpClientUtils.getInstance()
.putParams(params)
.putHeader("access-token", 'client_token')
.setContentType("application/json")
.doGet("https://open.douyin.com/share-id/").toMap();
log.debug("shareId:{}", map);
Map data = (Map) map.get("data");
if (!Objects.equals(data.get("error_code"), 0)) {
throw new RuntimeException(data.get("description").toString());
}
return data.get("share_id").toString();
}
第五步 schema
這里需要注意的是我們生成并不是一個 URL 地址,他是一個 schema 地址,特殊URL地址和蘋果URL Schemes差不多意思。就是可以通過瀏覽器訪問這個鏈接喚起對應APP。
具體參數(shù)參考官網(wǎng):https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/h5/share-to-h5
注意
- 提供的素材必須是http地址的可以下載的素材,像有些資源有防盜鏈等就是不行的
- 生成 schema 地址可以復制在瀏覽器直接打開,會自動跳轉(zhuǎn)抖音
- 也可以生成二維碼使用抖音掃碼,原則上這個分享鏈接有效期2小時
- 蘋果手機請使用 Safari 瀏覽器打開,其他瀏覽器可能有問題
// 構(gòu)建 schema
URLBuildUtil url = new URLBuildUtil("snssdk1128://openplatform/share");
url.putParams("share_type", "h5");
url.putParams("client_key",'client_key');
url.putParams("title", 'title');
url.putParams("nonce_str", 'nonce_str');
url.putParams("timestamp", 'timestamp' );
url.putParams("signature", 'signature');
url.putParams("image_path",URLEncoder.encode("https://js.ibaotu.com/act/23/04/20/6441198db0ef0.jpg", "UTF-8"););
// 分享 id
url.putParams("state", "shareId");
schema 地址
snssdk1128://openplatform/share?client_key=1111111&image_path=https%3A%2F%2Fjs.ibaotu.com%2Fact%2F23%2F04%2F20%2F6441198db0ef0.jpg&nonce_str=29649247100043823967999435052604&share_type=h5&signature=efb371d33b0deefb6e471ded1724faef&state=1767391659575207×tamp=1685516015&title=哈哈哈
完整代碼示例
import com.github.chenlijia1111.utils.core.RandomUtil;
import com.github.chenlijia1111.utils.http.HttpClientUtils;
import com.github.chenlijia1111.utils.http.URLBuildUtil;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
class Tiktok {
/**
* description: 客戶端 token
* create by: Mr.Fang
*
* @return: java.lang.String
* @date: 2023/6/2 16:11
*/
public static String clientToken() {
Map<String, Object> params = new HashMap<>();
params.put("client_secret", Constants.TIKTOK_CLIENT_SECRET); //
params.put("client_key", Constants.TIKTOK_CLIENT_KEY); //
params.put("grant_type", "client_credential"); // 回調(diào)地址
Map map = HttpClientUtils.getInstance()
.putParams(params)
.doPost("https://open.douyin.com/oauth/client_token/").toMap();
Map data = (Map) map.get("data");
if (!Objects.equals(data.get("error_code"), 0)) {
throw new RuntimeException(map.get("message").toString());
}
String access_token = data.get("access_token").toString();
return access_token;
}
/**
* description: 票據(jù)
* create by: Mr.Fang
*
* @return: java.lang.String
* @date: 2023/6/2 16:11
*/
public static String ticket() {
Map map = HttpClientUtils.getInstance()
.putHeader("access-token", clientToken())
.doGet("https://open.douyin.com/open/getticket/").toMap();
Map data = (Map) map.get("data");
if (!Objects.equals(data.get("error_code"), 0)) {
throw new RuntimeException(data.get("description").toString());
}
String ticket = data.get("ticket").toString();
return ticket;
}
/**
* description: 分享 id
* create by: Mr.Fang
*
* @return: java.lang.String
* @date: 2023/6/2 16:11
*/
public static String shareId() {
Map<String, Object> params = new HashMap<>();
params.put("need_callback", true); //
Map map = HttpClientUtils.getInstance()
.putParams(params)
.putHeader("access-token", clientToken())
.setContentType("application/json")
.doGet("https://open.douyin.com/share-id/").toMap();
Map data = (Map) map.get("data");
if (!Objects.equals(data.get("error_code"), 0)) {
throw new RuntimeException(data.get("description").toString());
}
return data.get("share_id").toString();
}
/**
* description: schema
* create by: Mr.Fang
*
* @return: java.lang.String
* @date: 2023/6/2 16:11
*/
public static String schemaH5() {
String randomCode = RandomUtil.createRandomCode(32); // 隨機字符
long timestamp = System.currentTimeMillis() / 1000; // 秒
String ticket = ticket(); // 票據(jù)
TreeMap<String, String> map = new TreeMap<>();
map.put("nonce_str", randomCode);
map.put("ticket", ticket);
map.put("timestamp", timestamp + "");
List<String> list = new ArrayList<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
list.add(String.format("%s=%s", key, value));
}
String join = String.join("&", list);
String signature = Md5Util.md5Encode(join, "UTF-8");
// 構(gòu)建 URL
URLBuildUtil url = new URLBuildUtil("snssdk1128://openplatform/share");
url.putParams("share_type", "h5");
url.putParams("client_key", Constants.TIKTOK_CLIENT_KEY);
url.putParams("title", "標題");
url.putParams("nonce_str", randomCode);
url.putParams("timestamp", timestamp + "");
url.putParams("signature", signature);
url.putParams("state", shareId());
url.putParams("image_path", urlEncode("https://js.ibaotu.com/act/23/04/20/6441198db0ef0.jpg"));
return url.build();
}
private static String urlEncode(String url) {
try {
return URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException(url);
}
}
}
API請求工具類型使用了第三方工具包,在此鳴謝 chenlijia1111 該作者
<dependency>
<groupId>com.github.chenlijia1111</groupId>
<artifactId>utils</artifactId>
<version>1.2.0-RELEASE</version>
</dependency>

浙公網(wǎng)安備 33010602011771號