抖音开放平台接入

抖音开放平台接入

准备工作

注册抖音开放平台 https://developer.open-douyin.com/ 并且进行企业认证。内网穿透工具(需支持https),推荐ngrok

相关网站

需要资料

  • 应用对应:Client Key 和 Client Secret
  • 白名单管理:增加白名单抖音账号

注意事项

  1. 抖音权登录抖音号需要加入白名单 控制台-->设置-->白名单管理
  2. 授权登录地址权限scope需要配置已有权限,就是抖音对应的能力管理
  3. 授权回调订阅事件都需要在抖音配置才可以生效,控制台-->设置-->开发配置

授权登录

第一步 授权登录地址拼接

授权登录和微信授权登录类似,拼接授权登录地址。包含回调地址(必须https)携带code以及自定义参数 state。回调地址可以是到指定前端页面可以是接口,我这里是通过接口去接收。

public String access(String uid, String redirectUri) {
    List<String> scope = Lists.newArrayList();
    scope.add("data.external.item");//视频数据
    scope.add("user_info");// 用户信息
    scope.add("h5.share");// 支持H5场景的内容可以分享发布到抖音,且可携带指定话题、小程序等内容
    scope.add("open.get.ticket");// 用于h5链接拉起抖音发布器分享视频时对开发者身份进行验签
    scope.add("trial.whitelist");// 测试应用白名单权限
    scope.add("poi.cps.common");// CPS佣金设置与查询
    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", "回调地址");
    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、生成的地址可以直接通过浏览器打开,抖音进行扫码授权。

2、将上面生成的地址再次作为二维码内容生成一个二维码,用户直接抖音扫码也是可以完成授权登录。

第二步 配置授权回调

如果是开发环境请使用内网穿透地址,可以配置多个回调地址。

第三步 回调接口处理

授权成功抖音会重定向到回调地址,GET请求,并且携带 codestate 参数如果有

@ApiOperation(value = "授权回调")
@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();
      //... 自己的业务逻辑
      return Result.success("授权成功");
}

订阅事件

第一步 配置Webhooks

抖音是以POST请求进行验证配置的,后续所有事件都会发送到改接口上。和微信公众号开发服务器验证类似。

第二步 订阅接口处理

该接口接收抖音验证消息,验证通过才能配置成功。请求头类型是 application/json 内容是在body中所以我的接口用 map 来接收,消息体有返回openid 抖音用户唯一标识

抖音事件如下

  • create_video 视频(通过h5分享发布视频触发该事件)
  • unauthorize 取消授权(设置->账号与安全->授权管理-解绑触发)
  • authorize 授权(扫码授权登录触发)
  • verify_webhook 服务器验证(抖音开发配置添加URL触发)

注意

抖音签名是headers里面获取,签名验证是 应用秘钥+body字符 进行sha1 加密结果比对相对表示没问题。返回给抖音的是content 内容,响应头必须是 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);
    // 消息内容
    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; // 提前结束
        }
    }
    // ... 其他业务逻辑
}
// 处理响应结果
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();
}

官网代码示例

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("获取请求内容失败");
}
// 获取请求头中的加签信息
String  signature = re.getHeader("X-Douyin-Signature");
String data = appSecret + wholeStr;
String sign = DigestUtils.sha1Hex(data);
if(!sign.equals(signature)){
    log.error("验签失败");
}

第三步 事件消息体

消息内容做了脱敏处理,实际返回内容以接口为准

h5发布视频

{
    "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": ""
}

取消授权

{
    "event": "unauthorize",
    "client_key": "11111",
    "from_user_id": "11111dU7BKTbvqPiLPts8KxFUDKS",
    "content": {
        "scopes": [
            "user_info",
            "trial.whitelist",
            "data.external.item"
        ],
        "code": 1,
        "description": "用户取消授权"
    },
    "log_id": "20230601165505000000000000596F91E",
    "event_id": ""
}

授权登录

{
    "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 发布 Schema 生成示例

https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/h5/share-to-h5

H5 分享是指第三方应用通过接入该功能,让用户可以从网页或者外部应用分享在线视频或图片等信息到抖音。

说白就是通过h5可以做视频批量分发,平台可以检测这些视频数据。

步骤:

  1. 首先获取 client_token
  2. 其次通过 client_token 获取 ticket
  3. 最后 票据 ticket 、随机字符串nonce_str、 时间戳timestamp进行签名
  4. share id 这个参数可选,用来跟踪用户发布h5视频状态,是否发布成功,以及后续业务处理。

第一步 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

票据有效期 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 因为官网要求要 ASCII 码从小到大排序(字典序) 注意时间戳字段是秒不是毫秒

long timestamp = System.currentTimeMillis() / 1000; // 秒
TreeMap<String, String> map = new TreeMap<>();
map.put("nonce_str", '随机字符串');
map.put("ticket", '票据');
map.put("timestamp", '时间戳' + "");
log.debug("参数:{}", 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。
具体参数参考官网:https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/sdk/web-app/h5/share-to-h5

注意

  • 提供的素材必须是http地址的可以下载的素材,像有些资源有防盗链等就是不行的
  • 生成 schema 地址可以复制在浏览器直接打开,会自动跳转抖音
  • 也可以生成二维码使用抖音扫码,原则上这个分享链接有效期2小时
  • 苹果手机请使用 Safari 浏览器打开,其他浏览器可能有问题
// 构建 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&timestamp=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"); // 回调地址
        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: 票据
     * 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(); // 票据

        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");

        // 构建 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>
posted @ 2023-06-02 17:14  天葬  阅读(5304)  评论(0编辑  收藏  举报