关于获取微信第三方平台的 component_verify_ticket 和 component_access_token

独立的小程序已经满足不了我司的需求了,继而准备做微信第三方平台,,今天主要想总结下获取微信授权第三方平台流程中遇到的一些问题,,主要是Java代码(用php开发的朋友可以 Command + W 了..)。

推送 component_verify_ticket 协议

这一部分,官方文档真心惜墨。。对首次进行开发第三方授权开发的程序员来说真心不友好,在此提出批评(白天已经骂过无数次娘了🙂)!

关于 POST 数据示例
1
2
3
4
5
6
<xml>
<AppId></AppId>
<CreateTime></CreateTime>
<InfoType></InfoType>
<ComponentVerifyTicket></ComponentVerifyTicket>
</xml>

👆上边的 XML 其实是解密之后的格式。。推送过来时候我们获取到的数据是经过加密的,,像这样👇

1
2
3
4
<xml>
<AppId><![CDATA[wx5a062835463d1c61]]></AppId>
<Encrypt><![CDATA[3KAGFTaD32rFYsXMmjK/6Hk7nfJzrgIqQs4tqlFA5ORsuMhFvMM9P0cUKEoW9v+2oJztVFRI/OV09szzN7QqqxgapTGLYcyKnbViqs+uSnVkwc5YBM4YkAKGU2Yyp89/6tXU0FloSfUvM8bHZteed44Wo3B3diuhA3JIOK/viBfdfgODTZCe/q/JFt7yYc2Cr3ojtmR1n2M+PdQTfs3bf7tLlR4yURD5kXYrJkZPK7giNzE+uEgn52SUAAhLnAc68hWm0rnPhvPgVARSqL3sHK5xtxihoXcX0h15j8Al3KbZ3IqIdM/SIJrB0mnDx8hOnoI8UaBmChVWS8iz/Ygp/lr8mT37WK0bVeFKm2PkNBG91/icsxOacEJCuXdj/yD+rthlB2OQbmbdFPXw/8QPCNjHdj2RmEL78flwrRQeoNOHYvmMBtoU9jm0WAix6db8siyCA2CNFsC/QxD84p0N/Q==]]></Encrypt>
</xml>

那么加密后的数据是怎样获取到的呢??最开始的时候百思不得其解,要崩溃,都做噩梦了。。其实在后边的消息加解密接入指引提到了一些,,总之呢,我们可以通过request.getParameter(paraName)获取到四个参数,分别是👇

  • 时间戳 timestamp
  • 随机数 nonce
  • 加密类型 encrypt_type(值为aes)
  • 消息体签名 msg_signature(用于验证消息体的正确性)

这四个参数呢主要用来解密 经过加密的 xml 数据,即所谓的 postdata。

// 说到这儿,,不得不提下官方给出的加解密示例代码
其实,这5种语言的示例代码的场景是用来加解密微信公众号与用户互动的消息的,所以xml结构中有ToUserName标签。而实际上在本文的场景中,是不存在ToUserName标签的,取而代之的是AppId标签,,这一点,在官方文档中的消息加解密接入指引也有提到,只不过比较隐晦。。👇

公众号第三方平台可能会接收到两种类型的消息:

  1. 用户发送给公众号的消息(由公众号第三方平台代收)。此时,消息XML体中,ToUserName(即接收者)为公众号的原始 ID(可通过《接口说明》中的获取授权方信息接口来获得)。
  2. 微信服务器发送给服务自身的事件推送(如取消授权通知,Ticket 推送等)。此时,消息XML体中没有 ToUserName 字段,而是 AppId 字段,即公众号服务的 AppId。这种系统事件推送通知(现在包括推送 component_verify_ticket 协议和推送取消授权通知),服务开发者收到后也需进行解密,接收到后只需直接返回字符串“success”。

(PS: 这个 postdata 在官方给的解密加密示例代码中(Java版)的体现即 replyMsg)

