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 }