实现远程验证回调企业微信本地接口服务开发场景
内网穿透
通过使用内网穿透工具提供的公网域名成功验证回调本地接口服务,企业微信开发者在应用的开发测试阶段,应用服务通常是部署在开发环境,在有数据回调的开发场景下,企业微信的回调数据无法直接请求到开发环境的服务。
内网穿透工具可以帮助开发者将应用开发调试过程中的回调请求,穿透到本地的开发环境。Cpolar
是一种安全的内网穿透的服务,可以将内网下的本地服务器通过安全隧道暴露至公网,使得公网用户可以正常访问内网服务,是一款优秀内网穿透软件。
cpolar
官网:https://www.cpolar.com/
安装完成后,启动软件,会在浏览器打开一个网页,输入邮箱、密码登录
(图1
)
隧道管理 -> 创建隧道,创建一个本地服务的 http
隧道
- 隧道名称:可自定义命名,注意不要与已有的隧道名称重复
- 协议:选择
http
- 本地地址:
8080
(回调自己本地服务的端口)
- 域名类型:免费选择随机域名
- 地区:选择
China
(图2
)
隧道创建成功后,状态 -> 在线隧道列表,查看所生成的域名地址,有两种访问方式,分别是 http
和 https
,域名我们需要记住,稍后在企业微信开发者中心创建应用的时候需要填写该域名!
(图3
)
进入微信企业开发者中心,登录后,点击上方工具,再点击左侧网页应用开发,进入创建应用界面,点击创建应用
(图4
)
把要求填写的域名信息,填写上面我们在 cpolar
中创建的域名,然后点击创建应用
(图5
)
创建项目引入加解密库
使用 xml
版本的
(图6
)
(图7
)
解压后,放在 tool 文件夹中
(图8
)
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
|
<!-- 针对org.apache.commons.codec.binary.Base64-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<!-- 使用IOUtils-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- 企业微信json格式包-->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20200518</version>
</dependency>
<!-- dom4j解析xml -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
|
引入加密库后,提示非法字符’\ufeff'
该错误通常发生在尝试编译 Java
源代码文件时,文件开头的字符是一个字节顺序标记(Byte Order Mark,BOM
),即 \ufeff
。在 Java
中,\ufeff
不是一个合法的字符,因此编译器会报 “非法字符: ‘\ufeff
’” 错误。
解决方法
- 在
Idea
界面右下角处将 utf-8
改为 GBK
- 在弹出的窗口中点击转换
- 接着再重新将编码格式改回
utf-8
就可以啦
验证可信域名
下载可信域名验证文件,并放到静态目录下
(图9
)
验证回调
企业微信会先判断 URL
服务是否具备解析企业微信推送消息的能力,然后再发送业务回调,也就是连着发送两次请求,所以在 command
方法中,需要判断请求的类型,如果是 get
,表示该请求用来验证是否具备解析能力,如果是 post
,表示该请求用来执行业务回调
(图10
)
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
@RestController
@RequestMapping(value = "/callback")
public class WeChatConfig {
/**
* 3.1 支持Http Get请求验证URL有效性
*/
@GetMapping("/data")
public String data(HttpServletRequest request) throws IOException {
String sEchoStr = "";
String sVerifyMsgSig = request.getParameter("msg_signature");
String sVerifyTimeStamp = request.getParameter("timestamp");
String sVerifyNonce = request.getParameter("nonce");
String sVerifyEchoStr = request.getParameter("echostr");
try {
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt("Nqyyj3yLDT4R2Q6idhtTJ2tP8t", "F20sj9GfH8lx66HKirERy5zjoMCDGHdugTXVmOvXJ7f", "");
sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);
System.out.println("verifyurl echostr: " + sEchoStr);
// 验证URL成功,将sEchoStr返回
// response.getWriter().write(sEchoStr);
return sEchoStr;
} catch (Exception e) {
//验证URL失败,错误原因请查看异常
e.printStackTrace();
// response.getWriter().write("failed");
return "failed";
}
}
/**
* 3.2 支持Http Post请求接收业务数据
*
* @return
*/
@RequestMapping(value = "/command", method = {RequestMethod.GET, RequestMethod.POST})
public String command(HttpServletRequest request, @RequestBody(required = false) String body) throws Exception {
// 判断请求方法
String method = request.getMethod();
if ("GET".equalsIgnoreCase(method)) {
return data(request);
} else {
String sReqMsgSig = request.getParameter("msg_signature");
String sReqTimeStamp = request.getParameter("timestamp");
String sReqNonce = request.getParameter("nonce");
try {
//wwfb662f05212e49eb是应用id
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt("Nqyyj3yLDT4R2Q6idhtTJ2tP8t", "F20sj9GfH8lx66HKirERy5zjoMCDGHdugTXVmOvXJ7f", "wwfb662f05212e49eb");
//解密
String sMsg = wxcpt.DecryptMsg(sReqMsgSig, sReqTimeStamp, sReqNonce, body);
System.out.println("解密后的数据:" + sMsg);
//将post数据转换为map
Map<String, String> dataMap = MessageUtil.parseXml(sMsg);
System.out.println("转为map的数据:" + dataMap.toString());
return "success";
} catch (AesException e) {
throw new RuntimeException(e);
}
}
}
}
|
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
package com.example.demo.tool;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MessageUtil {
/**
* 返回消息类型:文本.
*/
public static final String RESP_MESSAGE_TYPE_TEXT = "text";
/**
* 返回消息类型:音乐.
*/
public static final String RESP_MESSAGE_TYPE_MUSIC = "music";
/**
* 返回消息类型:图文.
*/
public static final String RESP_MESSAGE_TYPE_NEWS = "news";
/**
* 请求消息类型:文本.
*/
public static final String REQ_MESSAGE_TYPE_TEXT = "text";
/**
* 请求消息类型:图片.
*/
public static final String REQ_MESSAGE_TYPE_IMAGE = "image";
/**
* 请求消息类型:链接.
*/
public static final String REQ_MESSAGE_TYPE_LINK = "link";
/**
* 请求消息类型:地理位置.
*/
public static final String REQ_MESSAGE_TYPE_LOCATION = "location";
/**
* 请求消息类型:音频.
*/
public static final String REQ_MESSAGE_TYPE_VOICE = "voice";
/**
* 请求消息类型:推送.
*/
public static final String REQ_MESSAGE_TYPE_EVENT = "event";
/**
* 事件类型:subscribe(订阅).
*/
public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";
/**
* 事件类型:unsubscribe(取消订阅).
*/
public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";
/**
* 事件类型:CLICK(自定义菜单点击事件).
*/
public static final String EVENT_TYPE_CLICK = "CLICK";
/**
* 事件类型:taskcard_click(点击任务卡片按钮).
*/
public static final String EVENT_TYPE_TASKCARD_CLICK = "taskcard_click";
/**
* 事件类型:open_approval_change(审批状态通知事件).
*/
public static final String EVENT_TYPE_OPEN_APPROVAL_CHANGE = "open_approval_change";
public static final String EVENT_TYPE_ENTER_AGENT = "enter_agent";
/**
* 解析微信发来的请求(XML).
*
* @param msg 消息
* @return map
*/
@SuppressWarnings("unchecked")
public static Map<String, String> parseXml(final String msg) {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
try (InputStream inputStream = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8.name()))) {
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
|
注意
数据回调 URL
申请校验不通过
加解密库里,ReceiveId
在各个场景的含义不同:
- 企业应用的回调,表示
corpid
- 第三方事件的回调,表示
suiteid
- 个人主体的第三方应用的回调,
ReceiveId
是一个空字符串
(图13
)
应用安装测试,应用添加失败
(图11
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@RestController
public class WechatController {
@Autowired
private WeChatConfig weChatConfig;
/**
* 安装测试应用,对微信的请求进行响应
* @param request
* @param body
* @return
* @throws Exception
*/
@RequestMapping(value = "/callback", method = {RequestMethod.GET, RequestMethod.POST})
public String callback(HttpServletRequest request, @RequestBody(required = false) String body) throws Exception {
return weChatConfig.command(request,body);
}
}
|
企业微信网页应用的局限性
权限少,什么都干不了
(图12
)
(图14
)
(图15
)