目前支付宝官方的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
}