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
|
||
}
|