1
This commit is contained in:
@@ -1,25 +1,29 @@
|
|||||||
package cn.van333.wxsend.business.controller;
|
package cn.van333.wxsend.business.controller;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.van333.wxsend.business.model.R;
|
import cn.hutool.http.HttpRequest;
|
||||||
import cn.van333.wxsend.util.WeComCallbackCrypto;
|
import cn.van333.wxsend.util.WeComCallbackCrypto;
|
||||||
|
import cn.van333.wxsend.util.WeComPassiveReplyBuilder;
|
||||||
|
import cn.van333.wxsend.util.WeComPlainXmlParser;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/wecom/callback")
|
@RequestMapping("/wecom/callback")
|
||||||
public class WeComCallbackController {
|
public class WeComCallbackController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(WeComCallbackController.class);
|
private static final Logger logger = LoggerFactory.getLogger(WeComCallbackController.class);
|
||||||
|
private static final int HTTP_OK = 200;
|
||||||
|
|
||||||
@Value("${qywx.app.corpId:}")
|
@Value("${qywx.app.corpId:}")
|
||||||
private String corpId;
|
private String corpId;
|
||||||
@@ -30,6 +34,12 @@ public class WeComCallbackController {
|
|||||||
@Value("${qywx.app.encodingAESKey:}")
|
@Value("${qywx.app.encodingAESKey:}")
|
||||||
private String encodingAESKey;
|
private String encodingAESKey;
|
||||||
|
|
||||||
|
@Value("${jarvis.wecom.bridge-url:}")
|
||||||
|
private String jarvisBridgeUrl;
|
||||||
|
|
||||||
|
@Value("${jarvis.wecom.shared-secret:}")
|
||||||
|
private String jarvisSharedSecret;
|
||||||
|
|
||||||
@GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
|
@GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
public String verify(@RequestParam("msg_signature") String msgSignature,
|
public String verify(@RequestParam("msg_signature") String msgSignature,
|
||||||
@RequestParam("timestamp") String timestamp,
|
@RequestParam("timestamp") String timestamp,
|
||||||
@@ -37,12 +47,11 @@ public class WeComCallbackController {
|
|||||||
@RequestParam("echostr") String echostr) {
|
@RequestParam("echostr") String echostr) {
|
||||||
logger.info("WeCom callback verify: ts={}, nonce={}", timestamp, nonce);
|
logger.info("WeCom callback verify: ts={}, nonce={}", timestamp, nonce);
|
||||||
WeComCallbackCrypto crypto = new WeComCallbackCrypto(token, encodingAESKey, corpId);
|
WeComCallbackCrypto crypto = new WeComCallbackCrypto(token, encodingAESKey, corpId);
|
||||||
String plain = crypto.verifyURL(msgSignature, timestamp, nonce, echostr);
|
return crypto.verifyURL(msgSignature, timestamp, nonce, echostr);
|
||||||
return plain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = MediaType.TEXT_XML_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
@PostMapping(consumes = MediaType.TEXT_XML_VALUE)
|
||||||
public String receiveXml(@RequestParam("msg_signature") String msgSignature,
|
public ResponseEntity<String> receiveXml(@RequestParam("msg_signature") String msgSignature,
|
||||||
@RequestParam("timestamp") String timestamp,
|
@RequestParam("timestamp") String timestamp,
|
||||||
@RequestParam("nonce") String nonce,
|
@RequestParam("nonce") String nonce,
|
||||||
HttpServletRequest request) throws IOException {
|
HttpServletRequest request) throws IOException {
|
||||||
@@ -52,8 +61,67 @@ public class WeComCallbackController {
|
|||||||
WeComCallbackCrypto crypto = new WeComCallbackCrypto(token, encodingAESKey, corpId);
|
WeComCallbackCrypto crypto = new WeComCallbackCrypto(token, encodingAESKey, corpId);
|
||||||
String plainXml = crypto.decryptMsg(msgSignature, timestamp, nonce, encrypt);
|
String plainXml = crypto.decryptMsg(msgSignature, timestamp, nonce, encrypt);
|
||||||
logger.info("WeCom callback plain xml: {}", plainXml);
|
logger.info("WeCom callback plain xml: {}", plainXml);
|
||||||
// TODO: 在此解析 plainXml 的 MsgType/Event/Content 等,进行业务处理
|
|
||||||
return "success";
|
String msgType = WeComPlainXmlParser.msgType(plainXml);
|
||||||
|
if (!"text".equalsIgnoreCase(StrUtil.emptyToDefault(msgType, ""))) {
|
||||||
|
return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body("success");
|
||||||
|
}
|
||||||
|
|
||||||
|
String from = WeComPlainXmlParser.fromUserName(plainXml);
|
||||||
|
String content = WeComPlainXmlParser.content(plainXml);
|
||||||
|
String toCorp = WeComPlainXmlParser.toUserName(plainXml);
|
||||||
|
String agentId = WeComPlainXmlParser.agentId(plainXml);
|
||||||
|
String msgId = WeComPlainXmlParser.msgId(plainXml);
|
||||||
|
|
||||||
|
String reply = callJarvisBridge(from, content, toCorp, agentId, msgId);
|
||||||
|
if (StrUtil.isBlank(reply)) {
|
||||||
|
return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body("success");
|
||||||
|
}
|
||||||
|
|
||||||
|
String inner = WeComPassiveReplyBuilder.text(from, toCorp, reply);
|
||||||
|
String encryptedOuter = crypto.encryptMsg(inner, timestamp, nonce);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.TEXT_XML)
|
||||||
|
.body(encryptedOuter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callJarvisBridge(String fromUserName, String content, String toUserName,
|
||||||
|
String agentId, String msgId) {
|
||||||
|
if (StrUtil.isBlank(jarvisBridgeUrl) || StrUtil.isBlank(jarvisSharedSecret)) {
|
||||||
|
logger.warn("未配置 jarvis.wecom.bridge-url 或 shared-secret,跳过转发");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject body = new JSONObject();
|
||||||
|
body.put("fromUserName", fromUserName);
|
||||||
|
body.put("content", content != null ? content : "");
|
||||||
|
body.put("toUserName", toUserName);
|
||||||
|
body.put("agentId", agentId);
|
||||||
|
body.put("msgId", msgId);
|
||||||
|
try {
|
||||||
|
String resp = HttpRequest.post(jarvisBridgeUrl.trim())
|
||||||
|
.header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
.header("X-Jarvis-WeCom-Secret", jarvisSharedSecret)
|
||||||
|
.body(body.toJSONString())
|
||||||
|
.timeout(120_000)
|
||||||
|
.execute()
|
||||||
|
.body();
|
||||||
|
JSONObject root = JSON.parseObject(resp);
|
||||||
|
if (root == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (root.getIntValue("code") != HTTP_OK) {
|
||||||
|
logger.warn("Jarvis 桥接返回非200: {}", resp);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject data = root.getJSONObject("data");
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data.getString("reply");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("调用 Jarvis 企微桥接失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String readBody(HttpServletRequest request) throws IOException {
|
private static String readBody(HttpServletRequest request) throws IOException {
|
||||||
@@ -68,7 +136,6 @@ public class WeComCallbackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String parseEncrypt(String xml) {
|
private static String parseEncrypt(String xml) {
|
||||||
// 简单提取 <Encrypt><![CDATA[...]]></Encrypt>
|
|
||||||
String start = "<Encrypt><![CDATA[";
|
String start = "<Encrypt><![CDATA[";
|
||||||
String end = "]]></Encrypt>";
|
String end = "]]></Encrypt>";
|
||||||
int i = xml.indexOf(start);
|
int i = xml.indexOf(start);
|
||||||
@@ -76,7 +143,6 @@ public class WeComCallbackController {
|
|||||||
if (i >= 0 && j > i) {
|
if (i >= 0 && j > i) {
|
||||||
return xml.substring(i + start.length(), j);
|
return xml.substring(i + start.length(), j);
|
||||||
}
|
}
|
||||||
// 兼容无 CDATA 的情况
|
|
||||||
start = "<Encrypt>";
|
start = "<Encrypt>";
|
||||||
end = "</Encrypt>";
|
end = "</Encrypt>";
|
||||||
i = xml.indexOf(start);
|
i = xml.indexOf(start);
|
||||||
@@ -87,5 +153,3 @@ public class WeComCallbackController {
|
|||||||
throw new RuntimeException("Encrypt not found");
|
throw new RuntimeException("Encrypt not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package cn.van333.wxsend.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企微被动回复明文 XML(将被 {@link WeComCallbackCrypto#encryptMsg} 再包一层)
|
||||||
|
*/
|
||||||
|
public final class WeComPassiveReplyBuilder {
|
||||||
|
|
||||||
|
private WeComPassiveReplyBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String text(String toUser, String fromCorpId, String content) {
|
||||||
|
long now = System.currentTimeMillis() / 1000;
|
||||||
|
return "<xml>"
|
||||||
|
+ "<ToUserName><![CDATA[" + toUser + "]]></ToUserName>"
|
||||||
|
+ "<FromUserName><![CDATA[" + fromCorpId + "]]></FromUserName>"
|
||||||
|
+ "<CreateTime>" + now + "</CreateTime>"
|
||||||
|
+ "<MsgType><![CDATA[text]]></MsgType>"
|
||||||
|
+ "<Content><![CDATA[" + content + "]]></Content>"
|
||||||
|
+ "</xml>";
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/main/java/cn/van333/wxsend/util/WeComPlainXmlParser.java
Normal file
72
src/main/java/cn/van333/wxsend/util/WeComPlainXmlParser.java
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package cn.van333.wxsend.util;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企微解密后的明文 XML 简单解析(文本消息)
|
||||||
|
*/
|
||||||
|
public final class WeComPlainXmlParser {
|
||||||
|
|
||||||
|
private WeComPlainXmlParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String cdataOrText(String xml, String tag) {
|
||||||
|
if (xml == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Pattern p = Pattern.compile("<" + Pattern.quote(tag) + "><!\\[CDATA\\[([\\s\\S]*?)\\]\\]></" + Pattern.quote(tag) + ">");
|
||||||
|
Matcher m = p.matcher(xml);
|
||||||
|
if (m.find()) {
|
||||||
|
return m.group(1).trim();
|
||||||
|
}
|
||||||
|
p = Pattern.compile("<" + Pattern.quote(tag) + ">([^<]*)</" + Pattern.quote(tag) + ">");
|
||||||
|
m = p.matcher(xml);
|
||||||
|
if (m.find()) {
|
||||||
|
return m.group(1).trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String msgType(String plainXml) {
|
||||||
|
return cdataOrText(plainXml, "MsgType");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String content(String plainXml) {
|
||||||
|
return cdataOrText(plainXml, "Content");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String fromUserName(String plainXml) {
|
||||||
|
return cdataOrText(plainXml, "FromUserName");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toUserName(String plainXml) {
|
||||||
|
return cdataOrText(plainXml, "ToUserName");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String msgId(String plainXml) {
|
||||||
|
String v = cdataOrText(plainXml, "MsgId");
|
||||||
|
if (v != null) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
Pattern p = Pattern.compile("<MsgId>(\\d+)</MsgId>");
|
||||||
|
Matcher m = p.matcher(plainXml);
|
||||||
|
if (m.find()) {
|
||||||
|
return m.group(1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String agentId(String plainXml) {
|
||||||
|
String v = cdataOrText(plainXml, "AgentID");
|
||||||
|
if (v != null) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
Pattern p = Pattern.compile("<AgentID>(\\d+)</AgentID>");
|
||||||
|
Matcher m = p.matcher(plainXml);
|
||||||
|
if (m.find()) {
|
||||||
|
return m.group(1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,11 @@ logging:
|
|||||||
cn.van333: debug
|
cn.van333: debug
|
||||||
org.springframework: warn
|
org.springframework: warn
|
||||||
|
|
||||||
|
jarvis:
|
||||||
|
wecom:
|
||||||
|
bridge-url: https://jarvis.van333.cn/jarvis-api/jarvis/wecom/inbound
|
||||||
|
shared-secret: jarvis_wecom_bridge_change_me
|
||||||
|
|
||||||
qywx:
|
qywx:
|
||||||
webhook:
|
webhook:
|
||||||
# 默认 webhook key,可在请求体中显式传入key覆盖https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ea1737ff-f906-426d-b39c-2cdace31c3af
|
# 默认 webhook key,可在请求体中显式传入key覆盖https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ea1737ff-f906-426d-b39c-2cdace31c3af
|
||||||
|
|||||||
Reference in New Issue
Block a user