Browse Source

tiktok回调

master
[yxf] 3 weeks ago
parent
commit
17149d5341
  1. 38
      im-admin/ruoyi-im/src/main/java/org/dromara/im/config/TikTokConfig.java
  2. 16
      im-admin/ruoyi-im/src/main/java/org/dromara/im/constant/TikTokConstant.java
  3. 95
      im-admin/ruoyi-im/src/main/java/org/dromara/im/controller/TikTokAuthController.java
  4. 30
      im-admin/ruoyi-im/src/main/java/org/dromara/im/controller/TikTokOauth.java
  5. 74
      im-admin/ruoyi-im/src/main/java/org/dromara/im/domain/TikTokTokenResponse.java
  6. 21
      im-admin/ruoyi-im/src/main/java/org/dromara/im/service/ITikTokAuthService.java
  7. 106
      im-admin/ruoyi-im/src/main/java/org/dromara/im/service/impl/TikTokAuthServiceImpl.java

38
im-admin/ruoyi-im/src/main/java/org/dromara/im/config/TikTokConfig.java

@ -0,0 +1,38 @@
package org.dromara.im.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "tiktok")
public class TikTokConfig {
// ========== 授权页面地址 ==========
/** ROW(非美国地区) */
public static final String AUTH_URL_ROW =
"https://seller.tiktokglobalshop.com/account/authorize";
/** US(美国地区) */
public static final String AUTH_URL_US =
"https://seller-us.tiktok.com/account/authorize";
// ========== API 地址 ==========
public static final String TOKEN_URL =
"https://open-api.tiktokglobalshop.com/token/create";
public static final String REFRESH_TOKEN_URL =
"https://open-api.tiktokglobalshop.com/token/refresh";
private String appKey;
private String appSecret;
private String redirectUri;
private String region;
// getter / setter
public String getAppKey() { return appKey; }
public void setAppKey(String v) { this.appKey = v; }
public String getAppSecret() { return appSecret; }
public void setAppSecret(String v) { this.appSecret = v; }
public String getRedirectUri(){ return redirectUri; }
public void setRedirectUri(String v){ this.redirectUri = v; }
public String getRegion() { return region; }
public void setRegion(String v) { this.region = v; }
}

16
im-admin/ruoyi-im/src/main/java/org/dromara/im/constant/TikTokConstant.java

@ -0,0 +1,16 @@
package org.dromara.im.constant;
public class TikTokConstant {
/** 授权码有效期:30分钟(毫秒) */
public static final long AUTH_CODE_EXPIRE_MS = 30 * 60 * 1000L;
/** access_token 默认有效期:7天(秒) */
public static final long ACCESS_TOKEN_EXPIRE_SEC = 7 * 24 * 60 * 60L;
/** grant_type: 获取 token */
public static final String GRANT_TYPE_AUTHORIZED = "authorized_code";
/** grant_type: 刷新 token */
public static final String GRANT_TYPE_REFRESH = "refresh_token";
}

95
im-admin/ruoyi-im/src/main/java/org/dromara/im/controller/TikTokAuthController.java

