微信客服接口对接-签名验签-微信客服GET请求回调URL验证解析返回数据-【唯一客服】
在做微信客服的接口对接时,第一步就是在微信客服后台配置自己的回调URL
微信客服会往URL上get请求数据,要求把GET中的加密参数解析出来,并且原样输出,才能验证成功
下面就是使用官方的xml 版sdk 实现这个验证的类库,与测试用例
package wechat_kf_sdk // 创建微信客服API的封装结构体实例 func NewKefuWework(corpid string, corpsecret, Token, EncodingAESKey string) *KefuWework { return &KefuWework{ corpid: corpid, corpsecret: corpsecret, Token: Token, EncodingAESKey: EncodingAESKey, } } //验证签名 func (s *KefuWework) CheckSign(signature, timestamp, nonce, echostr string) (string, error) { wxcpt := NewWXBizMsgCrypt(s.Token, s.EncodingAESKey, s.corpid, XmlType) echoStr, cryptErr := wxcpt.VerifyURL(signature, timestamp, nonce, echostr) if cryptErr != nil { return "", errors.New(cryptErr.ErrMsg) } return string(echoStr), nil }
测试用例
func TestCheckSign(t *testing.T) { corpid := "wx5823bf96d3bd56c7" corpsecret := "your_corpsecret" token := "QDG6eK" encodingAeskey := "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C" verifyMsgSign := "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3" // verifyTimestamp := HttpUtils.ParseUrl("timestamp") verifyTimestamp := "1409659589" // verifyNonce := HttpUtils.ParseUrl("nonce") verifyNonce := "263014780" // verifyEchoStr := HttpUtils.ParseUrl("echoStr") verifyEchoStr := "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==" // 创建微信客服API的封装结构体实例 wxService := NewKefuWework(corpid, corpsecret, token, encodingAeskey) res, err := wxService.CheckSign(verifyMsgSign, verifyTimestamp, verifyNonce, verifyEchoStr) log.Println(res, err) }
官方的sdk
package wechat_kf_sdk import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/sha1" "encoding/base64" "encoding/binary" "encoding/xml" "fmt" "math/rand" "sort" "strings" ) const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( ValidateSignatureError int = -40001 ParseXmlError int = -40002 ComputeSignatureError int = -40003 IllegalAesKey int = -40004 ValidateCorpidError int = -40005 EncryptAESError int = -40006 DecryptAESError int = -40007 IllegalBuffer int = -40008 EncodeBase64Error int = -40009 DecodeBase64Error int = -40010 GenXmlError int = -40010 ParseJsonError int = -40012 GenJsonError int = -40013 IllegalProtocolType int = -40014 ) type ProtocolType int const ( XmlType ProtocolType = 1 ) type CryptError struct { ErrCode int ErrMsg string } func NewCryptError(err_code int, err_msg string) *CryptError { return &CryptError{ErrCode: err_code, ErrMsg: err_msg} } type WXBizMsg4Recv struct { Tousername string `xml:"ToUserName"` Encrypt string `xml:"Encrypt"` Agentid string `xml:"AgentID"` } type CDATA struct { Value string `xml:",cdata"` } type WXBizMsg4Send struct { XMLName xml.Name `xml:"xml"` Encrypt CDATA `xml:"Encrypt"` Signature CDATA `xml:"MsgSignature"` Timestamp string `xml:"TimeStamp"` Nonce CDATA `xml:"Nonce"` } func NewWXBizMsg4Send(encrypt, signature, timestamp, nonce string) *WXBizMsg4Send { return &WXBizMsg4Send{Encrypt: CDATA{Value: encrypt}, Signature: CDATA{Value: signature}, Timestamp: timestamp, Nonce: CDATA{Value: nonce}} } type ProtocolProcessor interface { parse(src_data []byte) (*WXBizMsg4Recv, *CryptError) serialize(msg_send *WXBizMsg4Send) ([]byte, *CryptError) } type WXBizMsgCrypt struct { token string encoding_aeskey string receiver_id string protocol_processor ProtocolProcessor } type XmlProcessor struct { } func (self *XmlProcessor) parse(src_data []byte) (*WXBizMsg4Recv, *CryptError) { var msg4_recv WXBizMsg4Recv err := xml.Unmarshal(src_data, &msg4_recv) if nil != err { return nil, NewCryptError(ParseXmlError, "xml to msg fail") } return &msg4_recv, nil } func (self *XmlProcessor) serialize(msg4_send *WXBizMsg4Send) ([]byte, *CryptError) { xml_msg, err := xml.Marshal(msg4_send) if nil != err { return nil, NewCryptError(GenXmlError, err.Error()) } return xml_msg, nil } func NewWXBizMsgCrypt(token, encoding_aeskey, receiver_id string, protocol_type ProtocolType) *WXBizMsgCrypt { var protocol_processor ProtocolProcessor if protocol_type != XmlType { panic("unsupport protocal") } else { protocol_processor = new(XmlProcessor) } return &WXBizMsgCrypt{token: token, encoding_aeskey: (encoding_aeskey + "="), receiver_id: receiver_id, protocol_processor: protocol_processor} } func (self *WXBizMsgCrypt) randString(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] } return string(b) } func (self *WXBizMsgCrypt) pKCS7Padding(plaintext string, block_size int) []byte { padding := block_size - (len(plaintext) % block_size) padtext := bytes.Repeat([]byte{byte(padding)}, padding) var buffer bytes.Buffer buffer.WriteString(plaintext) buffer.Write(padtext) return buffer.Bytes() } func (self *WXBizMsgCrypt) pKCS7Unpadding(plaintext []byte, block_size int) ([]byte, *CryptError) { plaintext_len := len(plaintext) if nil == plaintext || plaintext_len == 0 { return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding error nil or zero") } if plaintext_len%block_size != 0 { return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding text not a multiple of the block size") } padding_len := int(plaintext[plaintext_len-1]) return plaintext[:plaintext_len-padding_len], nil } func (self *WXBizMsgCrypt) cbcEncrypter(plaintext string) ([]byte, *CryptError) { aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey) if nil != err { return nil, NewCryptError(DecodeBase64Error, err.Error()) } const block_size = 32 pad_msg := self.pKCS7Padding(plaintext, block_size) block, err := aes.NewCipher(aeskey) if err != nil { return nil, NewCryptError(EncryptAESError, err.Error()) } ciphertext := make([]byte, len(pad_msg)) iv := aeskey[:aes.BlockSize] mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(ciphertext, pad_msg) base64_msg := make([]byte, base64.StdEncoding.EncodedLen(len(ciphertext))) base64.StdEncoding.Encode(base64_msg, ciphertext) return base64_msg, nil } func (self *WXBizMsgCrypt) cbcDecrypter(base64_encrypt_msg string) ([]byte, *CryptError) { aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey) if nil != err { return nil, NewCryptError(DecodeBase64Error, err.Error()) } encrypt_msg, err := base64.StdEncoding.DecodeString(base64_encrypt_msg) if nil != err { return nil, NewCryptError(DecodeBase64Error, err.Error()) } block, err := aes.NewCipher(aeskey) if err != nil { return nil, NewCryptError(DecryptAESError, err.Error()) } if len(encrypt_msg) < aes.BlockSize { return nil, NewCryptError(DecryptAESError, "encrypt_msg size is not valid") } iv := aeskey[:aes.BlockSize] if len(encrypt_msg)%aes.BlockSize != 0 { return nil, NewCryptError(DecryptAESError, "encrypt_msg not a multiple of the block size") } mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(encrypt_msg, encrypt_msg) return encrypt_msg, nil } func (self *WXBizMsgCrypt) calSignature(timestamp, nonce, data string) string { sort_arr := []string{self.token, timestamp, nonce, data} sort.Strings(sort_arr) var buffer bytes.Buffer for _, value := range sort_arr { buffer.WriteString(value) } sha := sha1.New() sha.Write(buffer.Bytes()) signature := fmt.Sprintf("%x", sha.Sum(nil)) return string(signature) } func (self *WXBizMsgCrypt) ParsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, *CryptError) { const block_size = 32 plaintext, err := self.pKCS7Unpadding(plaintext, block_size) if nil != err { return nil, 0, nil, nil, err } text_len := uint32(len(plaintext)) if text_len < 20 { return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 1") } random := plaintext[:16] msg_len := binary.BigEndian.Uint32(plaintext[16:20]) if text_len < (20 + msg_len) { return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 2") } msg := plaintext[20 : 20+msg_len] receiver_id := plaintext[20+msg_len:] return random, msg_len, msg, receiver_id, nil } func (self *WXBizMsgCrypt) VerifyURL(msg_signature, timestamp, nonce, echostr string) ([]byte, *CryptError) { signature := self.calSignature(timestamp, nonce, echostr) if strings.Compare(signature, msg_signature) != 0 { return nil, NewCryptError(ValidateSignatureError, "signature not equal") } plaintext, err := self.cbcDecrypter(echostr) if nil != err { return nil, err } _, _, msg, receiver_id, err := self.ParsePlainText(plaintext) if nil != err { return nil, err } if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 { fmt.Println(string(receiver_id), self.receiver_id, len(receiver_id), len(self.receiver_id)) return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil") } return msg, nil } func (self *WXBizMsgCrypt) EncryptMsg(reply_msg, timestamp, nonce string) ([]byte, *CryptError) { rand_str := self.randString(16) var buffer bytes.Buffer buffer.WriteString(rand_str) msg_len_buf := make([]byte, 4) binary.BigEndian.PutUint32(msg_len_buf, uint32(len(reply_msg))) buffer.Write(msg_len_buf) buffer.WriteString(reply_msg) buffer.WriteString(self.receiver_id) tmp_ciphertext, err := self.cbcEncrypter(buffer.String()) if nil != err { return nil, err } ciphertext := string(tmp_ciphertext) signature := self.calSignature(timestamp, nonce, ciphertext) msg4_send := NewWXBizMsg4Send(ciphertext, signature, timestamp, nonce) return self.protocol_processor.serialize(msg4_send) } func (self *WXBizMsgCrypt) DecryptMsg(msg_signature, timestamp, nonce string, post_data []byte) ([]byte, *CryptError) { msg4_recv, crypt_err := self.protocol_processor.parse(post_data) if nil != crypt_err { return nil, crypt_err } signature := self.calSignature(timestamp, nonce, msg4_recv.Encrypt) if strings.Compare(signature, msg_signature) != 0 { return nil, NewCryptError(ValidateSignatureError, "signature not equal") } plaintext, crypt_err := self.cbcDecrypter(msg4_recv.Encrypt) if nil != crypt_err { return nil, crypt_err } _, _, msg, receiver_id, crypt_err := self.ParsePlainText(plaintext) if nil != crypt_err { return nil, crypt_err } if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 { return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil") } return msg, nil }
测试结果成功解析出加密的echostr
go test -v kf.go kf_test.go wxbizmsgcrypt.go --run TestCheckSign
=== RUN TestCheckSign
1616140317555161061 <nil>
--- PASS: TestCheckSign (0.01s)
放到验证接口里使用就可以了
这是验证通过的效果
十年开发经验程序员,离职全心创业中,历时三年开发出的产品《唯一客服系统》
一款基于Golang+Vue开发的在线客服系统,软件著作权编号:2021SR1462600。一套可私有化部署的网站在线客服系统,编译后的二进制文件可直接使用无需搭开发环境,下载zip解压即可,仅依赖MySQL数据库,是一个开箱即用的全渠道在线客服系统,致力于帮助广大开发者/公司快速部署整合私有化客服功能。
开源地址:唯一客服(开源学习版)
官网地址:唯一客服官网