package providers import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "time" ) // QQProvider QQ OAuth提供者 type QQProvider struct { AppID string AppKey string RedirectURI string } // QQAuthURLResponse QQ授权URL响应 type QQAuthURLResponse struct { URL string `json:"url"` State string `json:"state"` Redirect string `json:"redirect,omitempty"` } // QQTokenResponse QQ Token响应 type QQTokenResponse struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` } // QQOpenIDResponse QQ OpenID响应 type QQOpenIDResponse struct { ClientID string `json:"client_id"` OpenID string `json:"openid"` } // QQUserInfo QQ用户信息 type QQUserInfo struct { Ret int `json:"ret"` Msg string `json:"msg"` Nickname string `json:"nickname"` Gender string `json:"gender"` // 男, 女 Province string `json:"province"` City string `json:"city"` Year string `json:"year"` FigureURL string `json:"figureurl"` FigureURL1 string `json:"figureurl_1"` FigureURL2 string `json:"figureurl_2"` } // NewQQProvider 创建QQ OAuth提供者 func NewQQProvider(appID, appKey, redirectURI string) *QQProvider { return &QQProvider{ AppID: appID, AppKey: appKey, RedirectURI: redirectURI, } } // GenerateState 生成随机状态码 func (q *QQProvider) GenerateState() (string, error) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } // GetAuthURL 获取QQ授权URL func (q *QQProvider) GetAuthURL(state string) (*QQAuthURLResponse, error) { authURL := fmt.Sprintf( "https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=get_user_info&state=%s", q.AppID, url.QueryEscape(q.RedirectURI), state, ) return &QQAuthURLResponse{ URL: authURL, State: state, Redirect: q.RedirectURI, }, nil } // ExchangeCode 用授权码换取访问令牌 func (q *QQProvider) ExchangeCode(ctx context.Context, code string) (*QQTokenResponse, error) { tokenURL := fmt.Sprintf( "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s&fmt=json", q.AppID, q.AppKey, code, url.QueryEscape(q.RedirectURI), ) req, err := http.NewRequestWithContext(ctx, "GET", tokenURL, 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 tokenResp QQTokenResponse if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("parse token response failed: %w", err) } return &tokenResp, nil } // GetOpenID 用访问令牌获取OpenID func (q *QQProvider) GetOpenID(ctx context.Context, accessToken string) (*QQOpenIDResponse, error) { openIDURL := fmt.Sprintf( "https://graph.qq.com/oauth2.0/me?access_token=%s&fmt=json", accessToken, ) req, err := http.NewRequestWithContext(ctx, "GET", openIDURL, 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 openIDResp QQOpenIDResponse if err := json.Unmarshal(body, &openIDResp); err != nil { return nil, fmt.Errorf("parse openid response failed: %w", err) } return &openIDResp, nil } // GetUserInfo 获取QQ用户信息 func (q *QQProvider) GetUserInfo(ctx context.Context, accessToken, openID string) (*QQUserInfo, error) { userInfoURL := fmt.Sprintf( "https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s&format=json", accessToken, q.AppID, openID, ) 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 QQUserInfo if err := json.Unmarshal(body, &userInfo); err != nil { return nil, fmt.Errorf("parse user info failed: %w", err) } if userInfo.Ret != 0 { return nil, fmt.Errorf("qq api error: %s", userInfo.Msg) } return &userInfo, nil } // ValidateToken 验证访问令牌是否有效 func (q *QQProvider) ValidateToken(ctx context.Context, accessToken string) (bool, error) { _, err := q.GetOpenID(ctx, accessToken) if err != nil { return false, err } return true, nil }