目前支付宝官方的api没有关于golang语言的sdk,最近在开发支付宝小程序登录,发现支付宝对于敏感信息会进行AES加密 ,例如获取用户手机号,会先由前端获取手机号密文、传给服务端,进行解密,从而获取手机号,目前百度和google到的示例都不好用,内容同质化明显。手机号只做了AES解密。关于手机号sign校验,还没看懂支付宝官方文档是什么意思,验签逻辑说的比较笼统,所以还没有写。下面是解密示例。

package ali

import (
	"account-api-v3/models/user"
	"account-api-v3/pkg/config"
	"account-api-v3/pkg/e"
	"account-api-v3/pkg/http"
	"account-api-v3/pkg/logger"
	"account-api-v3/pkg/oauth2/errors"
	"crypto"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io/ioutil"
	"reflect"
	"sort"
	"strings"
	"time"
)

const (
	// PEMBEGIN 私钥 PEMBEGIN 开头
	PEMBEGIN = "-----BEGIN RSA PRIVATE KEY-----\n"
	// PEMEND 私钥 PEMEND 结尾
	PEMEND = "\n-----END RSA PRIVATE KEY-----"
	// PUBPEMBEGIN 公钥 PEMBEGIN 开头
	PUBPEMBEGIN = "-----BEGIN PUBLIC KEY-----\n"
	// PUBPEMEND 公钥 PEMEND 结尾
	PUBPEMEND = "\n-----END PUBLIC KEY-----"
)

type (
	CodeSession struct {
		ErrorResponse                  ErrorResponse                  `json:"error_response"`
		AlipaySystemOauthTokenResponse AlipaySystemOauthTokenResponse `json:"alipay_system_oauth_token_response"`
		Sign                           string                         `json:"sign"`
	}

	AlipaySystemOauthTokenResponse struct {
		AlipayUserId string `json:"alipay_user_id"`
		UserId       string `json:"user_id"`
		AccessToken  string `json:"access_token"`
		ExpiresIn    string `json:"expires_in"`
		RefreshToken string `json:"refresh_token"`
		ReExpiresIn  string `json:"re_expires_in"`
		AuthStart    string `json:"auth_start"`
	}

	ErrorResponse struct {
		Code    string `json:"code"`
		Msg     string `json:"msg"`
		SubCode string `json:"sub_code"`
		SubMsg  string `json:"sub_msg"`
	}

	UserRes struct {
		ErrorResponse               ErrorResponse               `json:"error_response"`
		AlipayUserInfoShareResponse AlipayUserInfoShareResponse `json:"alipay_user_info_share_response"`
		Sign                        string                      `json:"sign"`
	}

	AlipayUserInfoShareResponse struct {
		Code     string `json:"code"`
		Msg      string `json:"msg"`
		UserId   string `json:"user_id"`
		Avatar   string `json:"avatar"`
		City     string `json:"city"`
		NickName string `json:"nick_name"`
		Province string `json:"province"`
		Gender   string `json:"gender"`
	}

	MobileRes struct {
		Code   string `json:"code"`
		Msg    string `json:"msg"`
		Mobile string `json:"mobile"`
	}
	
	UserPhone struct {
		ErrorResponse
		Mobile string `json:"mobile,omitempty"`
	}
)

// 解密用户手机号密文
// @content 手机号密文、sign密文签名
func GetUserMobile(content, sign string) string {
	phone := new(UserPhone)

	// todo 验签,支付宝官方目前没有go相关的sdk,验签功能还没有实现,只做了密文解密,后续如果成功验签,会跟新

	// 解密密文
	err := DecryptOpenDataToStruct(content, config.AliMiniLogin.AesSecret, phone)
	if err == nil {
		return phone.Mobile
	}
	return ""
}

