Files
user-system/internal/auth/providers/douyin.go

139 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package providers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// DouyinProvider 抖音 OAuth提供者
// 抖音 OAuth 文档https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/account-permission/get-access-token
type DouyinProvider struct {
ClientKey string // 抖音开放平台 client_key
ClientSecret string // 抖音开放平台 client_secret
RedirectURI string
}
// DouyinTokenResponse 抖音 Token响应
type DouyinTokenResponse struct {
Data struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
RefreshExpiresIn int `json:"refresh_expires_in"`
OpenID string `json:"open_id"`
Scope string `json:"scope"`
} `json:"data"`
Message string `json:"message"`
}
// DouyinUserInfo 抖音用户信息
type DouyinUserInfo struct {
Data struct {
OpenID string `json:"open_id"`
UnionID string `json:"union_id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Gender int `json:"gender"` // 0:未知 1:男 2:女
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
} `json:"data"`
Message string `json:"message"`
}
// NewDouyinProvider 创建抖音 OAuth提供者
func NewDouyinProvider(clientKey, clientSecret, redirectURI string) *DouyinProvider {
return &DouyinProvider{
ClientKey: clientKey,
ClientSecret: clientSecret,
RedirectURI: redirectURI,
}
}
// GetAuthURL 获取抖音授权URL
func (d *DouyinProvider) GetAuthURL(state string) (string, error) {
authURL := fmt.Sprintf(
"https://open.douyin.com/platform/oauth/connect?client_key=%s&redirect_uri=%s&response_type=code&scope=user_info&state=%s",
d.ClientKey,
url.QueryEscape(d.RedirectURI),
url.QueryEscape(state),
)
return authURL, nil
}
// ExchangeCode 用授权码换取 access_token
func (d *DouyinProvider) ExchangeCode(ctx context.Context, code string) (*DouyinTokenResponse, error) {
tokenURL := "https://open.douyin.com/oauth/access_token/"
data := url.Values{}
data.Set("client_key", d.ClientKey)
data.Set("client_secret", d.ClientSecret)
data.Set("code", code)
data.Set("grant_type", "authorization_code")
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL,
strings.NewReader(data.Encode()))
if err != nil {
return nil, fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := readOAuthResponseBody(resp)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
var tokenResp DouyinTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("parse token response failed: %w", err)
}
if tokenResp.Data.AccessToken == "" {
return nil, fmt.Errorf("抖音 OAuth: %s", tokenResp.Message)
}
return &tokenResp, nil
}
// GetUserInfo 获取抖音用户信息
func (d *DouyinProvider) GetUserInfo(ctx context.Context, accessToken, openID string) (*DouyinUserInfo, error) {
userInfoURL := fmt.Sprintf("https://open.douyin.com/oauth/userinfo/?open_id=%s&access_token=%s",
url.QueryEscape(openID), url.QueryEscape(accessToken))
req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL, nil)
if err != nil {
return nil, fmt.Errorf("create request failed: %w", err)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := readOAuthResponseBody(resp)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
var userInfo DouyinUserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("parse user info failed: %w", err)
}
return &userInfo, nil
}