Files

203 lines
5.1 KiB
Go

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
}