Files

202 lines
5.4 KiB
Go
Raw Permalink Normal View History

package providers
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
// WeiboProvider 微博OAuth提供者
type WeiboProvider struct {
AppKey string
AppSecret string
RedirectURI string
}
// WeiboAuthURLResponse 微博授权URL响应
type WeiboAuthURLResponse struct {
URL string `json:"url"`
State string `json:"state"`
Redirect string `json:"redirect,omitempty"`
}
// WeiboTokenResponse 微博Token响应
type WeiboTokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RemindIn string `json:"remind_in"`
UID string `json:"uid"`
}
// WeiboUserInfo 微博用户信息
type WeiboUserInfo struct {
ID int64 `json:"id"`
IDStr string `json:"idstr"`
ScreenName string `json:"screen_name"`
Name string `json:"name"`
Province string `json:"province"`
City string `json:"city"`
Location string `json:"location"`
Description string `json:"description"`
URL string `json:"url"`
ProfileImageURL string `json:"profile_image_url"`
Gender string `json:"gender"` // m:男, f:女, n:未知
FollowersCount int `json:"followers_count"`
FriendsCount int `json:"friends_count"`
StatusesCount int `json:"statuses_count"`
}
// NewWeiboProvider 创建微博OAuth提供者
func NewWeiboProvider(appKey, appSecret, redirectURI string) *WeiboProvider {
return &WeiboProvider{
AppKey: appKey,
AppSecret: appSecret,
RedirectURI: redirectURI,
}
}
// GenerateState 生成随机状态码
func (w *WeiboProvider) GenerateState() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// GetAuthURL 获取微博授权URL
func (w *WeiboProvider) GetAuthURL(state string) (*WeiboAuthURLResponse, error) {
authURL := fmt.Sprintf(
"https://api.weibo.com/oauth2/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s",
w.AppKey,
url.QueryEscape(w.RedirectURI),
state,
)
return &WeiboAuthURLResponse{
URL: authURL,
State: state,
Redirect: w.RedirectURI,
}, nil
}
// ExchangeCode 用授权码换取访问令牌
func (w *WeiboProvider) ExchangeCode(ctx context.Context, code string) (*WeiboTokenResponse, error) {
tokenURL := "https://api.weibo.com/oauth2/access_token"
data := url.Values{}
data.Set("client_id", w.AppKey)
data.Set("client_secret", w.AppSecret)
data.Set("grant_type", "authorization_code")
data.Set("code", code)
data.Set("redirect_uri", w.RedirectURI)
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 tokenResp WeiboTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("parse token response failed: %w", err)
}
return &tokenResp, nil
}
// GetUserInfo 获取微博用户信息
func (w *WeiboProvider) GetUserInfo(ctx context.Context, accessToken, uid string) (*WeiboUserInfo, error) {
userInfoURL := fmt.Sprintf(
"https://api.weibo.com/2/users/show.json?access_token=%s&uid=%s",
accessToken,
uid,
)
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 errResp struct {
Error int `json:"error"`
ErrorCode int `json:"error_code"`
Request string `json:"request"`
}
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Error != 0 {
return nil, fmt.Errorf("weibo api error: code=%d", errResp.ErrorCode)
}
var userInfo WeiboUserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("parse user info failed: %w", err)
}
return &userInfo, nil
}
// ValidateToken 验证访问令牌是否有效
func (w *WeiboProvider) ValidateToken(ctx context.Context, accessToken string) (bool, error) {
// 微博没有专门的token验证接口通过获取API token信息来验证
tokenInfoURL := fmt.Sprintf("https://api.weibo.com/oauth2/get_token_info?access_token=%s", accessToken)
req, err := http.NewRequestWithContext(ctx, "GET", tokenInfoURL, nil)
if err != nil {
return false, fmt.Errorf("create request failed: %w", err)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := readOAuthResponseBody(resp)
if err != nil {
return false, fmt.Errorf("read response failed: %w", err)
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return false, fmt.Errorf("parse response failed: %w", err)
}
// 如果返回了错误说明token无效
if _, ok := result["error"]; ok {
return false, nil
}
// 如果有expire_in字段说明token有效
if _, ok := result["expire_in"]; ok {
return true, nil
}
return false, nil
}