func DecryptOpenDataToStruct(encryptedData, secretKey string, beanPtr interface{}) (err error) {
	if encryptedData == "" || secretKey == "" {
		return errors.New("encryptedData or secretKey is null")
	}
	beanValue := reflect.ValueOf(beanPtr)
	if beanValue.Kind() != reflect.Ptr {
		return errors.New("传入参数类型必须是以指针形式")
	}
	if beanValue.Elem().Kind() != reflect.Struct {
		return errors.New("传入interface{}必须是结构体")
	}
	var (
		block      cipher.Block
		blockMode  cipher.BlockMode
		originData []byte
	)
	aesKey, _ := base64.StdEncoding.DecodeString(secretKey)
	ivKey := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
	secretData, _ := base64.StdEncoding.DecodeString(encryptedData)
	if block, err = aes.NewCipher(aesKey); err != nil {
		return fmt.Errorf("aes.NewCipher:%w", err)
	}
	if len(secretData)%len(aesKey) != 0 {
		return errors.New("encryptedData is error")
	}
	blockMode = cipher.NewCBCDecrypter(block, ivKey)
	originData = make([]byte, len(secretData))
	blockMode.CryptBlocks(originData, secretData)
	if len(originData) > 0 {
		originData = PKCS5UnPadding(originData)
	}
	if err = json.Unmarshal(originData, beanPtr); err != nil {
		return fmt.Errorf("json.Unmarshal(%s):%w", string(originData), err)
	}
	return nil
}

// 解密填充模式(去除补全码) PKCS5UnPadding
// 解密时,需要在最后面去掉加密时添加的填充byte
func PKCS5UnPadding(origData []byte) []byte {
	length := len(origData)
	unpadding := int(origData[length-1])   //找到Byte数组最后的填充byte
	return origData[:(length - unpadding)] //只截取返回有效数字内的byte数组
}

// 根据授权码获取授权信息
func GetAliSession(appID, code string) (session *CodeSession, errCode int) {
	thirdConfigs := user.GetThirdConfigs()
	_, ok := thirdConfigs[appID]
	if !ok {
		return nil, e.InvalidThirdPartAppID
	}

	timestamp := time.Now().Format("2006-01-02 15:04:05")

	params := map[string]string{
		"app_id":     appID,
		"code":       code,
		"grant_type": "authorization_code",
		"method":     "alipay.system.oauth.token",
		"charset":    "utf-8",
		"sign_type":  "RSA2",
		"timestamp":  timestamp,
		"version":    "1.0",
	}

	queryStr := FormatQuery(params)
	private := config.AliMiniLogin.Private
	sign := RsaSignWithSha256(queryStr, private)

	params["sign"] = sign

	res, err := http.Get(config.AliMiniLogin.Host, params, nil)
	if res == nil && err != nil {
		logger.Error("Request Ali AliMiniLogin session  Failed:" + err.Error())
		return nil, e.BUSY
	}

	body, err := ioutil.ReadAll(res.Body)

	if err != nil {
		return nil, e.BUSY
	}

	err = json.Unmarshal(body, &session)

	if err != nil {
		return nil, e.BUSY
	}

	if session.ErrorResponse.Code != "10000" {
		return nil, e.BUSY
	}

	return session, e.SUCCESS
}

func FormatQuery(params map[string]string) (queryStr string) {
	if len(params) == 0 {
		return
	}

	var keys []string
	for k, _ := range params {
		keys = append(keys, k)
	}

	sort.Strings(keys)

	for _, v := range keys {
		queryStr += v + "=" + params[v] + "&"
	}
	return strings.TrimRight(queryStr, "&")
}

func RsaSignWithSha256(data string, privateKey string) string {
	h := sha256.New()
	h.Write([]byte(data))
	hashed := h.Sum(nil)
	privateKey = FormatPrivateKey(privateKey)
	block, _ := pem.Decode([]byte(privateKey))
	if block == nil {
		return ""
	}
	priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return ""
	}

	signature, err := rsa.SignPKCS1v15(rand.Reader, priKey, crypto.SHA256, hashed)
	if err != nil {
		return ""
	}
	return base64.StdEncoding.EncodeToString(signature)
}

// 私钥验证
func ParsePrivateKey(privateKey string) (*rsa.PrivateKey, error) {
	privateKey = FormatPrivateKey(privateKey)
	block, _ := pem.Decode([]byte(privateKey))
	if block == nil {
		return nil, errors.New("私钥信息错误!")
	}
	priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	return priKey, nil
}

// 组装私钥
func FormatPrivateKey(privateKey string) string {
	if !strings.HasPrefix(privateKey, PEMBEGIN) {
		privateKey = PEMBEGIN + privateKey
	}
	if !strings.HasSuffix(privateKey, PEMEND) {
		privateKey = privateKey + PEMEND
	}
	return privateKey
}