Go 實戰丨微信公眾號接入及使用者訊息處理

ColeLie發表於2020-04-04

使用 Go 語言的 Web 框架 Gin 進行微信公眾號接入,並實現對微信訊息的接收以及回覆處理。

同時藉助 nginx 代理伺服器對代理的埠號以及 URI 進行優化處理。

在文章末尾給出該 Demo 的專案地址。

目錄

  • 公眾號接入
  • 訊息接收
  • 訊息回覆
  • 使用 ngxin 代理伺服器
  • 小結

公眾號接入

這裡使用微信公眾平臺提供的介面測試號用於開發使用,介面測試號申請

公眾號的接入主要有兩個步驟,微信公眾平臺接入指南

  1. 填寫伺服器配置
  2. 驗證伺服器地址的有效性

第一步需要配置伺服器的 URL 地址,並且必須以 http://https:// 開頭,分別支援 80 埠和 443 埠;還需配置一個 3 ~ 32 位字元的 Token,用於訊息驗證。

第二步用於驗證訊息來源的正確性,當第一步配置完成並點提交後,微信伺服器將傳送 GET 請求到填寫的伺服器地址上,GET 請求攜帶的引數及描述如下表所示:

引數 描述
signature 微信加密簽名,signature結合了開發者填寫的token引數和請求中的timestamp引數、nonce引數。
timestamp 時間戳
nonce 隨機數
echostr 隨機字串

伺服器需要做的驗證操作流程大致為:

  1. tokentimestampnonce 三個引數進行字典序排序;
  2. 將排序後的 tokentimestampnonce 三個引數按順序拼接成一個字串,並對該字串進行 sha1 加密;
  3. 使用加密後的字串與 signature 引數進行比較,如果字串值相同,則表示校驗通過,將 echostr 引數原樣返回即可。

使用 Go 實現的微信公眾號接入程式碼如下:

package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"weixin-demo/util"
)

// 與填寫的伺服器配置中的Token一致
const Token = "coleliedev"

func main() {
	router := gin.Default()

	router.GET("/wx", WXCheckSignature)

	log.Fatalln(router.Run(":80"))
}

// WXCheckSignature 微信接入校驗
func WXCheckSignature(c *gin.Context) {
	signature := c.Query("signature")
	timestamp := c.Query("timestamp")
	nonce := c.Query("nonce")
	echostr := c.Query("echostr")

	ok := util.CheckSignature(signature, timestamp, nonce, Token)
	if !ok {
		log.Println("微信公眾號接入校驗失敗!")
		return
	}

	log.Println("微信公眾號接入校驗成功!")
	_, _ = c.Writer.WriteString(echostr)
}
複製程式碼
package util

import (
	"crypto/sha1"
	"encoding/hex"
	"sort"
	"strings"
)

// CheckSignature 微信公眾號簽名檢查
func CheckSignature(signature, timestamp, nonce, token string) bool {
	arr := []string{timestamp, nonce, token}
	// 字典序排序
	sort.Strings(arr)

	n := len(timestamp) + len(nonce) + len(token)
	var b strings.Builder
	b.Grow(n)
	for i := 0; i < len(arr); i++ {
		b.WriteString(arr[i])
	}

	return Sha1(b.String()) == signature
}

// 進行Sha1編碼
func Sha1(str string) string {
	h := sha1.New()
	h.Write([]byte(str))
	return hex.EncodeToString(h.Sum(nil))
}
複製程式碼

最後,將專案部署至伺服器,並在介面配置資訊中點選提交按鈕,完成微信公眾號的接入。

Go 實戰丨微信公眾號接入及使用者訊息處理

Go 實戰丨微信公眾號接入及使用者訊息處理

需注意,由於該 Web 程式需監聽 80 埠,所以伺服器不能有其他監聽 80 埠的程式,如 nginx

訊息接收

完成微信公眾號的接入後,接下來以普通訊息接收和被動回覆使用者訊息這兩個 API 為例,來完成 Go 對微信訊息的接收和回覆處理的具體實現。

