一 token机制微信公众号签名验证的方法
1 token机制
token机制就是使用一个token(通常是一个字符串,长度没有特别限制,一般是10字节或者16字节),然后按照一定的算法生成签名,然后对接的双方通过这个签名进行判断,相等则token认证成功,不相等则认证失败。
下面将以微信公众号的token机制算法为例进行讲述。
1.1 token算法
微信公众号token的验证方法,实际在验证token时,不会直接验证token,而是验证由token生成的签名,因为直接验证token,就需要进行传输,这样会导致泄露风险大大增加。
验证步骤:
- 1)后台服务器(我们自己的程序)获取token,nonce,timestamp组成列表list。
其中token在微信公众号、后台服务器各保存一份,因为不能直接传输,防止被破解。nonce,timestamp是微信公众号发来的。 - 2)list列表排序。
- 3)排序后的元素进行字符串拼接。
- 4)对比签名signature。
- 5)响应echostr。
在这里插入图片描述
1.2 token算法-流程图
1.3 代码演示token机制
下面以微信公众号的token算法为例,演示后台服务器如何通过token生成签名,然后再对比微信公众号发送过来的签名进行对比验证。
package main
import (
"bytes"
"crypto/rand"
"crypto/sha1"
"fmt"
"math/big"
"sort"
"strconv"
"time"
)
func CreateRandomString(len int) string {
// 1. 定义产生随机字符串的源
var container string
var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
// 2. 获取字符串的长度,并转成big.Int的类型,方便调用时rand.Int传参。
// b这个变量应该可以不需要
b := bytes.NewBufferString(str) // 创建一个Buffer(内部最终是使用[]string存储),并使用参数str进行初始化。
length := b.Len() // 62
bigInt := big.NewInt(int64(length)) // 创建一个Int结构(内部最终是使用uint存储),并使用参数x int64进行初始化。
fmt.Println("bingInt: ", bigInt) // 62
// 3. 根据传进的长度,获取随机字符串container。
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt) // 获取[0, max)随机字符的下标。
container += string(str[randomInt.Int64()])
}
return container
}
// 根据 Token Timestamp Nonce 生成对应的校验码, Token是不能明文传输的
func GenerateSignature(token string) (timestamp string, nonce string, signature string) {
// 1. 产生随机字符串。
nonce = CreateRandomString(10)
// 2. 产生时间戳并转成字符串形式。
timestamp = strconv.FormatInt(time.Now().Unix(), 10) // 将int64类型的时间戳转成字符串。10代表这个int64时间戳的进制。
// 3. 排序,微信约定好的。这样在对比签名时才能准确判断签名。
strs := sort.StringSlice{token, timestamp, nonce} // 将三者作为元素放到字符串切片。
sort.Strings(strs) // 排序。strs: [1650007402 lw5cs21kw2 tanyiyuan]
fmt.Println("strs:", strs)
// 4. 将切片里的每个元素进行依次拼接,得到一个完整的字符串。
str := ""
for _, s := range strs {
str += s // 拼接字符串
}
fmt.Println("str:", str) // str: 1650007402lw5cs21kw2tanyiyuan
// 5. 加密得到签名。
h := sha1.New() // 微信用sha1,所以这里使用sha1加密。如果完全都是自己的服务的时候,用md5加密也是没问题的。
h.Write([]byte(str)) // 转成byte写进digest中。h实际是一个digest类型。
signature = fmt.Sprintf("%x", h.Sum(nil)) // h.Sum(nil)做hash,然后转成16进制赋值给signature。
return
}
func VerifySignature(token string, timestamp string, nonce string, signature string) bool {
// 1. 排序。
// str = token + timestamp + nonce
strs := sort.StringSlice{token, timestamp, nonce} // 使用本地的token生成校验
sort.Strings(strs)
// 2. 将切片里的每个元素进行依次拼接,得到一个完整的字符串。
str := ""
for _, s := range strs {
str += s
}
// 5. 加密得到签名,并判断签名是否一致。
h := sha1.New()
h.Write([]byte(str))
return fmt.Sprintf("%x", h.Sum(nil)) == signature
}
func main() {
// 1. 定义token
token := "tanyiyuan"
// 2. 模拟微信公众号产生签名。
timestamp, nonce, signature := GenerateSignature(token) // 发送给服务器的时候是发送 timestamp, nonce, signature
fmt.Printf("1. token %s , timestamp:%s, nonce:%s, -> 产生签名: %s\n", token, timestamp, nonce, signature)
// 3. 模拟后台服务器验证签名。其中token在后台服务器会保留一份,timestamp, nonce, signature都是微信公众号发送过来的。
ok := VerifySignature(token, timestamp, nonce, signature) // 服务进行校验
if ok {
fmt.Println("2. 验证签名正常")
} else {
fmt.Println("2. 验证签名失败")
}
}
运行结果:
上面的内容可以微信公众号的开发者文档参考:开启公众号开发者模式。
二 XML解析,CDATA解析
因为微信公众号是使用xml进行交互的。所以我们需要用到xml相关的第三方库。
下面可能涉及到对一些协议的结构体的封装。具体微信公众号协议格式,可以看微信官方文档:基础消息能力。
2.1 XML解析-解析XML
首先我们先看如何解析xml。
package main
import (
"encoding/xml"
"fmt"
"io/ioutil"
"os"
)
// 如果struct中有一个叫做XMLName,且类型为xml.Name字段,
// 那么在解析的时候就会保存这个element的名字到该字段, 比如这里的config。
// 即XMLName xml.Name的名字以xml文件的最外层进行命名。
type SConfig struct {
XMLName xml.Name `xml:"config"` // 指定最外层的标签为config
SmtpServer string `xml:"smtpServer"` // 读取smtpServer配置项,并将结果保存到SmtpServer变量中
SmtpPort int `xml:"smtpPort"`
Sender string `xml:"sender"`
SenderPasswd string `xml:"senderPasswd"`
Receivers SReceivers `xml:"receivers"` // 读取receivers标签下的内容,以结构方式获取。说明可以嵌套读取。
}
type SReceivers struct {
Age int `xml:"age"`
Flag string `xml:"flag,attr"` // 读取flag属性
User []string `xml:"user"` // 读取user数组
Script string `xml:"script"` // 读取 <![CDATA[ xxx ]]> 数据
}
func main() {
// 1. 打开xml文件。
file, err := os.Open("4-1-xml.xml") // For read access.
if err != nil {
fmt.Printf("error: %v", err)
return
}
defer file.Close()
// 2. 读取数据。
data, err := ioutil.ReadAll(file)
if err != nil {
fmt.Printf("error: %v", err)
return
}
// 3. 反序列化。将二进制数据读到结构体。
v := SConfig{}
err = xml.Unmarshal(data, &v) // 反序列化
if err != nil {
fmt.Printf("error: %v", err)
return
}
// 4. 这样便得到xml里面的数据,进行下一步操作。
fmt.Println("文本:", v)
fmt.Println("+++++++++++解析结果:")
fmt.Println("XMLName: ", v.XMLName)
fmt.Println("SmtpServer: ", v.SmtpServer)
fmt.Println("SmtpPort: ", v.SmtpPort)
fmt.Println("Sender: ", v.Sender)
fmt.Println("SenderPasswd: ", v.SenderPasswd)
fmt.Println("Receivers.Flag: ", v.Receivers.Flag)
for i, element := range v.Receivers.User {
fmt.Println(i, element)
}
fmt.Println("Receivers.Script: ", v.Receivers.Script)
}
4-1-xml.xml:
<config>
<smtpServer>smtp.xxx.com</smtpServer>
<smtpPort>5678</smtpPort>
<sender>tyy</sender>
<senderPasswd>123456</senderPasswd>
<receivers flag="true">
<user>user1</user>
<user>user2</user>
<script>
<![CDATA[
function &%< matchwo(a,b) {
if (a < b && a < 0) then {
return 1;
} else {
return 0;
}
}
]]>
</script>
</receivers>
</config>
运行结果:
2.2 XML解析-解析CDATA
-
XML 文档中的所有文本均会被解析器解析。只有 CDATA 区段中的文本会被解析器忽略。术语 CDATA 是不应该由 XML 解析器解析。
-
像 “<” 和 “&” 字符在 XML 元素中都是非法的。
“<” 会产生错误,因为解析器会把该字符解释为新元素的开始。
“&” 会产生错误,因为解析器会把该字符解释为字符实体的开始,因为在xml中,遇到"&"解析器会认为它是一个转义字符,解析器会将其进行转义。 等价于C++字符串中的反斜杠"",因为字符想转义就需要假设反斜杠。 -
某些文本,比如 JavaScript 代码,包含大量 “<” 或 “&” 字符。为了避免错误,可以将脚本代码定义为 CDATA。
CDATA 部分中的所有内容都会被解析器忽略。
CDATA 部分由 “ <![CDATA[ " 开始,由 "]]>" 结束。
例如,演示上面"&"在xml会被转义。我们使用在线xml文档进行测试。JSON/XML互转。
当我们输入<config>&</config>
,转成json会变成{ "config": "&" }
。说明"&"就是转义字符。
在xml中,&转义后就是&。
package main
import (
"encoding/xml"
"fmt"
"strconv"
"time"
"github.com/clbanning/mxj"
)
// tag中含有"-"的字段不会输出
// tag中含有"name,attr",会以name作为属性名,字段值作为值输出为这个XML元素的属性,如上version字段所描述
// tag中含有",attr",会以这个struct的字段名作为属性名输出为XML元素的属性,类似上一条,只是这个name默认是字段名了。
// tag中含有",chardata",输出为xml的 character data而非element。
// tag中含有",innerxml",将会被原样输出,而不会进行常规的编码过程
// tag中含有",comment",将被当作xml注释来输出,而不会进行常规的编码过程,字段值中不能含有"--"字符串
// tag中含有"omitempty",如果该字段的值为空值那么该字段就不会被输出到XML,空值包括:false、0、nil指针或nil接口,任何长度为0的array, slice, map或者string
// 封装CData,方便后续增加成员.
type CDATAText struct {
Text string `xml:",innerxml"`
}
// 微信公众号的消息都会有这4个字段。所以封装为一个结构体。
type Base struct {
FromUserName CDATAText
ToUserName CDATAText
MsgType CDATAText
CreateTime CDATAText
}
// 文本消息的封装
type TextMessage struct {
XMLName xml.Name `xml:"xml"`
Base
Content CDATAText
}
// 图片消息的封装
type PictureMessage struct {
XMLName xml.Name `xml:"xml"`
Base
PicUrl CDATAText
MediaId CDATAText
}
// 将传入的字符串封装成CDATAText结构
func value2CDATA(v string) CDATAText {
return CDATAText{"<![CDATA[" + v + "]]>"}
}
func main() {
fmt.Println("============================")
xmlStr := `<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1649048791</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
</xml>`
// 1. 解析 XML
var Message map[string]interface{}
m, err := mxj.NewMapXml([]byte(xmlStr)) //使用了第三方的库
if err != nil {
return
}
// 2. 判断最外层元素是否是xml,如果不是则认为它不是xml格式。
if _, ok := m["xml"]; !ok {
fmt.Println("Invalid Message.")
return
}
fmt.Println("-->m:", m)
// 3. 把xml对应的值读取出来
message, ok := m["xml"].(map[string]interface{})
if !ok {
fmt.Println("Invalid Field `xml` Type.")
return
}
Message = message
fmt.Println("解析出来:", Message) // xml对应的字段还是在map
fmt.Println("============================")
// 2. 回复消息。依赖读取到的内容,重新封装成XML。
var reply TextMessage
inMsg, ok := Message["Content"].(string) // 读取内容 .(string)转成什么类型的数据
if !ok {
return
}
fmt.Println("Message[ToUserName].(string):", Message["ToUserName"].(string)) // 如果服务器要处理
// 封装回复消息,需要添加 CDATA
reply.Base.FromUserName = value2CDATA(Message["ToUserName"].(string))
reply.Base.ToUserName = value2CDATA(Message["FromUserName"].(string))
reply.Base.CreateTime = value2CDATA(strconv.FormatInt(time.Now().Unix(), 10))
reply.Base.MsgType = value2CDATA("text")
reply.Content = value2CDATA(fmt.Sprintf("我收到的是:%s", inMsg))
replyXml, err := xml.Marshal(reply) // 得到的是byte
fmt.Println("生成XML:", string(replyXml)) // []byte -> string
fmt.Println("生成XML:", []byte(string(replyXml))) // string -> []byte
}
运行结果:
三 交换协议、接收消息协议、被动回复消息协议
交换协议可以理解为包含接收消息协议、被动回复消息协议。
下面以接收消息协议、被动回复消息协议进行讲解。
3.1 你问我答-接收消息协议
以文本类消息为例,接收消息协议的文本协议格式以及各字段含义入下图。
详细参考微信官方文档:接收普通消息。
该文档看到,除了上图的文本类格式,还要其它例如图片、语音、视频等格式。
3.2 你问我答-被动回复消息协议
同样以文本类消息为例,该被动回复消息协议的文本消息与上面的差不多,只是少了一个MsgId属性字段。
详细请看微信官方文档:被动回复用户消息。
该文档看到,除了上图的文本类格式,还要其它例如图片、语音、视频等格式。
关于接收消息协议、被动回复消息协议两者的区别,笔者目前认为只是单纯的协议字段不太一样,实际含义应该差不多,都是微信粉丝发送信息后,微信公众号根据情况封装成 接收消息协议 或者 被动回复消息协议,再转发到后台服务器处理。
例如上面接收消息协议、被动回复消息协议的文本消息,两者只相差了一个MsgId属性字段。
四 演示结果
这里go写的代码就不给出来了,大家可以使用微信官方提供的python代码案例进行测试。链接是开启公众号开发者模式。
这是我的结果,公众号粉丝发送什么内容,我们后台就回复什么内容:
五 go语言之进阶篇正则表达式
参考我之前写的文章:go语言基础-----11-----正则表达式。
或者go的官方:Golang标准库文档。
这里不再多说。