@ -0,0 +1,95 @@
package org.dromara.im.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import org.dromara.im.domain.TikTokTokenResponse;
import org.dromara.im.service.ITikTokAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.security.SecureRandom;
import java.util.HexFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/tiktok")
public class TikTokAuthController {
private static final Logger log = LoggerFactory.getLogger(TikTokAuthController.class);
private final ITikTokAuthService authService;
private final ConcurrentHashMap<String, Long> stateStore = new ConcurrentHashMap<>();
public TikTokAuthController(ITikTokAuthService authService) {
this.authService = authService;
}
@SaIgnore
@GetMapping("/authorize")
public String authorize() {
String state = generateState();
stateStore.put(state, System.currentTimeMillis());
String authUrl = authService.buildAuthUrl(state);
// 生产环境建议 302 跳转:
// return "redirect:" + authUrl;
return "请访问以下链接完成授权:\n" + authUrl;
}
@SaIgnore
@GetMapping("/callback")
public String callback(
@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "state", required = false) String state,
@RequestParam(value = "error", required = false) String error) {
// 1. 用户拒绝
if ("auth_denied".equals(error) || code == null || "null".equals(code)) {
return "用户拒绝了授权";
}
// 2. state 校验
if (state == null || !stateStore.containsKey(state)) {
return "state 校验失败,可能存在 CSRF 攻击";
}
stateStore.remove(state);
// 3. 用 code 换 token
log.info("收到授权回调, code={}, state={}", code, state);
TikTokTokenResponse tokenResp = authService.getAccessToken(code);
if (tokenResp.getCode() != 0) {
return "获取 Token 失败:" + tokenResp.getMessage();
}
TikTokTokenResponse.TokenData data = tokenResp.getData();
// 4. ⚠️ 生产环境应将 data 存入数据库/Redis,而非直接返回
return "授权成功!\n"
+ "卖家名称: " + data.getSellerName() + "\n"
+ "地区: " + data.getSellerBaseRegion() + "\n"
+ "open_id: " + data.getOpenId() + "\n"
+ "user_type: " + data.getUserType() + "\n"
+ "access_token: " + data.getAccessToken() + "\n"
+ "refresh_token: " + data.getRefreshToken() + "\n"
+ "access_token 过期(秒): " + data.getAccessTokenExpireIn() + "\n"
+ "refresh_token 过期(秒): " + data.getRefreshTokenExpireIn() + "\n"
+ "授权范围: " + String.join(", ", data.getGrantedScopes());
}
@SaIgnore
@GetMapping("/refresh")
public String refresh(@RequestParam String refreshToken) {
TikTokTokenResponse resp = authService.refreshToken(refreshToken);
if (resp.getCode() != 0) {
return "刷新失败:" + resp.getMessage();
}
return "刷新成功!新 access_token:" + resp.getData().getAccessToken();
}
private String generateState() {
byte[] bytes = new byte[16];
new SecureRandom().nextBytes(bytes);
return HexFormat.of().formatHex(bytes);
}
}

30
im-admin/ruoyi-im/src/main/java/org/dromara/im/controller/TikTokOauth.java

@ -1,30 +0,0 @@
package org.dromara.im.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* tiktok接口
*
* @author Blue
* @date 2026-04-07
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/tiktok")
public class TikTokOauth {
@SaIgnore
@GetMapping("/callback")
public void TikTokCallbackVO() {
}
}

74
im-admin/ruoyi-im/src/main/java/org/dromara/im/domain/TikTokTokenResponse.java

@ -0,0 +1,74 @@
package org.dromara.im.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
public class TikTokTokenResponse {
private int code;
private String message;
@JsonProperty("request_id")
private String requestId;
private TokenData data;
// getter / setter(同你原来的,省略空间)
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }
public TokenData getData() { return data; }
public void setData(TokenData data) { this.data = data; }
public static class TokenData {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("access_token_expire_in")
private long accessTokenExpireIn;
@JsonProperty("refresh_token")
private String refreshToken;
@JsonProperty("refresh_token_expire_in")
private long refreshTokenExpireIn;
@JsonProperty("open_id")
private String openId;
@JsonProperty("seller_name")
private String sellerName;
@JsonProperty("seller_base_region")
private String sellerBaseRegion;
@JsonProperty("user_type")
private int userType;
@JsonProperty("granted_scopes")
private String[] grantedScopes;
public String getAccessToken() { return accessToken; }
public void setAccessToken(String v) { this.accessToken = v; }
public long getAccessTokenExpireIn() { return accessTokenExpireIn; }
public void setAccessTokenExpireIn(long v) { this.accessTokenExpireIn = v; }
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String v) { this.refreshToken = v; }
public long getRefreshTokenExpireIn() { return refreshTokenExpireIn; }
public void setRefreshTokenExpireIn(long v) { this.refreshTokenExpireIn = v; }
public String getOpenId() { return openId; }
public void setOpenId(String v) { this.openId = v; }
public String getSellerName() { return sellerName; }
public void setSellerName(String v) { this.sellerName = v; }
public String getSellerBaseRegion() { return sellerBaseRegion; }
public void setSellerBaseRegion(String v) { this.sellerBaseRegion = v; }
public int getUserType() { return userType; }
public void setUserType(int v) { this.userType = v; }
public String[] getGrantedScopes() { return grantedScopes; }
public void setGrantedScopes(String[] v) { this.grantedScopes = v; }
}
}

