Golang集成企业微信接收消息服务处理URL接口(验证、解密)信息(GoFly快速开发框架)

发布于:2025-03-04 ⋅ 阅读:(13) ⋅ 点赞:(0)

说明:​

本插件是集成企业微信自定义应用开发时接收消息与事件处理的URL接口的msg_signature请求进行校验、对消息解密Encrypt和处理被动回复消息内容功能,让自建应用和企业微信进行双向通信,企业可以在应用的管理后台开启接收消息模式,方便开发企业微信时快速接入。

功能:

  • 验证URL有效性,通过参数msg_signature​​对请求进行校验​​,确认调用者的合法性。
  • 收到消息后对​​解密​​Encrypt,得到明文的消息结构体
  • 被动回复消息,构造被动响应包

使用代码:

插件把安装后会安装在wx模块下的work中,在work目录下有lib目录是处理接口相关功能。了解更多插件到社区查看,为地址:https://goflys.cn/codedetail?id=77

使用代码如下:

// 企业微信服务入口
package work

import (
	"encoding/xml"
	"fmt"
	"gofly/utils/gf"
	"gofly/utils/tools/gconv"
	"log"
	"time"

	wxbizmsgcrypt "gofly/app/wx/work/lib"
)

type Index struct {
	NoNeedLogin []string //忽略登录接口配置-忽略全部传[*]
}

// 初始化路由
func init() {
	fpath := Index{NoNeedLogin: []string{"*"}}
	gf.Register(&fpath, fpath)
}

// 读取wxwork插件配置文件
var (
	confobj, _ = gf.GetConfByFile("wxwork")
	confData   = gconv.Map(gconv.Map(confobj)["data"])
)

/**
* url接口配置
 */
func (api *Index) GetPostApi(c *gf.GinCtx) {
	CorpId := gf.String(confData["corpid"])                 // 企业微信 ID (在企业微信的“我的企业->企业信息-企业ID”)
	Token := gf.String(confData["token"])                   // 添加回调时自动生成的 Token
	EncodingAESKey := gf.String(confData["encodingaeskey"]) // 添加回调时自动生成的 EncodingAESKey
	// 获取到请求参数
	msgSignature := c.Query("msg_signature")
	timestamp := c.Query("timestamp")
	nonce := c.Query("nonce")
	if c.Request.Method == "GET" {
		echostr := c.Query("echostr")
		//使用示例一:验证回调URL-调用企业微信官方提供的接口进行解析校验
		wxcpt := wxbizmsgcrypt.NewWXBizMsgCrypt(Token, EncodingAESKey, CorpId, wxbizmsgcrypt.XmlType)
		echoStr, cryptErr := wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr)
		if nil != cryptErr {
			fmt.Println("verifyUrl fail", cryptErr)
			return
		}
		// 将解密出来的字符串返回出去
		// c.String(200, string(echoStr))
		_, _ = c.Writer.WriteString(string(echoStr))
	} else { //post请求
		wxcpt := wxbizmsgcrypt.NewWXBizMsgCrypt(Token, EncodingAESKey, CorpId, wxbizmsgcrypt.XmlType)
		postdata, err := c.GetRawData()
		if err != nil {
			log.Fatalln(err)
		}
		//使用示例二:对用户回复的消息解密
		msgText, cryptErr := wxcpt.DecryptMsg(msgSignature, timestamp, nonce, []byte(postdata))
		if nil != cryptErr {
			fmt.Println("DecryptMsg fail", cryptErr)
			return
		}
		msgContent, err := ReceiveCommonMsg(msgText)
		if err != nil {
			fmt.Println("ReceiveMsg fail", err)
			return
		}
		fmt.Println("解析消息: ", msgContent)
		fmt.Println("解析消息内容: ", msgContent.Content)
		//使用示例三:企业回复用户消息的加密(被动回复)
		if msgContent.Content != "" { //有内容是在回复
			respData := fmt.Sprintf("<xml><ToUserName><![CDATA[%v]]></ToUserName><FromUserName><![CDATA[%v]]></FromUserName><CreateTime>%v</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[AI正在为您解答,请稍等]]></Content></xml>", msgContent.FromUsername, msgContent.ToUsername, time.Now().Unix())
			encryptMsg, cryptErr := wxcpt.EncryptMsg(respData, timestamp, nonce)
			if nil != cryptErr {
				fmt.Println("DecryptMsg fail", cryptErr)
			}
			sEncryptMsg := string(encryptMsg)
			_, _ = c.Writer.WriteString(sEncryptMsg)
		}
	}
}

type MsgContent struct {
	ToUsername   string `xml:"ToUserName"`
	FromUsername string `xml:"FromUserName"`
	CreateTime   uint32 `xml:"CreateTime"`
	MsgType      string `xml:"MsgType"`
	Content      string `xml:"Content"`
	Msgid        string `xml:"MsgId"`
	Agentid      uint32 `xml:"AgentId"`
}

type WxReceiveCommonMsg struct {
	ToUserName   string //接收者 开发者 微信号
	FromUserName string //发送者 发送方帐号(一个OpenID)
	Content      string //文本内容
	CreateTime   int64  //创建时间
	MsgType      string //消息类型
	MsgId        int64  //消息id
	AgentID      int64  //接收的应用id,可在应用的设置页面获取
	PicUrl       string //图片url
	MediaId      string //媒体id
	Event        string //事件类型,VIEW
	EventKey     string //事件KEY值,设置的跳转URL
	MenuId       string
	Format       string
	Recognition  string
	ThumbMediaId string //缩略图媒体ID
}

// MsgContentFunc (接收到消息之后,会将消息交于这个函数处理)
var MsgContentFunc func(msg MsgContent) error

// 处理接口事件
func ReceiveCommonMsg(msgData []byte) (MsgContent, error) {
	fmt.Printf("received weixin msgData:\n%s\n", msgData)
	rootmsg := MsgContent{}
	err := xml.Unmarshal(msgData, &rootmsg)
	if MsgContentFunc == nil {
		return rootmsg, err
	}
	err = MsgContentFunc(rootmsg)
	return rootmsg, err
}

lib工具代码:

package lib

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
}