我们了解到:公众服务与微信服务器间的消息是“裸奔”的(即明文传输,通过抓包可以看到)。显然这对于一些对安全性要求较高的大企业服务号来说,比如银行、证券、电信运营商或航空客服等是不能完全满足要求的。于是乎就有了微信服务器与公众服务间的数据加密通信流程。

 

公众号管理员可以在公众号“开发者中心”选择是否采用”安全模式”(区别于明文模式):

2

一旦选择了“安全模式”,微信服务器在向公众号服务转发消息时会对XML数据包部分内容进行加密处理。这类加密后的请求Body中的XML数据变成了下面这样:

3

xml数据基本结构变成了:

4 

另外在“安全模式”下,Http Post Request line中也增加了两个字段:encrypt_type和msg_signuature,用于消息类型判断以及加密消息内容有效性校验:

POST/?signature=891789ec400309a6be74ac278030e472f90782a5&timestamp=1419214101&nonce=788148964&encrypt_type=aes&msg_signature=87d7b127fab3771b452bc6a592f530cd8edba950HTTP/1.1\r\n

 

其中:

 

encrypt_type = “aes”,说明是加密消息,否则为”raw”,即未加密消息。

 

msg_signature=sha1(sort(Token, timestamp,nonce, msg_encrypt))

 

对于测试号,测试号配置页面没有加密相关配置,因此只能通过“微信公众平台接口调试工具”来进行相关加密接口调试。

 

一、消息签名验证

 

对于“安全模式”下的消息交互,首先要做的就是消息签名验证,只有通过验证的消息才会进行下一步解密、解析和处理。

 

消息签名验证的原理是比较微信平台HTTP Post Line中携带的msg_signature与通过Token、timestamp、nonce和msg_encrypt等四个字段值计算出的msg_signture是否一致,一致则通过消息签名验证。

 

我们依旧在procRequest中完成对“安全模式”下消息的签名验证。

5

6

7

程序编译执行结果如下:

$sudo ./recvencryptedtextmsg

2014/12/22 13:15:56 Wechat Service: Start!

 

用手机微信发送一条消息给公众号,程序输出如下结果:

2014/12/22 13:17:35 Wechat Service: in safemode

2014/12/22 13:17:35 Wechat Service:msg_signature validation is ok!

 

二、数据包解密

 

到目前为止,我们已经得到了经过消息验证ok的加密数据包EncryptRequestBody的Encrypt。要想得到真正的消息内容,我们需要对Encrypt字段的值进行解密处理。微信采用的是AES加解密方案, 下面我们就来看看如何做AES解密。

 

在开发者中心选择转换为“安全模式”时,有一个字段EncodingAESKey需要填写,这个字段固定为43个字符,它就是我们在运用AES算法时需要的那个Key。不过这个EncodingAESKey是被编了码的,真正用来加解密的AESKey需要我们自己通过解码得到。解码方法为:

 

AESKey=Base64_Decode(EncodingAESKey + “=”)

Base64 decode后,我们就得到了一个32个字节的AESKey,可以看出微信加密解密用的是AES-256算法(256=32x8bit)。

 

在Golang中,我们可以通过下面代码得到真正的AESKey:

8

有了AESKey,我们再来解密数据包。微信公众平台开发文档给出了加密数据包的解析步骤:

 

1. aes_msg=Base64_Decode(msg_encrypt)

2. rand_msg=AES_Decrypt(aes_msg)

3. 验证尾部$AppId是否是自己的AppId,相同则表示消息没有被篡改,这里进一步加强了消息签名验证

4. 去掉rand_msg头部的16个随机字节,4个字节的msg_len和尾部的$AppId即为最终的xml消息体

 

微信Wiki中如果能用一个简单的图来说明Base64_Decode后的数据格式就更好了。这里进一步说明一下,解密后的数据,我们称之plainData,它由四部分组成,按先后顺序排列分别是:

 

1、随机值,16字节

2、xml包长度,4字节(注意以BIG_ENDIAN方式读取)

3、xml包(*这部分数据的长度由上一个字段标识,这个包等价于一个完整的文本接收消息体数据,从ToUsername到MsgID都 有)

4、appID

其中第三段xml包是一个完整的接收文本数据包,与“接收消息”一文中的标准文本数据包格式一致,这就方便我们解析了。好了,下面用代码阐述解密、解析过程以及appid验证:

 

在procRequest中,增加如下代码:

 9

根据解密方法,我们先对encryptRequestBody.Encrypt进行base64decode操作得到cipherData,再用aesDecrypt对cipherData进行解密得到上面提到的由四部分组成的plainData。plainData经过xmldecoding后就得到我们的TextRequestBody struct。

 

这里难点显然在aesDecrypt的实现上了。微信的加密包采用aes-256算法,秘钥长度32B,采用PKCS#7Padding方式。Golang提供了强大的AES加密解密方法,我们利用这些方法实现微信包的解密:

 10

对于解密后的plainData做appID校验以及xml Decoding处理如下:

11

编译执行输出textRequestBody:

&{{ xml} gh_6ebaca4bb551 on95ht9uPITsmZmq_mvuz4h6f6CI1.419239875s text Hello, Wechat 6095588848508047134}

 

三、响应消息的数据包加密

 

微信公众平台开发文档要求:公众账号对密文消息的回复也要求加密。

对比一下普通的响应消息格式和加密后的响应消息格式:

 12

加密后:

13

我们定义一个结构体映射响应消息数据包:

14

我们要做的就是给EncryptResponseBody的实例逐一赋值,然后通过xml.MarshalIndent转成xml数据流即可,各字段值生成规则如下:

Encrypt = Base64_Encode(AES_Encrypt[random(16B)+ msg_len(4B) + msg + $AppId])

MsgSignature=sha1(sort(Token, timestamp,nonce, msg_encrypt))

TimeStamp = 用请求中的值或新生成

Nonce = 用请求中的值或新生成

 

微信公众接口的加密复杂度要比解密高一些,关键问题在于加密结果的判定和加密逻辑的调试,AES加密出的结果每次都不同,我们要么通过微信平台真实操作验证,要么通过微信提供的在线调试工具验证加密是否正确。这里强烈建议使用在线调试工具(测试号只能选择这一种)。

 

在线调试工具的配置参考如下,ToUserName和FromUserName建议填写真实的(通过解密Post包打印输出得到):

15 

如果在线调试工具收到你的应答,并解密成功,会给出如下反馈:

16

在procRequest中,我们在接收解析完Http Request后,通过下面几行代码构造一个加密的Response返回给微信平台或调试工具:

17 

应答Xml包中只有Encrypt字段是加密的,该字段的生成方式如下:

 181920

根据官方文档: 微信所用的AES采用的时CBC模式,秘钥长度为32个字节(aesKey),数据采用PKCS#7填充;PKCS#7:K为秘钥字节数(采用32),buf为待加密的内容,N为其字节数。Buf需要被填充为K的整数倍。因此我们pad要加密的数据时,务必pad为k(=32)的整数倍,而不是aes.BlockSize(=16)的整数倍。

 

采用安全模式后的公众号消息交互性能似乎下降了,发送”hello,wechat”给公众号后好长时间才收到响应。

 

本文转载自Tony Bai。