203 lines
5.1 KiB
Go
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
|
||
|
|
}
|