feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
256
internal/auth/providers/alipay.go
Normal file
256
internal/auth/providers/alipay.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AlipayProvider 支付宝 OAuth提供者
|
||||
// 支付宝使用 RSA2 签名(SHA256withRSA)
|
||||
type AlipayProvider struct {
|
||||
AppID string
|
||||
PrivateKey string // RSA2 私钥(PKCS#8 PEM格式)
|
||||
RedirectURI string
|
||||
IsSandbox bool
|
||||
}
|
||||
|
||||
// AlipayTokenResponse 支付宝 Token响应
|
||||
type AlipayTokenResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// AlipayUserInfo 支付宝用户信息
|
||||
type AlipayUserInfo struct {
|
||||
UserID string `json:"user_id"`
|
||||
Nickname string `json:"nick_name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
|
||||
// NewAlipayProvider 创建支付宝 OAuth提供者
|
||||
func NewAlipayProvider(appID, privateKey, redirectURI string, isSandbox bool) *AlipayProvider {
|
||||
return &AlipayProvider{
|
||||
AppID: appID,
|
||||
PrivateKey: privateKey,
|
||||
RedirectURI: redirectURI,
|
||||
IsSandbox: isSandbox,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlipayProvider) getGateway() string {
|
||||
if a.IsSandbox {
|
||||
return "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
|
||||
}
|
||||
return "https://openapi.alipay.com/gateway.do"
|
||||
}
|
||||
|
||||
// GetAuthURL 获取支付宝授权URL
|
||||
func (a *AlipayProvider) GetAuthURL(state string) (string, error) {
|
||||
authURL := fmt.Sprintf(
|
||||
"https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?app_id=%s&scope=auth_user&redirect_uri=%s&state=%s",
|
||||
a.AppID,
|
||||
url.QueryEscape(a.RedirectURI),
|
||||
url.QueryEscape(state),
|
||||
)
|
||||
return authURL, nil
|
||||
}
|
||||
|
||||
// ExchangeCode 用授权码换取 access_token
|
||||
func (a *AlipayProvider) ExchangeCode(ctx context.Context, code string) (*AlipayTokenResponse, error) {
|
||||
params := map[string]string{
|
||||
"app_id": a.AppID,
|
||||
"method": "alipay.system.oauth.token",
|
||||
"charset": "UTF-8",
|
||||
"sign_type": "RSA2",
|
||||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"version": "1.0",
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
}
|
||||
|
||||
if a.PrivateKey != "" {
|
||||
sign, err := a.signParams(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign failed: %w", err)
|
||||
}
|
||||
params["sign"] = sign
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
for k, v := range params {
|
||||
form.Set(k, v)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", a.getGateway(),
|
||||
strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
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 rawResp map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &rawResp); err != nil {
|
||||
return nil, fmt.Errorf("parse response failed: %w", err)
|
||||
}
|
||||
|
||||
tokenData, ok := rawResp["alipay_system_oauth_token_response"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid alipay response structure")
|
||||
}
|
||||
|
||||
var tokenResp AlipayTokenResponse
|
||||
if err := json.Unmarshal(tokenData, &tokenResp); err != nil {
|
||||
return nil, fmt.Errorf("parse token response failed: %w", err)
|
||||
}
|
||||
|
||||
return &tokenResp, nil
|
||||
}
|
||||
|
||||
// GetUserInfo 获取支付宝用户信息
|
||||
func (a *AlipayProvider) GetUserInfo(ctx context.Context, accessToken string) (*AlipayUserInfo, error) {
|
||||
params := map[string]string{
|
||||
"app_id": a.AppID,
|
||||
"method": "alipay.user.info.share",
|
||||
"charset": "UTF-8",
|
||||
"sign_type": "RSA2",
|
||||
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"version": "1.0",
|
||||
"auth_token": accessToken,
|
||||
}
|
||||
|
||||
if a.PrivateKey != "" {
|
||||
sign, err := a.signParams(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign failed: %w", err)
|
||||
}
|
||||
params["sign"] = sign
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
for k, v := range params {
|
||||
form.Set(k, v)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", a.getGateway(),
|
||||
strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
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 rawResp map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &rawResp); err != nil {
|
||||
return nil, fmt.Errorf("parse response failed: %w", err)
|
||||
}
|
||||
|
||||
userData, ok := rawResp["alipay_user_info_share_response"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid alipay user info response")
|
||||
}
|
||||
|
||||
var userInfo AlipayUserInfo
|
||||
if err := json.Unmarshal(userData, &userInfo); err != nil {
|
||||
return nil, fmt.Errorf("parse user info failed: %w", err)
|
||||
}
|
||||
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
// signParams 使用 RSA2(SHA256withRSA)对参数签名
|
||||
func (a *AlipayProvider) signParams(params map[string]string) (string, error) {
|
||||
// 按字典序排列参数
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
if k != "sign" {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+"="+params[k])
|
||||
}
|
||||
signContent := strings.Join(parts, "&")
|
||||
|
||||
// 解析私钥
|
||||
privKey, err := parseAlipayPrivateKey(a.PrivateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse private key: %w", err)
|
||||
}
|
||||
|
||||
// SHA256withRSA 签名
|
||||
hash := sha256.Sum256([]byte(signContent))
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256, hash[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("rsa sign: %w", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// parseAlipayPrivateKey 解析支付宝私钥(支持 PKCS#8 和 PKCS#1)
|
||||
func parseAlipayPrivateKey(pemStr string) (*rsa.PrivateKey, error) {
|
||||
// 如果没有 PEM 头,添加 PKCS#8 头
|
||||
if !strings.Contains(pemStr, "-----BEGIN") {
|
||||
pemStr = "-----BEGIN PRIVATE KEY-----\n" + pemStr + "\n-----END PRIVATE KEY-----"
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(pemStr))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode PEM block")
|
||||
}
|
||||
|
||||
// 尝试 PKCS#8
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err == nil {
|
||||
rsaKey, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not an RSA private key")
|
||||
}
|
||||
return rsaKey, nil
|
||||
}
|
||||
|
||||
// 尝试 PKCS#1
|
||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
}
|
||||
Reference in New Issue
Block a user