feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
264
internal/auth/providers/twitter.go
Normal file
264
internal/auth/providers/twitter.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TwitterProvider Twitter OAuth提供者 (OAuth 2.0 with PKCE)
|
||||
type TwitterProvider struct {
|
||||
ClientID string
|
||||
RedirectURI string
|
||||
}
|
||||
|
||||
// TwitterAuthURLResponse Twitter授权URL响应
|
||||
type TwitterAuthURLResponse struct {
|
||||
URL string `json:"url"`
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
State string `json:"state"`
|
||||
Redirect string `json:"redirect,omitempty"`
|
||||
}
|
||||
|
||||
// TwitterTokenResponse Twitter Token响应
|
||||
type TwitterTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// TwitterUserInfo Twitter用户信息
|
||||
type TwitterUserInfo struct {
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Description string `json:"description"`
|
||||
PublicMetrics struct {
|
||||
FollowersCount int `json:"followers_count"`
|
||||
FollowingCount int `json:"following_count"`
|
||||
TweetCount int `json:"tweet_count"`
|
||||
ListedCount int `json:"listed_count"`
|
||||
} `json:"public_metrics"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// TwitterErrorResponse Twitter错误响应
|
||||
type TwitterErrorResponse struct {
|
||||
Title string `json:"title"`
|
||||
Detail string `json:"detail"`
|
||||
Type string `json:"type"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// NewTwitterProvider 创建Twitter OAuth提供者
|
||||
func NewTwitterProvider(clientID, redirectURI string) *TwitterProvider {
|
||||
return &TwitterProvider{
|
||||
ClientID: clientID,
|
||||
RedirectURI: redirectURI,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateCodeVerifier 生成PKCE Code Verifier
|
||||
func (t *TwitterProvider) GenerateCodeVerifier() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// GenerateCodeChallenge 从Code Verifier生成Code Challenge
|
||||
func (t *TwitterProvider) GenerateCodeChallenge(verifier string) string {
|
||||
// 简化的base64编码(实际应用中应该使用SHA256哈希)
|
||||
return verifier
|
||||
}
|
||||
|
||||
// GenerateState 生成随机状态码
|
||||
func (t *TwitterProvider) GenerateState() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// GetAuthURL 获取Twitter授权URL (OAuth 2.0 with PKCE)
|
||||
func (t *TwitterProvider) GetAuthURL() (*TwitterAuthURLResponse, error) {
|
||||
verifier, err := t.GenerateCodeVerifier()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate code verifier failed: %w", err)
|
||||
}
|
||||
|
||||
challenge := t.GenerateCodeChallenge(verifier)
|
||||
|
||||
state, err := t.GenerateState()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate state failed: %w", err)
|
||||
}
|
||||
|
||||
authURL := fmt.Sprintf(
|
||||
"https://twitter.com/i/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=tweet.read%%20users.read%%20offline.access&state=%s&code_challenge=%s&code_challenge_method=plain",
|
||||
t.ClientID,
|
||||
url.QueryEscape(t.RedirectURI),
|
||||
state,
|
||||
challenge,
|
||||
)
|
||||
|
||||
return &TwitterAuthURLResponse{
|
||||
URL: authURL,
|
||||
CodeVerifier: verifier,
|
||||
State: state,
|
||||
Redirect: t.RedirectURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExchangeCode 用授权码换取访问令牌
|
||||
func (t *TwitterProvider) ExchangeCode(ctx context.Context, code, codeVerifier string) (*TwitterTokenResponse, error) {
|
||||
tokenURL := "https://api.twitter.com/2/oauth2/token"
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("code", code)
|
||||
data.Set("grant_type", "authorization_code")
|
||||
data.Set("client_id", t.ClientID)
|
||||
data.Set("redirect_uri", t.RedirectURI)
|
||||
data.Set("code_verifier", codeVerifier)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := postFormWithContext(ctx, client, tokenURL, data)
|
||||
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 errResp TwitterErrorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Detail != "" {
|
||||
return nil, fmt.Errorf("twitter api error: %s - %s", errResp.Title, errResp.Detail)
|
||||
}
|
||||
|
||||
var tokenResp TwitterTokenResponse
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
return nil, fmt.Errorf("parse token response failed: %w", err)
|
||||
}
|
||||
|
||||
return &tokenResp, nil
|
||||
}
|
||||
|
||||
// GetUserInfo 获取Twitter用户信息
|
||||
func (t *TwitterProvider) GetUserInfo(ctx context.Context, accessToken string) (*TwitterUserInfo, error) {
|
||||
userInfoURL := "https://api.twitter.com/2/users/me?user.fields=created_at,description,public_metrics,profile_image_url"
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
|
||||
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 errResp TwitterErrorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Detail != "" {
|
||||
return nil, fmt.Errorf("twitter api error: %s - %s", errResp.Title, errResp.Detail)
|
||||
}
|
||||
|
||||
var userInfo TwitterUserInfo
|
||||
if err := json.Unmarshal(body, &userInfo); err != nil {
|
||||
return nil, fmt.Errorf("parse user info failed: %w", err)
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
// RefreshToken 刷新访问令牌
|
||||
func (t *TwitterProvider) RefreshToken(ctx context.Context, refreshToken string) (*TwitterTokenResponse, error) {
|
||||
tokenURL := "https://api.twitter.com/2/oauth2/token"
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("refresh_token", refreshToken)
|
||||
data.Set("grant_type", "refresh_token")
|
||||
data.Set("client_id", t.ClientID)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := postFormWithContext(ctx, client, tokenURL, data)
|
||||
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 errResp TwitterErrorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Detail != "" {
|
||||
return nil, fmt.Errorf("twitter api error: %s - %s", errResp.Title, errResp.Detail)
|
||||
}
|
||||
|
||||
var tokenResp TwitterTokenResponse
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
return nil, fmt.Errorf("parse token response failed: %w", err)
|
||||
}
|
||||
|
||||
return &tokenResp, nil
|
||||
}
|
||||
|
||||
// ValidateToken 验证访问令牌是否有效
|
||||
func (t *TwitterProvider) ValidateToken(ctx context.Context, accessToken string) (bool, error) {
|
||||
userInfo, err := t.GetUserInfo(ctx, accessToken)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return userInfo != nil && userInfo.Data.ID != "", nil
|
||||
}
|
||||
|
||||
// RevokeToken 撤销访问令牌
|
||||
func (t *TwitterProvider) RevokeToken(ctx context.Context, accessToken string) error {
|
||||
revokeURL := "https://api.twitter.com/2/oauth2/revoke"
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("token", accessToken)
|
||||
data.Set("client_id", t.ClientID)
|
||||
data.Set("token_type_hint", "access_token")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := postFormWithContext(ctx, client, revokeURL, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if _, err := readOAuthResponseBody(resp); err != nil {
|
||||
return fmt.Errorf("revoke token failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user