WXBizMsgCrypt 工具類。Token、EncodingAESKey、CorpId、AgentId、Secret。WXBizMsgCrypt.java 放入專案。package com.base.pf.restful;
import com.qq.weixin.mp.aes.WXBizMsgCrypt;
import net.sf.json.JSONObject;
import org.apache.commons.httpclient.*;
import org.w3c.dom.*;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.servlet.http.*;
import javax.xml.parsers.*;
import java.io.*;
import java.net.*;
import javax.net.ssl.HttpsURLConnection;
@Path("wx")
public class StationResource {
// === 企業微信後台取得的參數 ===
private final String token = "spm";
private final String encodingAesKey = "nT6ZWTVFlyNXOhFOGGOZWdJpAgeFSV8ln5CNeYw7mwl";
private final String corpId = "wxe49318eb604cf00b";
private final String agentId = "1";
private final String secret = "M-YFKmgl_kXBVEtginZH3RQWbz4xb6MFeQXXLk77mkpxZenFDYq-UgerxdUF8rel";
@Context HttpServletRequest request;
@Context HttpServletResponse response;
private final WXBizMsgCrypt wxcpt;
public StationResource() {
try { wxcpt = new WXBizMsgCrypt(token, encodingAesKey, corpId); }
catch (Exception e) { throw new RuntimeException(e); }
}
// ================== 1. 驗證回調 URL (GET) ==================
@GET @Path("station")
public String verify() {
String msgSig = request.getParameter("msg_signature");
String ts = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
try {
return wxcpt.VerifyURL(msgSig, ts, nonce, echostr); // 回傳解密後的明文
} catch (Exception e) {
e.printStackTrace();
return "verify failed";
}
}
// ================== 2. 接收訊息 (POST) ==================
@POST @Path("station")
public String receiveMsg(String reqData) {
String msgSig = request.getParameter("msg_signature");
String ts = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
try {
String xml = wxcpt.DecryptMsg(msgSig, ts, nonce, reqData); // 解密得明文 XML
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new InputSource(new StringReader(xml)));
Element root = doc.getDocumentElement();
String content = getNodeText(root, "Content");
System.out.println("收到訊息:" + content);
// TODO: 業務邏輯處理
} catch (Exception e) { e.printStackTrace(); }
return "ok"; // 必須回傳 "ok" 確認收到
}
private String getNodeText(Element root, String tag) {
NodeList nl = root.getElementsByTagName(tag);
return nl.getLength() > 0 ? nl.item(0).getTextContent() : null;
}
// ================== 3. 發送訊息 (主動推送) ==================
private static final String TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken";
private static final String SEND_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=";
/** 發送任意類型訊息 */
public void sendWeChatMsg(String msgType, String toUser, String toParty, String toTag,
String content, String mediaId, String title, String description,
String url, String picUrl, String safe) {
String accessToken = getAccessToken();
String json = buildJson(msgType, toUser, toParty, toTag, content, mediaId, title, description, url, picUrl, safe);
postHttps(SEND_URL + accessToken, json);
}
private String getAccessToken() {
try {
HttpClient client = new HttpClient();
PostMethod post = new PostMethod(TOKEN_URL);
post.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
post.setRequestBody(new NameValuePair[]{
new NameValuePair("corpid", corpId),
new NameValuePair("corpsecret", secret)
});
client.executeMethod(post);
JSONObject jo = JSONObject.fromObject(post.getResponseBodyAsString());
return jo.getString("access_token");
} catch (Exception e) { throw new RuntimeException(e); }
}
private String buildJson(String msgType, String toUser, String toParty, String toTag,
String content, String mediaId, String title, String description,
String url, String picUrl, String safe) {
StringBuilder sb = new StringBuilder("{");
sb.append("\"touser\":\"").append(toUser).append("\",");
sb.append("\"toparty\":\"").append(toParty).append("\",");
sb.append("\"totag\":\"").append(toTag).append("\",");
sb.append("\"msgtype\":\"").append(msgType).append("\",");
sb.append("\"").append(msgType).append("\":{");
switch (msgType) {
case "text": sb.append("\"content\":\"").append(content).append("\""); break;
case "image": case "voice": case "file":
sb.append("\"media_id\":\"").append(mediaId).append("\""); break;
case "video":
sb.append("\"media_id\":\"").append(mediaId).append("\",")
.append("\"title\":\"").append(title).append("\",")
.append("\"description\":\"").append(description).append("\""); break;
case "news":
sb.append("\"articles\":[{\"title\":\"").append(title).append("\",")
.append("\"description\":\"").append(description).append("\",")
.append("\"url\":\"").append(url).append("\",")
.append("\"picurl\":\"").append(picUrl).append("\"}]"); break;
}
sb.append("},");
sb.append("\"safe\":\"").append(safe).append("\",");
sb.append("\"agentid\":\"").append(agentId).append("\",");
sb.append("\"debug\":\"1\"}");
return sb.toString();
}
private void postHttps(String urlStr, String json) {
try {
URL url = new URL(urlStr);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
try (OutputStream os = conn.getOutputStream()) {
os.write(json.getBytes("UTF-8"));
}
try (InputStream is = conn.getInputStream()) {
byte[] buf = new byte[is.available()];
is.read(buf);
System.out.println("發送結果:" + new String(buf, "UTF-8"));
}
} catch (Exception e) { e.printStackTrace(); }
}
// ================== 測試主程式 ==================
public static void main(String[] args) {
StationResource wx = new StationResource();
// 發送圖文訊息給全體成員
wx.sendWeChatMsg("news", "@all", "", "", "測試標題", "", "測試描述", "真的是測試",
"https://www.baidu.com", "https://example.com/image.jpg", "0");
}
}
<servlet>
<servlet-name>JAX-RS REST Servlet</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>com.base.pf.restful</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>JAX-RS REST Servlet</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
最終回調 URL 為:https://your-domain.com/rest/wx/station
| 項目 | 說明 |
|---|---|
| HTTPS 憑證 | 企業微信**強制要求 HTTPS**,且憑證需受信任(Let's Encrypt 即可),自簽憑證會驗證失敗。 |
| 回應超時 | 接收訊息必須在 **5 秒內** 回傳 ok 或解密後明文,否則會重試。 |
| 加解密庫 | 官方 WXBizMsgCrypt 依賴 bcprov-jdk15on,需加入 Maven 依賴。 |
| Access Token 快取 | Token 有效期 7200 秒,建議用 Redis 快取並提前刷新,避免頻繁呼叫 /gettoken 觸發頻率限制。 |
| 被動回覆 vs 主動發送 | 被動回覆(5 秒內)直接回傳加密 XML;主動發送需先拿 Token 再呼叫 /message/send。 |
本文提供基於 Jersey + 官方加解密庫的企業微信完整整合範例,涵蓋 **URL 驗證、訊息接收解密、主動發送多種訊息類型**。實務上建議將 WXBizMsgCrypt 封裝為 Spring Bean、Token 交由排程器快取、訊息處理採非同步佇列,提升穩定性與擴展性。