package wechatmp import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha1" "encoding/base64" "encoding/binary" "encoding/hex" "encoding/xml" "errors" "fmt" "io" "sort" "strings" ) // ============================================================= // 微信被动消息:签名校验 / 安全模式 AES 加解密 / XML 解析与回复构造 // 规范参考:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Message_encryption_and_decryption_instructions.html // ============================================================= // pkcs7BlockSize 微信官方填充块大小(注意不是 16,是 32) const pkcs7BlockSize = 32 // InboundMessage 接收到的消息/事件(已解密后的 XML 内容) type InboundMessage struct { XMLName xml.Name `xml:"xml"` ToUserName string `xml:"ToUserName"` FromUserName string `xml:"FromUserName"` CreateTime int64 `xml:"CreateTime"` MsgType string `xml:"MsgType"` // text / image / event ... Content string `xml:"Content"` Event string `xml:"Event"` // subscribe / unsubscribe / SCAN / CLICK ... EventKey string `xml:"EventKey"` // qrscene_ 或 Ticket string `xml:"Ticket"` MsgID int64 `xml:"MsgId"` Encrypt string `xml:"Encrypt"` // 安全模式下外层 XML 携带的密文 } // ParseInboundXML 解析(明文/已解密的)消息 XML func ParseInboundXML(raw string) (*InboundMessage, error) { var msg InboundMessage if err := xml.Unmarshal([]byte(raw), &msg); err != nil { return nil, fmt.Errorf("解析微信消息失败: %w", err) } return &msg, nil } // CheckSignature 明文模式签名校验:sha1(sort(token, timestamp, nonce)) func CheckSignature(token, timestamp, nonce, signature string) bool { if token == "" || signature == "" { return false } arr := []string{token, timestamp, nonce} sort.Strings(arr) sum := sha1.Sum([]byte(strings.Join(arr, ""))) return hex.EncodeToString(sum[:]) == signature } // CheckMsgSignature 安全/兼容模式签名校验:sha1(sort(token, timestamp, nonce, encrypt)) func CheckMsgSignature(token, timestamp, nonce, encrypt, msgSignature string) bool { if token == "" || msgSignature == "" { return false } arr := []string{token, timestamp, nonce, encrypt} sort.Strings(arr) sum := sha1.Sum([]byte(strings.Join(arr, ""))) return hex.EncodeToString(sum[:]) == msgSignature } // MsgSignature 计算安全模式消息签名(回复加密时使用) func MsgSignature(token, timestamp, nonce, encrypt string) string { arr := []string{token, timestamp, nonce, encrypt} sort.Strings(arr) sum := sha1.Sum([]byte(strings.Join(arr, ""))) return hex.EncodeToString(sum[:]) } // ============================ AES 加解密 ============================ func decodeAESKey(encodingAESKey string) ([]byte, error) { key := strings.TrimSpace(encodingAESKey) if len(key) != 43 { return nil, fmt.Errorf("EncodingAESKey 长度应为 43 位(当前 %d)", len(key)) } return base64.StdEncoding.DecodeString(key + "=") } // DecryptMessage 安全模式消息解密,返回明文 XML;appID 非空时会校验一致性 func DecryptMessage(aesKey, appID, encryptBase64 string) (string, error) { key, err := decodeAESKey(aesKey) if err != nil { return "", err } raw, err := base64.StdEncoding.DecodeString(encryptBase64) if err != nil { return "", errors.New("密文不是有效的 base64") } block, err := aes.NewCipher(key) if err != nil { return "", err } if len(raw) < aes.BlockSize || len(raw)%aes.BlockSize != 0 { return "", errors.New("密文长度不合法") } iv := key[:aes.BlockSize] plain := make([]byte, len(raw)) cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, raw) plain, err = pkcs7Unpad(plain) if err != nil { return "", err } if len(plain) < 20 { return "", errors.New("解密内容长度不合法") } msgLen := int(binary.BigEndian.Uint32(plain[16:20])) if msgLen < 0 || 20+msgLen > len(plain) { return "", errors.New("解密内容长度不合法") } msg := string(plain[20 : 20+msgLen]) gotAppID := strings.TrimSpace(string(plain[20+msgLen:])) if appID != "" && gotAppID != appID { return "", fmt.Errorf("AppID 校验失败(收到 %s)", gotAppID) } return msg, nil } // EncryptMessage 安全模式消息加密,返回 base64 密文 func EncryptMessage(aesKey, appID, msg string) (string, error) { key, err := decodeAESKey(aesKey) if err != nil { return "", err } random := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, random); err != nil { return "", err } msgBytes := []byte(msg) lenBytes := make([]byte, 4) binary.BigEndian.PutUint32(lenBytes, uint32(len(msgBytes))) buf := make([]byte, 0, 16+4+len(msgBytes)+len(appID)) buf = append(buf, random...) buf = append(buf, lenBytes...) buf = append(buf, msgBytes...) buf = append(buf, []byte(appID)...) buf = pkcs7Pad(buf) block, err := aes.NewCipher(key) if err != nil { return "", err } iv := key[:aes.BlockSize] ciphertext := make([]byte, len(buf)) cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, buf) return base64.StdEncoding.EncodeToString(ciphertext), nil } func pkcs7Pad(data []byte) []byte { pad := pkcs7BlockSize - len(data)%pkcs7BlockSize if pad <= 0 { pad = pkcs7BlockSize } out := make([]byte, 0, len(data)+pad) out = append(out, data...) for i := 0; i < pad; i++ { out = append(out, byte(pad)) } return out } func pkcs7Unpad(data []byte) ([]byte, error) { if len(data) == 0 { return nil, errors.New("空数据") } pad := int(data[len(data)-1]) if pad < 1 || pad > pkcs7BlockSize || pad > len(data) { return nil, errors.New("PKCS7 填充不合法") } for i := len(data) - pad; i < len(data); i++ { if int(data[i]) != pad { return nil, errors.New("PKCS7 填充不合法") } } return data[:len(data)-pad], nil } // ============================ 被动回复构造 ============================ // BuildTextReply 构造文本消息回复 XML(明文模式直接返回) func BuildTextReply(toUser, fromUser, content string, timestamp int64) string { esc := func(s string) string { return strings.NewReplacer( "]]>", "]] >", ).Replace(s) } return fmt.Sprintf( `%d`, esc(toUser), esc(fromUser), timestamp, esc(content)) } // BuildEncryptedReply 安全模式回复:对明文回复加密并组装外层 XML func BuildEncryptedReply(cfg *Config, plainReply, timestamp, nonce string) (string, error) { encrypt, err := EncryptMessage(cfg.AESKey, cfg.AppID, plainReply) if err != nil { return "", err } sig := MsgSignature(cfg.Token, timestamp, nonce, encrypt) return fmt.Sprintf( `%s`, encrypt, sig, timestamp, nonce), nil }