tiktok第三方登录授权流程

需要先申请账号 拿到
tiktok:
oauth:
client-key: xxxx
client-secret: xxx
redirect-uri: xxx
这些参数从这里拿到 https://developers.tiktok.com/app/7584277019718944779/pending 审核需要时间,可以先用沙箱环境进行测试。
主要流程如下:

前端
↓ 跳转 TikTok 授权页
TikTok
↓ 用户确认授权
前端回调页
↓ 拿到 code
后端
↓ code 换 access_token
↓ access_token 拉用户信息
↓ 本地登录 / 注册

贴一下测试的h5页面

总结就是后端服务只需要提供一个回调接口 ,tiktok会回调给服务器code和state参数,code就是用来换取tiktok登录的token的,拿到token后就可以调用tiktok第三方用户详情接口了,拿到第三方用户详情后,就可以进行自己app的登录注册逻辑了

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>TikTok 登录测试</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 50px; }
        button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
        #userinfo { margin-top: 20px; }
        img { width: 100px; height: 100px; border-radius: 50%; }
        pre { background: #f5f5f5; padding: 10px; border-radius: 5px; }
    </style>
</head>
<body>
<h1>TikTok 登录测试</h1>
<button id="loginBtn">TikTok 登录</button>

<div id="userinfo"></div>

<script>
    const clientKey = "xxx"; // 替换成你的 TikTok client_key
    const redirectUri = encodeURIComponent("xxx"); // 后端服务回调地址
    const state = "12345"; // 随机值防 CSRF

    document.getElementById("loginBtn").onclick = function() {
        const url = `https://www.tiktok.com/v2/auth/authorize?client_key=${clientKey}&scope=user.info.basic&response_type=code&redirect_uri=${redirectUri}&state=${state}`;
        window.location.href = url;
    }

    // 解析 URL 参数
    function getQueryParam(name) {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get(name);
    }

    // 如果页面带有 code 和 state,则说明回调返回了
    window.onload = function() {
        const code = getQueryParam("code");
        const returnedState = getQueryParam("state");

        if(code) {
            // 调用后端接口获取用户信息
            fetch(`/tiktok/callback?code=${code}&state=${returnedState}`)
                .then(res => res.json())
                .then(data => {
                    const userDiv = document.getElementById("userinfo");
                    if(data.data) {
                        const user = data.data;
                        userDiv.innerHTML = `
                                <h3>用户信息</h3>
                                <img src="${user.avatar_url}" alt="avatar"/>
                                <p>用户信息: ${user}</p>
                                <p>昵称: ${user.display_name}</p>
                                <p>open_id: ${user.open_id}</p>
                                <pre>${JSON.stringify(user, null, 2)}</pre>
                            `;
                    } else {
                        userDiv.innerHTML = `<p>获取用户信息失败</p>`;
                    }
                })
                .catch(err => {
                    console.error(err);
                    document.getElementById("userinfo").innerHTML = "<p>接口请求错误</p>";
                });
        }
    }
</script>
</body>
</html>

  后端代码

@GetMapping("/auth/tiktok/callback")
    public R<AuthSuccessResult> loginTiktok(CommonHeader header,
                                            @RequestParam String code,
                                            @RequestParam String state) {
        // 这里的 "code" 来自 URL 查询参数
        Assert.hasLength(code, "authCode cannot be null or empty");

        try {
            String tokenUrl = "https://open.tiktokapis.com/v2/oauth/token/";
            HttpResponse tokenResponse = HttpRequest.post(tokenUrl)
                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("Cache-Control", "no-cache")
                    .form("client_key", tiktokOAuthProperties.getClientKey())
                    .form("client_secret", tiktokOAuthProperties.getClientSecret())
                    .form("code", code)
                    .form("grant_type", "authorization_code")
                    .form("redirect_uri", tiktokOAuthProperties.getRedirectUri())
                    .execute();

            //{
            //    "access_token": "act.example12345Example12345Example",
            //    "expires_in": 86400,
            //    "open_id": "asdf-12345c-1a2s3d-ac98-asdf123as12as34",
            //    "refresh_expires_in": 31536000,
            //    "refresh_token": "rft.example12345Example12345Example",
            //    "scope": "user.info.basic,video.list",
            //    "token_type": "Bearer"
            //}
            //{
            //    "error": "invalid_request",
            //    "error_description": "The request parameters are malformed.",
            //    "log_id": "202206221854370101130062072500FFA2"
            //}
            JSONObject tokenRes = JSONUtil.parseObj(tokenResponse.body());

            log.info("TikTok换取access_token响应: {}", tokenRes);
            if (tokenRes.get("access_token") == null) {
                log.error("TikTok 换取 access_token 失败, 响应: {}", tokenRes);
                return R.status(STATUS_ERROR_AUTH, "tiktok auth failed");
            }
            String accessToken = tokenRes.getStr("access_token");
            String openId = tokenRes.getStr("open_id");
            if (StringUtils.isBlank(openId)) {
                return R.status(STATUS_ERROR_AUTH, "tiktok open_id empty");
            }
            // 2. 使用 access_token 获取用户信息
            String userInfoUrl = "https://open.tiktokapis.com/v2/user/info/";
            // 指定要获取的字段,可按需增删
            String fields = "open_id,union_id,avatar_url,display_name";

            // 发送 GET 请求
            HttpResponse response = HttpRequest.get(userInfoUrl + "?fields=" + fields)
                    .header("Authorization", "Bearer " + accessToken)
                    .execute();

            // 解析返回 JSON
            String userInfoBody = response.body();
            JSONObject userInfoJson = JSONUtil.parseObj(userInfoBody);
            //{
            //   "data":{
            //      "user":{
            //         "avatar_url":"https://p19-sign.tiktokcdn-us.com/tos-avt-0068-tx/b17f0e4b3a4f4a50993cf72cda8b88b8~c5_168x168.jpeg",
            //         "open_id":"723f24d7-e717-40f8-a2b6-cb8464cd23b4",
            //         "union_id":"c9c60f44-a68e-4f5d-84dd-ce22faeb0ba1"
            //      }
            //   },
            //   "error":{
            //      "code":"ok",
            //      "message":"",
            //      "log_id":"20220829194722CBE87ED59D524E727021"
            //   }
            //}
            log.info("TikTok获取用户信息响应: {}", userInfoJson);
            JSONObject userInfoData = userInfoJson.getJSONObject("data");

            // 获取 user
            JSONObject userObj = userInfoData.getJSONObject("user");
            //TikTok 第三方接口获取用户信息: user=
            // {display_name=xxxx, open_id=xxx,
            // union_id=c39504f0-e2a4-53a9-9b1e-693004c64c57,
            // avatar_url=https://p16-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/7b7fe5d30c056d5b54bd055dbc1b3b54~tplv-tiktokx-cropcenter:168:168.jpeg?dr=9638&refresh_token=9f0c3ad0&x-expires=1766134800&x-signature=reknXEW77ReXn3TNPDpozvAVOL8%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=8aecc5ac&idc=useast5}
            log.info("TikTok 第三方接口获取用户信息: user={}", userObj);

            String avatarUrl = userObj.getStr("avatar_url");
            String unionId = userObj.getStr("union_id");
            String displayName = userObj.getStr("display_name");

            // 3. 走统一的第三方登录流程
            ThirdParam thirdParam = new ThirdParam();
            thirdParam.setThirdId(openId);
            thirdParam.setThirdType(ThirdLoginEnum.TIKTOK.getDataFlag());
            thirdParam.setNickname(displayName);
            // unionId 暂不入库,如需可扩展 center 服务
            return R.ok(authService.loginThird(header, thirdParam));
        } catch (Exception e) {
            log.error("TikTok 登录异常, authCode={},msg:{}", code, e.getMessage());
            return R.status(STATUS_ERROR_AUTH, "tiktok auth exception");
        }
    }

 



posted @ 2025-12-18 09:10  Fyy发大财  阅读(1)  评论(0)    收藏  举报