首先是訊息接收,參考微信官方文件,接收普通訊息

當普通微信使用者向公眾賬號發訊息時,微信伺服器將POST訊息的XML資料包到開發者填寫的URL上。

以文字訊息為例,其 XML 資料包結構以及引數描述分別如下:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[this is a test]]></Content>
  <MsgId>1234567890123456</MsgId>
</xml>
複製程式碼
引數 描述
ToUserName 開發者微訊號
FromUserName 傳送方帳號(一個OpenID)
CreateTime 訊息建立時間 (整型)
MsgType 訊息型別,文字為text
Content 文字訊息內容
MsgId 訊息id,64位整型

明白了微信伺服器向開發伺服器傳遞微信使用者訊息的方式以及傳遞的資料包結構後,可知進行訊息接收開發,大致需要進行兩個步驟:

  1. 建立處理微信伺服器傳送到開發伺服器的 POST 型別請求的處理函式;
  2. 對請求中的 XML 資料包進行解析。

對於第二步,我們可以藉助 Gin 框架的 ShouldBindXMLBindXML 方法來對 XML 資料包進行解析。

使用 Go 實現的訊息接收程式碼如下:

package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"weixin-demo/util"
)

const Token = "coleliedev"

func main() {
	router := gin.Default()

	router.GET("/wx", WXCheckSignature)
	router.POST("/wx", WXMsgReceive)

	log.Fatalln(router.Run(":80"))
}

// WXTextMsg 微信文字訊息結構體
type WXTextMsg struct {
	ToUserName   string
	FromUserName string
	CreateTime   int64
	MsgType      string
	Content      string
	MsgId        int64
}

// WXMsgReceive 微信訊息接收
func WXMsgReceive(c *gin.Context) {
	var textMsg WXTextMsg
	err := c.ShouldBindXML(&textMsg)
	if err != nil {
		log.Printf("[訊息接收] - XML資料包解析失敗: %v\n", err)
		return
	}

	log.Printf("[訊息接收] - 收到訊息, 訊息型別為: %s, 訊息內容為: %s\n", textMsg.MsgType, textMsg.Content)
}
複製程式碼

將新增訊息接收的程式碼更新至伺服器後,對該介面測試號傳送訊息,可在伺服器檢視到如下記錄:

Go 實戰丨微信公眾號接入及使用者訊息處理

Go 實戰丨微信公眾號接入及使用者訊息處理

訊息回覆

接下來以被動回覆使用者訊息這個 API 為例,實現對微信使用者傳送的訊息的回覆,參考微信官方文件,被動訊息回覆

訊息回覆與訊息接收類似,都需要使用 XML 格式的資料包,回覆文字訊息需要的 XML 資料包結構以及引數如下:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>12345678</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[你好]]></Content>
</xml>
複製程式碼
引數 是否必須 描述
ToUserName 接收方帳號(收到的OpenID)
FromUserName 開發者微訊號
CreateTime 訊息建立時間 (整型)
MsgType 訊息型別,文字為text
Content 回覆的訊息內容(換行:在content中能夠換行,微信客戶端就支援換行顯示)

使用 Go 實現的訊息回覆程式碼如下:

package main

import (
	"encoding/xml"
	"fmt"
	"github.com/gin-gonic/gin"
	"log"
	"time"
	"weixin-demo/util"
)

const Token = "coleliedev"

func main() {
	router := gin.Default()

	router.GET("/wx", WXCheckSignature)
	router.POST("/wx", WXMsgReceive)

	log.Fatalln(router.Run(":80"))
}

// WXMsgReceive 微信訊息接收
func WXMsgReceive(c *gin.Context) {
	var textMsg WXTextMsg
	err := c.ShouldBindXML(&textMsg)
	if err != nil {
		log.Printf("[訊息接收] - XML資料包解析失敗: %v\n", err)
		return
	}

	log.Printf("[訊息接收] - 收到訊息, 訊息型別為: %s, 訊息內容為: %s\n", textMsg.MsgType, textMsg.Content)

	// 對接收的訊息進行被動回覆
	WXMsgReply(c, textMsg.ToUserName, textMsg.FromUserName)
}

