208 lines
5.6 KiB
Go
208 lines
5.6 KiB
Go
|
|
package providers
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"crypto/rand"
|
|||
|
|
"encoding/base64"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"net/http"
|
|||
|
|
"net/url"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// FacebookProvider Facebook OAuth提供者
|
|||
|
|
type FacebookProvider struct {
|
|||
|
|
AppID string
|
|||
|
|
AppSecret string
|
|||
|
|
RedirectURI string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FacebookAuthURLResponse Facebook授权URL响应
|
|||
|
|
type FacebookAuthURLResponse struct {
|
|||
|
|
URL string `json:"url"`
|
|||
|
|
State string `json:"state"`
|
|||
|
|
Redirect string `json:"redirect,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FacebookTokenResponse Facebook Token响应
|
|||
|
|
type FacebookTokenResponse struct {
|
|||
|
|
AccessToken string `json:"access_token"`
|
|||
|
|
TokenType string `json:"token_type"`
|
|||
|
|
ExpiresIn int `json:"expires_in"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FacebookUserInfo Facebook用户信息
|
|||
|
|
type FacebookUserInfo struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
Email string `json:"email"`
|
|||
|
|
Picture struct {
|
|||
|
|
Data struct {
|
|||
|
|
URL string `json:"url"`
|
|||
|
|
Width int `json:"width"`
|
|||
|
|
Height int `json:"height"`
|
|||
|
|
IsSilhouette bool `json:"is_silhouette"`
|
|||
|
|
} `json:"data"`
|
|||
|
|
} `json:"picture"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewFacebookProvider 创建Facebook OAuth提供者
|
|||
|
|
func NewFacebookProvider(appID, appSecret, redirectURI string) *FacebookProvider {
|
|||
|
|
return &FacebookProvider{
|
|||
|
|
AppID: appID,
|
|||
|
|
AppSecret: appSecret,
|
|||
|
|
RedirectURI: redirectURI,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GenerateState 生成随机状态码
|
|||
|
|
func (f *FacebookProvider) GenerateState() (string, error) {
|
|||
|
|
b := make([]byte, 32)
|
|||
|
|
_, err := rand.Read(b)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
return base64.URLEncoding.EncodeToString(b), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetAuthURL 获取Facebook授权URL
|
|||
|
|
func (f *FacebookProvider) GetAuthURL(state string) (*FacebookAuthURLResponse, error) {
|
|||
|
|
authURL := fmt.Sprintf(
|
|||
|
|
"https://www.facebook.com/v18.0/dialog/oauth?client_id=%s&redirect_uri=%s&scope=email,public_profile&response_type=code&state=%s",
|
|||
|
|
f.AppID,
|
|||
|
|
url.QueryEscape(f.RedirectURI),
|
|||
|
|
state,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return &FacebookAuthURLResponse{
|
|||
|
|
URL: authURL,
|
|||
|
|
State: state,
|
|||
|
|
Redirect: f.RedirectURI,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExchangeCode 用授权码换取访问令牌
|
|||
|
|
func (f *FacebookProvider) ExchangeCode(ctx context.Context, code string) (*FacebookTokenResponse, error) {
|
|||
|
|
tokenURL := fmt.Sprintf(
|
|||
|
|
"https://graph.facebook.com/v18.0/oauth/access_token?client_id=%s&client_secret=%s&redirect_uri=%s&code=%s",
|
|||
|
|
f.AppID,
|
|||
|
|
f.AppSecret,
|
|||
|
|
url.QueryEscape(f.RedirectURI),
|
|||
|
|
code,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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 FacebookTokenResponse
|
|||
|
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
|||
|
|
return nil, fmt.Errorf("parse token response failed: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &tokenResp, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetUserInfo 获取Facebook用户信息
|
|||
|
|
func (f *FacebookProvider) GetUserInfo(ctx context.Context, accessToken string) (*FacebookUserInfo, error) {
|
|||
|
|
// 请求用户信息(包括头像)
|
|||
|
|
userInfoURL := fmt.Sprintf(
|
|||
|
|
"https://graph.facebook.com/v18.0/me?fields=id,name,email,picture&access_token=%s",
|
|||
|
|
accessToken,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Facebook错误响应
|
|||
|
|
var errResp struct {
|
|||
|
|
Error struct {
|
|||
|
|
Message string `json:"message"`
|
|||
|
|
Type string `json:"type"`
|
|||
|
|
Code int `json:"code"`
|
|||
|
|
ErrorSubcode int `json:"error_subcode,omitempty"`
|
|||
|
|
} `json:"error"`
|
|||
|
|
}
|
|||
|
|
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Error.Message != "" {
|
|||
|
|
return nil, fmt.Errorf("facebook api error: %s", errResp.Error.Message)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var userInfo FacebookUserInfo
|
|||
|
|
if err := json.Unmarshal(body, &userInfo); err != nil {
|
|||
|
|
return nil, fmt.Errorf("parse user info failed: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &userInfo, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ValidateToken 验证访问令牌是否有效
|
|||
|
|
func (f *FacebookProvider) ValidateToken(ctx context.Context, accessToken string) (bool, error) {
|
|||
|
|
userInfo, err := f.GetUserInfo(ctx, accessToken)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
return userInfo != nil && userInfo.ID != "", nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetLongLivedToken 获取长期有效的访问令牌(60天)
|
|||
|
|
func (f *FacebookProvider) GetLongLivedToken(ctx context.Context, shortLivedToken string) (*FacebookTokenResponse, error) {
|
|||
|
|
tokenURL := fmt.Sprintf(
|
|||
|
|
"https://graph.facebook.com/v18.0/oauth/access_token?grant_type=fb_exchange_token&client_id=%s&client_secret=%s&fb_exchange_token=%s",
|
|||
|
|
f.AppID,
|
|||
|
|
f.AppSecret,
|
|||
|
|
shortLivedToken,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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 FacebookTokenResponse
|
|||
|
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
|||
|
|
return nil, fmt.Errorf("parse token response failed: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &tokenResp, nil
|
|||
|
|
}
|