抖音开放平台接入
抖音开放平台接入
准备工作
注册抖音开放平台 https://developer.open-douyin.com/ 并且进行企业认证。内网穿透工具(需支持https),推荐ngrok。
相关网站
- 抖音开放平台: https://developer.open-douyin.com/console?type=1
- 官网开发文档:https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/list
- 抖音提供的h5发布视频演示页面:https://open.douyin.com/web_apps/h5_share
需要资料
- 应用对应:Client Key 和 Client Secret
- 白名单管理:增加白名单抖音账号
注意事项
- 抖音权登录抖音号需要加入白名单 控制台-->设置-->白名单管理
- 授权登录地址权限
scope
需要配置已有权限,就是抖音对应的能力管理 - 授权回调和订阅事件都需要在抖音配置才可以生效,控制台-->设置-->开发配置
授权登录
第一步 授权登录地址拼接
授权登录和微信授权登录类似,拼接授权登录地址。包含回调地址(必须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请求,并且携带 code
和 state
参数如果有
@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可以做视频批量分发,平台可以检测这些视频数据。
步骤:
- 首先获取
client_token
- 其次通过
client_token
获取ticket
- 最后 票据
ticket
、随机字符串nonce_str
、 时间戳timestamp
进行签名 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×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"); // 回调地址 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>
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 在缓慢中沉淀,在挑战中重生!2024个人总结!
· 大人,时代变了! 赶快把自有业务的本地AI“模型”训练起来!
· 从 Windows Forms 到微服务的经验教训