加解密方案说明 目录 - [概述](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E6%A6%82%E8%BF%B0) - [使用已有库](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E4%BD%BF%E7%94%A8%E5%B7%B2%E6%9C%89%E5%BA%93) -       [初始化加解密类](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8A%A0%E8%A7%A3%E5%AF%86%E7%B1%BB) -       [验证URL函数](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E9%AA%8C%E8%AF%81url%E5%87%BD%E6%95%B0) -       [解密函数](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E8%A7%A3%E5%AF%86%E5%87%BD%E6%95%B0) -       [加密函数](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%8A%A0%E5%AF%86%E5%87%BD%E6%95%B0) - [原理详解](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3) -       [消息体签名校验](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E6%B6%88%E6%81%AF%E4%BD%93%E7%AD%BE%E5%90%8D%E6%A0%A1%E9%AA%8C) -       [明文msg的加密过程](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E6%98%8E%E6%96%87msg%E7%9A%84%E5%8A%A0%E5%AF%86%E8%BF%87%E7%A8%8B) -       [密文解密得到msg的过程](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%AF%86%E6%96%87%E8%A7%A3%E5%AF%86%E5%BE%97%E5%88%B0msg%E7%9A%84%E8%BF%87%E7%A8%8B) -       [举例说明](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E4%B8%BE%E4%BE%8B%E8%AF%B4%E6%98%8E) - [附注:ReceiveId 含义](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E9%99%84%E6%B3%A8%EF%BC%9Areceiveid-%E5%90%AB%E4%B9%89) ## [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E6%A6%82%E8%BF%B0)概述 企业微信在推送消息给企业时,会对消息内容做AES加密,以XML格式POST到企业应用的URL上。 企业在被动响应时,也需要对数据加密,以XML格式返回给企业微信。 本章节即是对加解密方法的说明。 阅读本章节前,需要了解以下术语: - msg\_signature: 消息签名,用于验证请求是否来自企业微信(防止攻击者伪造)。 - EncodingAESKey:用于消息体的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取,是AESKey的Base64编码。解码后即为32字节长的AESKey > AESKey=Base64\_Decode(EncodingAESKey + “=”) - AESKey:AES算法的密钥,长度为32字节。 AES采用CBC模式,数据采用PKCS#7填充至32字节的倍数;IV初始向量大小为16字节,取AESKey前16字节,详见:[http://tools.ietf.org/html/rfc2315](http://tools.ietf.org/html/rfc2315 "http://tools.ietf.org/html/rfc2315") - msg:为消息体明文,格式为XML/JSON - msg\_encrypt:明文消息msg加密处理后的Base64编码。 ## [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E4%BD%BF%E7%94%A8%E5%B7%B2%E6%9C%89%E5%BA%93)使用已有库 鉴于加解密算法相对复杂,企业微信提供了算法库。 目前已有c++/python/php/java/golang/c#等语言版本。均提供了解密、加密、验证URL三个接口,企业可根据自身需要下载,[下载地址](https://developer.work.weixin.qq.com/devtool/introduce?id=10128)。 > 使用现有库,用户不必细究加解密原理。对于找不到相应语言库的用户,请阅读后文[原理详解](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#12976/%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3)自行实现。欢迎大家分享~ 以c++为例,使用示例见下载的文件夹中的Sample.cpp, 此处做简单说明。 ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8A%A0%E8%A7%A3%E5%AF%86%E7%B1%BB)初始化加解密类 回调xml示例: > WXBizMsgCrypt wxcpt(sToken,sEncodingAESKey,sReceiveId); 回调json示例 > WXBizJsonMsgCrypt wxcpt(sToken,sEncodingAESKey,sReceiveId); 要求传参数sToken,sEncodingAESKey,sReceiveId。 sToken,sEncodingAESKey即[设置接收消息的参数](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#12977/%E8%AE%BE%E7%BD%AE%E6%8E%A5%E6%94%B6%E6%B6%88%E6%81%AF%E7%9A%84%E5%8F%82%E6%95%B0)章节所述配置的Token、EncodingAESKey。 特别注意, sReceiveId 在不同场景下有不同含义,见[附注](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#12976/%E9%99%84%E6%B3%A8)。 ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E9%AA%8C%E8%AF%81url%E5%87%BD%E6%95%B0)验证URL函数 本函数实现: 1. 签名校验 2. 解密数据包,得到明文消息内容 ```javascript int VerifyURL(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sEchoStr, string &sReplyEchoStr); ``` - 参数说明 | 参数 | 必须 | 说明 | |---------------|-----|----------------------------------------------| | sMsgSignature | 是 | 从接收消息的URL中获取的msg_signature参数 | | sTimeStamp | 是 | 从接收消息的URL中获取的timestamp参数 | | sNonce | 是 | 从接收消息的URL中获取的nonce参数 | | sEchoStr | 是 | 从接收消息的URL中获取的echostr参数。注意,此参数必须是urldecode后的值 | | sReplyEchoStr | 是 | 解密后的明文消息内容,用于回包。注意,必须原样返回,不要做加引号或其它处理 | ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E8%A7%A3%E5%AF%86%E5%87%BD%E6%95%B0)解密函数 本函数实现: 1. 签名校验 2. 解密数据包,得到明文消息结构体 ```javascript int DecryptMsg(const string &sMsgSignature, const string &sTimeStamp, const string &sNonce, const string &sPostData, string &sMsg); ``` - 参数说明 | 参数 | 必须 | 说明 | |---------------|-----|------------------------------------| | sMsgSignature | 是 | 从接收消息的URL中获取的msg_signature参数 | | sTimeStamp | 是 | 从接收消息的URL中获取的timestamp参数 | | sNonce | 是 | 从接收消息的URL中获取的nonce参数 | | sPostData | 是 | 从接收消息的URL中获取的整个post数据 | | sMsg | 是 | 用于返回解密后的msg,以xml组织,参见普通消息格式和事件消息格式 | ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%8A%A0%E5%AF%86%E5%87%BD%E6%95%B0)加密函数 本函数实现: 1. 加密明文消息结构体 2. 生成签名 3. 构造被动响应包 ```javascript int EncryptMsg(const string &sReplyMsg, const string &sTimeStamp, const string &sNonce, string &sEncryptMsg); ``` - 参数说明 | 参数 | 必须 | 说明 | |-------------|-----|---------------------------| | sReplyMsg | 是 | 返回的消息体原文 | | sTimeStamp | 是 | 时间戳,调用方生成 | | sNonce | 是 | 随机数,调用方生成 | | sEncryptMsg | 是 | 用于返回的密文,以xml组织,参见被动回复消息格式 | ## [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3)原理详解 目前官方已提供了php、python、c++等版本的加解密库,如果开发者需要进行别的语言的开发,需要自行根据加解密原理实现算法。 ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E6%B6%88%E6%81%AF%E4%BD%93%E7%AD%BE%E5%90%8D%E6%A0%A1%E9%AA%8C)消息体签名校验 为了让企业确认调用来自企业微信,企业微信在回调给接收消息url时会带上消息签名,以参数msg\_signature标识,企业需要验证此参数的正确性后再解密。 验证步骤如下: 1. 计算签名 > dev\_msg\_signature=sha1(sort(token、timestamp、nonce、msg\_encrypt))。 sort的含义是将**参数值**按照字母字典排序,然后**从小到大**拼接成一个字符串 sha1处理结果要编码为可见字符,编码的方式是把每字节散列值打印为%02x(即16进制,C printf语法)格式,全部小写 2. 比较dev\_msg\_signature和msg\_signature是否相等,相等则表示验证通过 3. 在被动响应消息时,企业同样需要用如上方法生成签名并传给企业微信 ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E6%98%8E%E6%96%87msg%E7%9A%84%E5%8A%A0%E5%AF%86%E8%BF%87%E7%A8%8B)明文msg的加密过程 1. 拼接明文字符串 > rand\_msg = random(16B) + msg\_len(4B) + msg + receiveid 明文字符串由16个字节的随机字符串、4个字节的msg长度、明文msg和receiveid拼接组成。其中msg\_len为msg的字节数,网络字节序;sReceiveId 在不同场景下有不同含义,见[附注](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#12976/%E9%99%84%E6%B3%A8) ![明文字符串](https://p.qpic.cn/pic_wework/23479275/c5c316c98b69520e9a274aa6542042bf7684f3674cb881d8/0) 2. 对明文字符串加密并Base64编码 > msg\_encrypt = Base64\_Encode(AES\_Encrypt(rand\_msg)) 将明文字符串AESKey加密后,再进行Base64编码,即获得密文msg\_encrypt。 ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E5%AF%86%E6%96%87%E8%A7%A3%E5%AF%86%E5%BE%97%E5%88%B0msg%E7%9A%84%E8%BF%87%E7%A8%8B)密文解密得到msg的过程 1. 对密文BASE64解码 > aes\_msg=Base64\_Decode(msg\_encrypt) 2. 使用AESKey做AES-256-CBC解密 > rand\_msg=AES\_Decrypt(aes\_msg) 3. 去掉rand\_msg头部的16个随机字节和4个字节的msg\_len,截取msg\_len长度的部分即为msg,剩下的为尾部的receiveid 4. 验证解密后的receiveid、msg\_len。注意,receiveid在不同场景含义不同。 ### [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E4%B8%BE%E4%BE%8B%E8%AF%B4%E6%98%8E)举例说明 假设在服务商管理端为某个套件有如下配置参数: ```python corpId = "wx5823bf96d3bd56c7" token = "QDG6eK" encodingAesKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C" ``` 收到来自企业微信的回调为: xml请求示例: ```xml POST /cgi-bin/wxpush?msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=1409659813&nonce=1372623149 HTTP/1.1 Host: qy.weixin.qq.com Content-Length: 603 ``` json请求示例: 注意这里的 tousername,encrypt,agentid均为小写 ```javascript POST /cgi-bin/wxpush?msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=1409659813&nonce=1372623149 HTTP/1.1 Host: qy.weixin.qq.com Content-Length: 364 { "tousername": "wx5823bf96d3bd56c7", "encrypt": "No8isRLoXqFMhLlpe7R/DA7UbJ88DKJxDhJH/UVG3o1ib0Fhzdd3qWYHH/KL1mITv5qOCp2FbyILqfI7zazrp/ARgSHR177OCrv8O9UrMHWdnOaMXaz+mLd5X5VWm5r2J3Qpm+NdTQRPhHbce88frKF3wqTaZunKW7ae87bRZUfaq5tLFnyTsf6aiy0su3SsQ06dQGKPcHfYHY3upB881008Q9t9xeAZ/uqfXpYQgSLQfaX+fk/K/FQEl4QpLk94eD1YjluFY3uLnKp40zDyxgeWRAmgTtmx1eLwediVqZ8=", "agentid": "218" } ``` **第一步:**准备相关参数 ```python AESKey = Base64_Decode(EncodingAESKey + "=") signature = "477715d11cdb4164915debcba66cb864d751f3e6"; timestamps = "1409659813"; nonce = "1372623149"; msg_encrypt = "RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q=="; ``` **第二步**:校验签名 1. token、timestamp、nonce、msg\_encrypt 这四个参数按照字典序排序 ```python "1372623149" "1409659813" "QDG6eK" "RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==" ``` 2. 拼接为一个字符串 ```python sort_str = "13726231491409659813QDG6eKRypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==" ``` 3. 对该字符串进行sha1计算得到签名 ```python signature = sha1(sort_str) = "477715d11cdb4164915debcba66cb864d751f3e6" ``` 4. 对比从URL得到的签名,发现两者一致,签名通过,说明没被篡改,是安全的 **第三步:** 解密消息 1. 对密文base64解码 ```python aes_msg = base64_decode(msg_encrypt) ``` 2. 使用AESKey做AES解密(**注意,不是EncodingAESKey**) ```python rand_msg = aes_decrypt(aes_msg, AESKey) ``` 3. 去掉rand\_msg头部的16个随机字节和4个字节的msg\_len,截取msg\_len长度的部分即为msg,剩下的为尾部的receiveid 下面为类似python的伪代码 ```python content = rand_msg[16:] # 去掉前16随机字节 msg_len = str_to_uint(content[0:4]) # 取出4字节的msg_len msg = content[4:msg_len+4] # 截取msg_len 长度的msg receiveid = content[msg_len+4:] = "wx5823bf96d3bd56c7" # 剩余字节为receiveid ``` 对于回调xml解密后得到明文为: ```xml 1409659813 4561255354251345929 218 ``` 对于回调json解密后的明文为: ```javascript { "ToUserName": "wx5823bf96d3bd56c7", "FromUserName": "mycreate", "CreateTime": "1409659813", "MsgType": "text", "Content": "hello", "MsgId": "4561255354251345929", "AgentID": "218" } ``` 根据明文中的MsgType可知,此为应用消息回调,因此receiveid应该为corpid,对比receiveid与corpid是否一致。 ## [](https://developer.work.weixin.qq.com/devtool/introduce?id=36388/#%E9%99%84%E6%B3%A8%EF%BC%9Areceiveid-%E5%90%AB%E4%B9%89)附注:ReceiveId 含义 加解密库里,ReceiveId 在各个场景的含义不同: 1. 企业应用的回调,表示corpid 2. 第三方事件的回调,表示suiteid 3. 机器人场景的回调,是一个空字符串