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 }