OK, 那么问题来了 postdata 又是怎么获取的嘞???文档中好像并没有提到(难道是我没看到?!?) 😤,,无力吐槽,直接甩代码👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 获取加密后的 postdata
String encodedPostdata = HttpKit.readData(getRequest());

// readData()方法实现
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder ret;
br = request.getReader();
String line = br.readLine();
if (line != null) {
ret = new StringBuilder();
ret.append(line);
} else {
return "";
}
while ((line = br.readLine()) != null) {
ret.append('\n').append(line);
}
return ret.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
finally {
if (br != null) {
try {br.close();} catch (IOException e) {LogKit.error(e.getMessage(), e);}
}
}
}

至此,,我们就获得了加密后的 postdata 数据了,剩下的就是进行解密了,,解密之后,需要把 xml 格式的数据转为 Map,这样就可以愉快的get("ComponentVerifyTicket")了~ 贴下核心代码 👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 部分引入的包
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

try {
WXBizMsgCrypt pc = new WXBizMsgCrypt(String token, String encodingAesKey, String component_appid);
String result = pc.decryptMsg(msg_signature, timestamp, nonce, encodedPostdata);
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(result.getBytes("UTF-8"));
Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
// 至此 通过 data.get("ComponentVerifyTicket") 就可以获取 component_verify_ticket 了~
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
} catch (Exception e) {
e.printStackTrace();
}

获取第三方平台 component_access_token

这一步相对来说就没那么复杂了,,只遇到一个问题,就是appid参数缺失,微信返回信息如下👇

1
{"errcode":41002,"errmsg":"appid missing hint: [M07502974]"}

这里需要注意的是,,官方文档中是这样写的

POST数据示例:

{
“component_appid”:”appid_value” ,
“component_appsecret”: “appsecret_value”,
“component_verify_ticket”: “ticket_value”
}

请求参数说明
| 参数 | 说明 |
|:———————-|:——————:|
|component_appid | 第三方平台appid |
|component_appsecret | 第三方平台appsecret|
|component_verify_ticket| 微信后台推送的ticket,此ticket会定时推送,具体请见本页的推送说明|

所以,,post请求的数据为 json 格式的字符串,f**k。。。代码实现接上文component_verify_ticket部分👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if("component_verify_ticket".equals(data.get("InfoType"))) {
// 以下涉及到Redis部分,为伪代码,,领会精神即可
// TODO 把component_verify_ticket保存到 Redis 中
redis.set("component_verify_ticket", data.get("ComponentVerifyTicket"));
// TODO 检查 component_access_token 是否存在(过期)(有效期为两小时,过期前提前申请) 。如果过期,则重新向微信服务器申请。
if(!redis.exists("component_access_token")) {
final String url = "https://api.weixin.qq.com/cgi-bin/component/api_component_token";
Map<String, String> params = new HashMap<String, String>();
params.put("component_appid", component_appid;
params.put("component_appsecret", component_appsecret);
params.put("component_verify_ticket", data.get("ComponentVerifyTicket"));
// json格式的字符串,mmp
String para = JsonKit.toJson(params);
// ApiResult 为 JFinal-weixin 框架工具类
ApiResult apiResult = new ApiResult(HttpUtils.post(url, para));
logger.debug(apiResult.getJson());
if (!apiResult.isSucceed()) {
logger.info("微信返回的错误码 >>" + apiResult.getErrorMsg());
}
if(StrKit.notBlank(apiResult.get("component_access_token")+"")) {
// 提前十分钟失效(默认两个小时(7200s)有效),申请新的~
redis.setex("component_access_token", (Integer.parseInt(apiResult.get("expires_in")+"")-600), apiResult.get("component_access_token")+"");
logger.info("component_access_token 加入 Redis 成功~");
}else {
logger.debug('wtf?!');
// renderError(apiResult.getErrorMsg());
}
}
}else {
// 未知错误..
// renderCommonError(9999);
}
// 至此,以后需要 component_access_token 的时候,直接从 Redis 中取就是啦~

明天修改下小程序模板中登陆部分的代码就好啦,,以上如果有更好的实现方式,欢迎指教~