21
im-admin/ruoyi-im/src/main/java/org/dromara/im/service/ITikTokAuthService.java

@ -0,0 +1,21 @@
package org.dromara.im.service;
import org.dromara.im.domain.TikTokTokenResponse;
public interface ITikTokAuthService {
/**
* 生成授权链接
*/
String buildAuthUrl(String state);
/**
* auth_code 换取 access_token
*/
TikTokTokenResponse getAccessToken(String authCode);
/**
* 刷新 access_token
*/
TikTokTokenResponse refreshToken(String refreshToken);
}

106
im-admin/ruoyi-im/src/main/java/org/dromara/im/service/impl/TikTokAuthServiceImpl.java

@ -0,0 +1,106 @@
package org.dromara.im.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.im.config.TikTokConfig;
import org.dromara.im.constant.TikTokConstant;
import org.dromara.im.domain.TikTokTokenResponse;
import org.dromara.im.service.ITikTokAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
@Service
public class TikTokAuthServiceImpl implements ITikTokAuthService {
private static final Logger log = LoggerFactory.getLogger(TikTokAuthServiceImpl.class);
private final TikTokConfig config;
private final ObjectMapper objectMapper = new ObjectMapper();
private final HttpClient httpClient = HttpClient.newHttpClient();
public TikTokAuthServiceImpl(TikTokConfig config) {
this.config = config;
}
@Override
public String buildAuthUrl(String state) {
// ── 修复1: 使用正确的授权页面地址 + 补齐 redirect_uri ──
String baseUrl = "US".equalsIgnoreCase(config.getRegion())
? TikTokConfig.AUTH_URL_US
: TikTokConfig.AUTH_URL_ROW;
return baseUrl
+ "?app_key=" + config.getAppKey()
+ "&redirect_uri=" + config.getRedirectUri()
+ "&state=" + state;
}
@Override
public TikTokTokenResponse getAccessToken(String authCode) {
// ── 修复2: POST + JSON body ──
String jsonBody = """
{
"app_key": "%s",
"app_secret": "%s",
"auth_code": "%s",
"grant_type": "%s"
}
""".formatted(
config.getAppKey(),
config.getAppSecret(),
authCode,
TikTokConstant.GRANT_TYPE_AUTHORIZED);
return doPostJson(TikTokConfig.TOKEN_URL, jsonBody);
}
@Override
public TikTokTokenResponse refreshToken(String refreshToken) {
String jsonBody = """
{
"app_key": "%s",
"app_secret": "%s",
"refresh_token": "%s",
"grant_type": "%s"
}
""".formatted(
config.getAppKey(),
config.getAppSecret(),
refreshToken,
TikTokConstant.GRANT_TYPE_REFRESH);
return doPostJson(TikTokConfig.REFRESH_TOKEN_URL, jsonBody);
}
/**
* 统一 POST JSON 请求
*/
private TikTokTokenResponse doPostJson(String url, String jsonBody) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
log.info("TikTok API 响应: {}", response.body());
return objectMapper.readValue(response.body(), TikTokTokenResponse.class);
} catch (Exception e) {
log.error("TikTok API 请求失败", e);
TikTokTokenResponse errorResp = new TikTokTokenResponse();
errorResp.setCode(-1);
errorResp.setMessage("请求失败: " + e.getMessage());
return errorResp;
}
}
}
Loading…
Cancel
Save