feat(api): 接入微信登录并原生实现咨询域(ECR-049/050)
小程序可在 Go 上完成微信手机号登录、测评、预约和下单,不再反代 Java。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session is the jscode2session result.
|
||||
type Session struct {
|
||||
OpenID string
|
||||
UnionID string
|
||||
SessionKey string
|
||||
}
|
||||
|
||||
// Client exchanges a mini-program js_code for a session.
|
||||
type Client interface {
|
||||
Code2Session(ctx context.Context, code string) (Session, error)
|
||||
}
|
||||
|
||||
// APIClient calls api.weixin.qq.com.
|
||||
type APIClient struct {
|
||||
AppID string
|
||||
Secret string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
type wxSessionResp struct {
|
||||
OpenID string `json:"openid"`
|
||||
SessionKey string `json:"session_key"`
|
||||
UnionID string `json:"unionid"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// Enabled reports whether WeChat credentials are configured.
|
||||
func (c *APIClient) Enabled() bool {
|
||||
return c != nil && strings.TrimSpace(c.AppID) != "" && strings.TrimSpace(c.Secret) != ""
|
||||
}
|
||||
|
||||
// Code2Session implements Client.
|
||||
func (c *APIClient) Code2Session(ctx context.Context, code string) (Session, error) {
|
||||
if !c.Enabled() {
|
||||
return Session{}, errors.New("微信登录未配置")
|
||||
}
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return Session{}, errors.New("缺少微信登录码")
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("appid", c.AppID)
|
||||
q.Set("secret", c.Secret)
|
||||
q.Set("js_code", code)
|
||||
q.Set("grant_type", "authorization_code")
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.weixin.qq.com/sns/jscode2session?"+q.Encode(), nil)
|
||||
if err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
cli := c.HTTP
|
||||
if cli == nil {
|
||||
cli = &http.Client{Timeout: 10 * time.Second}
|
||||
}
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return Session{}, errors.New("微信服务暂不可用")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var body wxSessionResp
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return Session{}, errors.New("微信服务暂不可用")
|
||||
}
|
||||
if body.ErrCode != 0 {
|
||||
return Session{}, fmt.Errorf("微信登录失败(%d)", body.ErrCode)
|
||||
}
|
||||
if body.OpenID == "" || body.SessionKey == "" {
|
||||
return Session{}, errors.New("微信登录失败")
|
||||
}
|
||||
return Session{OpenID: body.OpenID, UnionID: body.UnionID, SessionKey: body.SessionKey}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type phonePayload struct {
|
||||
PhoneNumber string `json:"phoneNumber"`
|
||||
PurePhoneNumber string `json:"purePhoneNumber"`
|
||||
Watermark struct {
|
||||
AppID string `json:"appid"`
|
||||
} `json:"watermark"`
|
||||
}
|
||||
|
||||
// DecryptPhone decrypts WeChat getPhoneNumber encryptedData (AES-128-CBC).
|
||||
func DecryptPhone(sessionKey, encryptedData, iv, expectAppID string) (string, error) {
|
||||
key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sessionKey))
|
||||
if err != nil || len(key) != 16 {
|
||||
return "", errors.New("无效的 session_key")
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encryptedData))
|
||||
if err != nil || len(raw) == 0 || len(raw)%aes.BlockSize != 0 {
|
||||
return "", errors.New("无效的加密数据")
|
||||
}
|
||||
ivb, err := base64.StdEncoding.DecodeString(strings.TrimSpace(iv))
|
||||
if err != nil || len(ivb) != aes.BlockSize {
|
||||
return "", errors.New("无效的 iv")
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
plain := make([]byte, len(raw))
|
||||
cipher.NewCBCDecrypter(block, ivb).CryptBlocks(plain, raw)
|
||||
plain, err = pkcs7Unpad(plain)
|
||||
if err != nil {
|
||||
return "", errors.New("解密失败")
|
||||
}
|
||||
var p phonePayload
|
||||
if err := json.Unmarshal(plain, &p); err != nil {
|
||||
return "", errors.New("解密失败")
|
||||
}
|
||||
if expectAppID != "" && p.Watermark.AppID != "" && p.Watermark.AppID != expectAppID {
|
||||
return "", errors.New("微信数据校验失败")
|
||||
}
|
||||
phone := strings.TrimSpace(p.PhoneNumber)
|
||||
if phone == "" {
|
||||
phone = strings.TrimSpace(p.PurePhoneNumber)
|
||||
}
|
||||
if phone == "" {
|
||||
return "", errors.New("未获取到手机号")
|
||||
}
|
||||
return phone, nil
|
||||
}
|
||||
|
||||
func pkcs7Unpad(b []byte) ([]byte, error) {
|
||||
if len(b) == 0 {
|
||||
return nil, errors.New("empty")
|
||||
}
|
||||
n := int(b[len(b)-1])
|
||||
if n == 0 || n > len(b) {
|
||||
return nil, errors.New("pad")
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
if b[len(b)-1-i] != byte(n) {
|
||||
return nil, errors.New("pad")
|
||||
}
|
||||
}
|
||||
return b[:len(b)-n], nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecryptPhone(t *testing.T) {
|
||||
key := make([]byte, 16)
|
||||
iv := make([]byte, 16)
|
||||
_, _ = rand.Read(key)
|
||||
_, _ = rand.Read(iv)
|
||||
plain := []byte(`{"phoneNumber":"13800138000","purePhoneNumber":"13800138000","watermark":{"appid":"wxapp"}}`)
|
||||
padded := pkcs7Pad(plain, aes.BlockSize)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enc := make([]byte, len(padded))
|
||||
cipher.NewCBCEncrypter(block, iv).CryptBlocks(enc, padded)
|
||||
|
||||
phone, err := DecryptPhone(
|
||||
base64.StdEncoding.EncodeToString(key),
|
||||
base64.StdEncoding.EncodeToString(enc),
|
||||
base64.StdEncoding.EncodeToString(iv),
|
||||
"wxapp",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if phone != "13800138000" {
|
||||
t.Fatalf("got %s", phone)
|
||||
}
|
||||
}
|
||||
|
||||
func pkcs7Pad(b []byte, block int) []byte {
|
||||
n := block - (len(b) % block)
|
||||
out := make([]byte, len(b)+n)
|
||||
copy(out, b)
|
||||
for i := 0; i < n; i++ {
|
||||
out[len(b)+i] = byte(n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user