Golang 接收企业微信消息
在实现一个企业微信中的机器人的时候遇到了一个问题,我想要获取用户输入的信息,而在企业微信API里面,对于消息的获取采取的是推的模式(webhook),由企业微信的服务器主动向我们推送消息,个人感觉相对于拉模式或者是websocket要麻烦一些。我们需要向企业微信提供一个“接收消息的服务器”。
企业应用接收来自企业微信的消息分为下面几个步骤
验证URL (验证服务器)
参考 https://work.weixin.qq.com/api/doc/90000/90139/90968
在添加应用后,企业微信的服务器会验证我们的接收消息的服务器的有效性,我们需要实现一个http服务器。我使用了已有的库进行加密解密 。 它们提供了解析JSON和xml两种格式信息的库,然而却没有办法指定传输的数据的格式是什么(默认是xml),真是令人费解。
假设接收消息地址设置为:http://api.3dept.com/,企业微信将向该地址发送如下验证请求:
请求方式:GET 请求地址:http://api.3dept.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFSS×tamp=13500001234&nonce=123412323&echostr=ENCRYPT_STR
接收消息
在验证完成URL之后,消息都会以POST方式发送到同一链接下,企业应用服务器在收到请求后需要及时做出响应,返回头部状态码200的响应,其他响应码都会被当作失败,对没得到响应的POST请求,企业微信服务器会尝试三次重新请求。
被动回复信息
在接收到信息后,做出响应的时候可以带上我们的回复。响应的消息内容需要为xml格式,在将回复消息结构体序列化成xml后,我们可以直接使用 wxbizmsgcrypt 包中的方法对其进行加密,得到加密后的xml信息,通过 Write 方法写回即可。
代码如下:
package bot
import (
"encoding/xml"
"fmt"
"io/ioutil"
"log"
"net/http"
"wxbot-test/wxbizmsgcrypt"
)
//ToUserName 成员UserID
//FromUserName 企业微信CorpID
//CreateTime 消息创建时间(整型)
//MsgType 消息类型,此时固定为:text
//Content 文本消息内容,最长不超过2048个字节,超过将截断
type ReplyTextMsg struct {
ToUsername string `xml:"ToUserName"`
FromUsername string `xml:"FromUserName"`
CreateTime uint32 `xml:"CreateTime"`
MsgType string `xml:"MsgType"`
Content string `xml:"Content"`
}
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"`
}
var (
corpId, token, encodingAesKey string
wxcrypt *wxbizmsgcrypt.WXBizMsgCrypt
)
func init() {
// 读取配置文件
corpId = "ww592d57xxxxxxx"
token = "VbmqbkBQWnxxxxxxxxx"
encodingAesKey = "HDyFtR7DB6oyeG7DuzCOgjWg7xxxxxxxxxxxxxxxxx"
// receive_id 企业应用的回调,表示corpid
wxcrypt = wxbizmsgcrypt.NewWXBizMsgCrypt(token, encodingAesKey, corpId, wxbizmsgcrypt.XmlType)
}
func Start() {
// 开启一个http服务器,接收来自企业微信的消息
http.HandleFunc("/api/bot/message", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
fmt.Println("接收到验证请求")
handleVerify(w, r)
} else if r.Method == "POST" {
fmt.Println("接收到消息")
handleMessage(w, r)
}
})
log.Fatalln(http.ListenAndServe("127.0.0.1:8888", nil))
}
func handleMessage(w http.ResponseWriter, r *http.Request) {
msgSignature := r.URL.Query().Get("msg_signature")
timestamp := r.URL.Query().Get("timestamp")
nonce := r.URL.Query().Get("nonce")
body, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Println("读取Body错误", err.Error())
} else {
msg, err := wxcrypt.DecryptMsg(msgSignature, timestamp, nonce, body)
if err != nil {
fmt.Println("解密消息Body错误", err.ErrMsg)
} else {
var msgContent MsgContent
err := xml.Unmarshal(msg, &msgContent)
if err != nil {
fmt.Println("反序列化错误")
} else {
fmt.Println(msgContent)
// 回复信息
replyMsg, _ := xml.Marshal(ReplyTextMsg{
ToUsername: msgContent.FromUsername,
FromUsername: msgContent.ToUsername,
CreateTime: msgContent.CreateTime,
MsgType: "text",
Content: "Receive",
})
encryptMsg, cryptErr := wxcrypt.EncryptMsg(string(replyMsg), timestamp, nonce)
if cryptErr != nil {
fmt.Println("回复加密出错", cryptErr)
} else {
fmt.Println(string(encryptMsg))
l, err := w.Write(encryptMsg)
if err != nil {
fmt.Println("返回消息失败")
} else {
fmt.Println("成功写入", l)
}
}
}
}
}
}
func handleVerify(w http.ResponseWriter, r *http.Request) {
msgSignature := r.URL.Query().Get("msg_signature")
timestamp := r.URL.Query().Get("timestamp")
nonce := r.URL.Query().Get("nonce")
echoStr := r.URL.Query().Get("echostr")
// 合法性验证
echoStrBytes, err := wxcrypt.VerifyURL(msgSignature, timestamp, nonce, echoStr)
if err != nil {
fmt.Println("验证失败", err.ErrMsg)
} else {
fmt.Println("验证成功", string(echoStrBytes))
// 需要返回才能通过验证
_, err := w.Write(echoStrBytes)
if err != nil {
fmt.Println("返回验证结果失败", err.Error())
}
}
}