Files

202 lines
5.4 KiB
Go
Raw Permalink 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"
"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
}