// WXRepTextMsg 微信回覆文字訊息結構體
type WXRepTextMsg struct {
	ToUserName   string
	FromUserName string
	CreateTime   int64
	MsgType      string
	Content      string
	// 若不標記XMLName, 則解析後的xml名為該結構體的名稱
	XMLName      xml.Name `xml:"xml"`
}

// WXMsgReply 微信訊息回覆
func WXMsgReply(c *gin.Context, fromUser, toUser string) {
	repTextMsg := WXRepTextMsg{
		ToUserName:   toUser,
		FromUserName: fromUser,
		CreateTime:   time.Now().Unix(),
		MsgType:      "text",
		Content:      fmt.Sprintf("[訊息回覆] - %s", time.Now().Format("2006-01-02 15:04:05")),
	}

	msg, err := xml.Marshal(&repTextMsg)
	if err != nil {
		log.Printf("[訊息回覆] - 將物件進行XML編碼出錯: %v\n", err)
		return
	}
	_, _ = c.Writer.Write(msg)
}
複製程式碼

需要注意的是 WXRepTextMsg 結構體中必須新增 XMLName 屬性,並且對該屬性進行 xml 標記,用於將該 xml 名標記為 xml,即使用 xml.Marshal 方法對該結構體物件進行編碼後,得到的 xml 資料的最外層標籤為 <xml></xml>,如若不新增該 XMLName 屬性,則編碼後得到的 xml 資料的最外層標籤為 <WXRepTextMsg></WXRepTextMsg>,不符合微信官方要求的 xml 資料包格式,因此所有 xml 名稱即編碼後的 xml 資料的最外層標籤不為 <xml></xml> 的資料包都無法成功回覆。

將新增訊息回覆的程式碼更新至伺服器後,向伺服器傳送訊息將收到如下回復:

Go 實戰丨微信公眾號接入及使用者訊息處理

Go 實戰丨微信公眾號接入及使用者訊息處理

使用 nginx 代理伺服器

通常伺服器都不會把 80 埠或 443 埠交給 Web 程式,這時可使用 nginx 作為代理伺服器,將 Web 程式跑在其它埠上,如 8002,讓 nginx 監聽 80 埠或 443 埠,並對指定的 URI 進行反向代理操作,如以下配置,將把 80 埠 URI 為 /weixin 的請求代理到伺服器本地的 8002 埠的 /wx 上:

server {
	listen 80;

	location /weixin {
		proxy_pass http://127.0.0.1:8002/wx;
		proxy_redirect default;
	}
}
複製程式碼

修改程式監聽的埠號為 8002:

func main() {
	router := gin.Default()

	router.GET("/wx", WXCheckSignature)
	router.POST("/wx", WXMsgReceive)

	log.Fatalln(router.Run(":8002"))
}
複製程式碼

修改微信公眾號接入介面配置:

Go 實戰丨微信公眾號接入及使用者訊息處理

Go 實戰丨微信公眾號接入及使用者訊息處理

最後測試結果如下:

Go 實戰丨微信公眾號接入及使用者訊息處理

Go 實戰丨微信公眾號接入及使用者訊息處理

Go 實戰丨微信公眾號接入及使用者訊息處理

nginx 的日誌檔案中可以看到其確實收到了 URI 為 /weixin 的請求,並且在該 Web 程式的日誌檔案中,也可以看到其收到的 URI 為 /wx 的請求,通過觀察兩份日誌記錄的請求引數,可以發現,nginx 做的代理是成功的。

小結

最後做一個對該文章的小結,這篇文章主要使用了 Go 語言的 Gin 框架以及藉助微信介面測試號,完成了對微信公眾號接入的開發,以及實現接收微信使用者訊息和回覆微信使用者訊息的兩個功能。

以及,感謝大家的耐心閱讀!!!

演示 Demo github 地址:github.com/hkail/weixi…

演示 Demo gitee 地址:gitee.com/hkail/weixi…

相關文章