259 lines
7.0 KiB
Go
259 lines
7.0 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
// WeChatProvider 微信OAuth提供者
|
|
type WeChatProvider struct {
|
|
AppID string
|
|
AppSecret string
|
|
Type string // "web" for 扫码登录, "mp" for 公众号, "mini" for 小程序
|
|
}
|
|
|
|
// WeChatAuthURLResponse 获取授权URL响应
|
|
type WeChatAuthURLResponse struct {
|
|
URL string `json:"url"`
|
|
State string `json:"state"`
|
|
Redirect string `json:"redirect,omitempty"`
|
|
}
|
|
|
|
// WeChatTokenResponse 微信Token响应
|
|
type WeChatTokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
OpenID string `json:"openid"`
|
|
Scope string `json:"scope"`
|
|
UnionID string `json:"unionid,omitempty"`
|
|
}
|
|
|
|
// WeChatUserInfo 微信用户信息
|
|
type WeChatUserInfo struct {
|
|
OpenID string `json:"openid"`
|
|
Nickname string `json:"nickname"`
|
|
Sex int `json:"sex"` // 1男性, 2女性, 0未知
|
|
Province string `json:"province"`
|
|
City string `json:"city"`
|
|
Country string `json:"country"`
|
|
HeadImgURL string `json:"headimgurl"`
|
|
UnionID string `json:"unionid,omitempty"`
|
|
}
|
|
|
|
// WeChatErrorCode 微信错误码
|
|
type WeChatErrorCode struct {
|
|
ErrCode int `json:"errcode"`
|
|
ErrMsg string `json:"errmsg"`
|
|
}
|
|
|
|
// NewWeChatProvider 创建微信OAuth提供者
|
|
func NewWeChatProvider(appID, appSecret, oAuthType string) *WeChatProvider {
|
|
return &WeChatProvider{
|
|
AppID: appID,
|
|
AppSecret: appSecret,
|
|
Type: oAuthType,
|
|
}
|
|
}
|
|
|
|
// GenerateState 生成随机状态码
|
|
func (w *WeChatProvider) GenerateState() (string, error) {
|
|
b := make([]byte, 32)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.URLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// GetAuthURL 获取微信授权URL
|
|
func (w *WeChatProvider) GetAuthURL(redirectURI, state string) (*WeChatAuthURLResponse, error) {
|
|
var authURL string
|
|
|
|
switch w.Type {
|
|
case "web":
|
|
// 微信扫码登录 (开放平台)
|
|
authURL = fmt.Sprintf(
|
|
"https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect",
|
|
w.AppID,
|
|
url.QueryEscape(redirectURI),
|
|
state,
|
|
)
|
|
case "mp":
|
|
// 微信公众号登录
|
|
authURL = fmt.Sprintf(
|
|
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect",
|
|
w.AppID,
|
|
url.QueryEscape(redirectURI),
|
|
state,
|
|
)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported wechat oauth type: %s", w.Type)
|
|
}
|
|
|
|
return &WeChatAuthURLResponse{
|
|
URL: authURL,
|
|
State: state,
|
|
Redirect: redirectURI,
|
|
}, nil
|
|
}
|
|
|
|
// ExchangeCode 用授权码换取访问令牌
|
|
func (w *WeChatProvider) ExchangeCode(ctx context.Context, code string) (*WeChatTokenResponse, error) {
|
|
tokenURL := fmt.Sprintf(
|
|
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
|
|
w.AppID,
|
|
w.AppSecret,
|
|
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 errResp WeChatErrorCode
|
|
if err := json.Unmarshal(body, &errResp); err == nil && errResp.ErrCode != 0 {
|
|
return nil, fmt.Errorf("wechat api error: %d - %s", errResp.ErrCode, errResp.ErrMsg)
|
|
}
|
|
|
|
var tokenResp WeChatTokenResponse
|
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
|
return nil, fmt.Errorf("parse token response failed: %w", err)
|
|
}
|
|
|
|
return &tokenResp, nil
|
|
}
|
|
|
|
// GetUserInfo 获取微信用户信息
|
|
func (w *WeChatProvider) GetUserInfo(ctx context.Context, accessToken, openID string) (*WeChatUserInfo, error) {
|
|
userInfoURL := fmt.Sprintf(
|
|
"https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN",
|
|
accessToken,
|
|
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 errResp WeChatErrorCode
|
|
if err := json.Unmarshal(body, &errResp); err == nil && errResp.ErrCode != 0 {
|
|
return nil, fmt.Errorf("wechat api error: %d - %s", errResp.ErrCode, errResp.ErrMsg)
|
|
}
|
|
|
|
var userInfo WeChatUserInfo
|
|
if err := json.Unmarshal(body, &userInfo); err != nil {
|
|
return nil, fmt.Errorf("parse user info failed: %w", err)
|
|
}
|
|
|
|
return &userInfo, nil
|
|
}
|
|
|
|
// RefreshToken 刷新访问令牌
|
|
func (w *WeChatProvider) RefreshToken(ctx context.Context, refreshToken string) (*WeChatTokenResponse, error) {
|
|
refreshURL := fmt.Sprintf(
|
|
"https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s",
|
|
w.AppID,
|
|
refreshToken,
|
|
)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", refreshURL, 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 errResp WeChatErrorCode
|
|
if err := json.Unmarshal(body, &errResp); err == nil && errResp.ErrCode != 0 {
|
|
return nil, fmt.Errorf("wechat api error: %d - %s", errResp.ErrCode, errResp.ErrMsg)
|
|
}
|
|
|
|
var tokenResp WeChatTokenResponse
|
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
|
return nil, fmt.Errorf("parse token response failed: %w", err)
|
|
}
|
|
|
|
return &tokenResp, nil
|
|
}
|
|
|
|
// ValidateToken 验证访问令牌是否有效
|
|
func (w *WeChatProvider) ValidateToken(ctx context.Context, accessToken, openID string) (bool, error) {
|
|
validateURL := fmt.Sprintf(
|
|
"https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s",
|
|
accessToken,
|
|
openID,
|
|
)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", validateURL, nil)
|
|
if err != nil {
|
|
return false, fmt.Errorf("create request failed: %w", err)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := readOAuthResponseBody(resp)
|
|
if err != nil {
|
|
return false, fmt.Errorf("read response failed: %w", err)
|
|
}
|
|
|
|
var result struct {
|
|
ErrCode int `json:"errcode"`
|
|
ErrMsg string `json:"errmsg"`
|
|
}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return false, fmt.Errorf("parse response failed: %w", err)
|
|
}
|
|
|
|
return result.ErrCode == 0, nil
|
|
}
|