微信服务号常见三大配置区别
配置项 用途 作用对象 是否必须公网 是否限制80/443
API IP白名单 后端调用微信API 服务器IP 是 否
JS接口安全域名 微信网页JS-SDK 前端网页域名 是 域名即可
消息推送URL 微信事件/消息回调 后端接口地址 是 必须80/443
开发时候发送模板消息修改 设置 ip白名单
监听消息: 1。启用消息推送
配置:
URL
http://XXXX/路径/wx-event
Token
XXXX --代码中要用
EncodingAESKey
XXXH--代码中要用
消息加密方式
安全模式
package cn.hgsl.pp.hgslwxservice.controller;
import cn.hgsl.pp.hgslwxservice.configuration.WechatParamConfig;
import cn.hgsl.pp.hgslwxservice.utils.TempDbService;
import cn.hgsl.pp.hgslwxservice.wx.WeChatCryptoUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.File;
import java.security.MessageDigest;
import java.util.Arrays;
@RestController
@RequestMapping("/wx-event")
public class WechatEventController {
static {
try {
Class<?> jceSecurity = Class.forName("javax.crypto.JceSecurity");
java.lang.reflect.Field isRestrictedField = jceSecurity.getDeclaredField("isRestricted");
isRestrictedField.setAccessible(true);
if (java.lang.reflect.Modifier.isFinal(isRestrictedField.getModifiers())) {
java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(isRestrictedField, isRestrictedField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
}
isRestrictedField.set(null, java.lang.Boolean.FALSE);
System.out.println("====== 成功通过反射解除 JDK AES-256 加密限制 ======");
} catch (Exception e) {
System.err.println("通过反射解除 AES 限制失败: " + e.getMessage());
}
}
@Resource
private TempDbService tempDbService;
@Resource
private WechatParamConfig wechatParamConfig;
@GetMapping
public String checkWxServer(String signature, String timestamp, String nonce, String echostr) {
String[] array = new String[]{wechatParamConfig.getMsgToken(), timestamp, nonce};
Arrays.sort(array);
String concatStr = String.join("", array);
String sha1Hex = sha1Encode(concatStr);
if (sha1Hex.equals(signature)) {
return echostr;
}
return "校验失败";
}
/**
* 接收微信推送消息
*/
@PostMapping
public String listenUserMessage(
@RequestBody String xmlData,
@RequestParam(value = "msg_signature", required = false) String msgSignature,
@RequestParam(value = "timestamp", required = false) String timestamp,
@RequestParam(value = "nonce", required = false) String nonce,
@RequestParam(value = "encrypt_type", required = false) String encryptType) {
System.out.println("========= 接收微信原始数据 =========");
System.out.println(xmlData);
boolean isAesMode = "aes".equals(encryptType);
String plainXml = xmlData;
if (isAesMode) {
try {
plainXml = WeChatCryptoUtil.decryptMsg(
wechatParamConfig.getMsgToken(),
wechatParamConfig.getEncodingAesKey(),
wechatParamConfig.getAppId(),
msgSignature, timestamp, nonce, xmlData
);
System.out.println("========= 解密后的明文 XML =========");
System.out.println(plainXml);
} catch (Exception e) {
System.err.println("微信消息解密失败: " + e.getMessage());
return "fail";
}
}
String openid = getXmlValue(plainXml, "FromUserName");
String toUser = getXmlValue(plainXml, "ToUserName");
String msgType = getXmlValue(plainXml, "MsgType");
if (!"text".equals(msgType)) {
return "success";
}
String content = getXmlValue(plainXml, "Content");
String time = DateUtil.now();
String logLine = openid + " | " + time + " | " + msgType + " | " + content + "\n";
String userDir = System.getProperty("user.dir");
FileUtil.appendUtf8String(logLine, userDir + File.separator + "wx-msg.log");
content = content.trim();
System.out.println("toUser:" + toUser);
System.out.println("openid:" + openid);
System.out.println("内容:" + content);
String replyContent;
if (content.startsWith("绑定")) {
String phone = content.replaceFirst("^绑定", "").trim();
String phoneRegex = "^1[3-9]\\d{9}$";
if (phone.matches(phoneRegex)) {
tempDbService.put(phone, openid);
replyContent = "绑定成功 ✅\n\n手机号:" + phone + "\n绑定时间:" + time;
} else {
replyContent = "手机号格式错误 ❌\n\n正确格式:绑定13800138000";
}
} else {
replyContent = "地址:XXXXX-529";
}
String replyXml = buildTextMessage(openid, toUser, replyContent);
if (isAesMode) {
try {
return WeChatCryptoUtil.encryptMsg(
wechatParamConfig.getMsgToken(),
wechatParamConfig.getEncodingAesKey(),
wechatParamConfig.getAppId(),
replyXml, timestamp, nonce
);
} catch (Exception e) {
System.err.println("微信消息加密失败: " + e.getMessage());
return "success";
}
}
return replyXml;
}
private String getXmlValue(String xml, String tag) {
String startTag = "<" + tag + "><![CDATA[";
String endTag = "]]></" + tag + ">";
int startIndex = xml.indexOf(startTag);
int endIndex = xml.indexOf(endTag);
if (startIndex == -1 || endIndex == -1) {
return "";
}
startIndex += startTag.length();
if (startIndex > endIndex) {
return "";
}
return xml.substring(startIndex, endIndex);
}
private String sha1Encode(String data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(data.getBytes());
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) sb.append("0");
sb.append(hex);
}
return sb.toString();
} catch (Exception e) {
return "";
}
}
private String buildTextMessage(String toUser, String fromUser, String content) {
return "<xml>" +
"<ToUserName><![CDATA[" + toUser + "]]></ToUserName>" +
"<FromUserName><![CDATA[" + fromUser + "]]></FromUserName>" +
"<CreateTime>" + System.currentTimeMillis() / 1000 + "</CreateTime>" +
"<MsgType><![CDATA[text]]></MsgType>" +
"<Content><![CDATA[" + content + "]]></Content>" +
"</xml>";
}
}
package cn.hgsl.pp.hgslwxservice.wx;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;
public class WeChatCryptoUtil {
/**
* 解密微信消息
*/
public static String decryptMsg(String token, String encodingAesKey, String appId,
String msgSignature, String timestamp, String nonce, String postData) throws Exception {
String encrypt = getXmlValue(postData, "Encrypt");
if (encrypt.isEmpty()) {
throw new IllegalArgumentException("密文内容不存在");
}
String[] array = new String[]{token, timestamp, nonce, encrypt};
Arrays.sort(array);
String sha1Hex = sha1Encode(String.join("", array));
if (!sha1Hex.equals(msgSignature)) {
throw new SecurityException("微信签名验证失败");
}
byte[] aesKey = Base64.getDecoder().decode(encodingAesKey + "=");
byte[] iv = Arrays.copyOfRange(aesKey, 0, 16);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] encryptedBytes = Base64.getDecoder().decode(encrypt);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
byte[] bytes = decodePkcs7(decryptedBytes);
byte[] networkOrderUuidBytes = Arrays.copyOfRange(bytes, 16, 20);
int xmlLength = recoverNetworkBytesOrderToInt(networkOrderUuidBytes);
String xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), StandardCharsets.UTF_8);
String fromAppId = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), StandardCharsets.UTF_8);
if (!fromAppId.equals(appId)) {
throw new SecurityException("AppId 校验不匹配,可能遭受非法请求");
}
return xmlContent;
}
/**
* 加密回复消息并组装成回复 XML
*/
public static String encryptMsg(String token, String encodingAesKey, String appId,
String replyXml, String timestamp, String nonce) throws Exception {
byte[] aesKey = Base64.getDecoder().decode(encodingAesKey + "=");
byte[] iv = Arrays.copyOfRange(aesKey, 0, 16);
byte[] randomBytes = new byte[16];
ThreadLocalRandom.current().nextBytes(randomBytes);
byte[] xmlBytes = replyXml.getBytes(StandardCharsets.UTF_8);
byte[] networkOrderUuidBytes = getNetworkBytesOrder(xmlBytes.length);
byte[] appIdBytes = appId.getBytes(StandardCharsets.UTF_8);
int totalLength = randomBytes.length + networkOrderUuidBytes.length + xmlBytes.length + appIdBytes.length;
byte[] plainBytes = new byte[totalLength];
System.arraycopy(randomBytes, 0, plainBytes, 0, 16);
System.arraycopy(networkOrderUuidBytes, 0, plainBytes, 16, 4);
System.arraycopy(xmlBytes, 0, plainBytes, 20, xmlBytes.length);
System.arraycopy(appIdBytes, 0, plainBytes, 20 + xmlBytes.length, appIdBytes.length);
byte[] paddedBytes = encodePkcs7(plainBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encryptedBytes = cipher.doFinal(paddedBytes);
String encryptBase64 = Base64.getEncoder().encodeToString(encryptedBytes);
String[] array = new String[]{token, timestamp, nonce, encryptBase64};
Arrays.sort(array);
String sha1Hex = sha1Encode(String.join("", array));
return "<xml>" +
"<Encrypt><![CDATA[" + encryptBase64 + "]]></Encrypt>" +
"<MsgSignature><![CDATA[" + sha1Hex + "]]></MsgSignature>" +
"<TimeStamp>" + timestamp + "</TimeStamp>" +
"<Nonce><![CDATA[" + nonce + "]]></Nonce>" +
"</xml>";
}
private static byte[] decodePkcs7(byte[] decrypted) {
int pad = decrypted[decrypted.length - 1];
if (pad < 1 || pad > 32) pad = 0;
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
private static byte[] encodePkcs7(byte[] count) {
int blockSize = 32;
int amountToPad = blockSize - (count.length % blockSize);
if (amountToPad == 0) amountToPad = blockSize;
byte padChr = (byte) (amountToPad & 0xFF);
byte[] padding = new byte[amountToPad];
Arrays.fill(padding, padChr);
byte[] updated = new byte[count.length + amountToPad];
System.arraycopy(count, 0, updated, 0, count.length);
System.arraycopy(padding, 0, updated, count.length, amountToPad);
return updated;
}
private static int recoverNetworkBytesOrderToInt(byte[] orderBytes) {
int sourceNumber = 0;
for (int i = 0; i < 4; i++) {
sourceNumber <<= 8;
sourceNumber |= (orderBytes[i] & 0xff);
}
return sourceNumber;
}
private static byte[] getNetworkBytesOrder(int sourceNumber) {
byte[] orderBytes = new byte[4];
orderBytes[3] = (byte) (sourceNumber & 0xFF);
orderBytes[2] = (byte) ((sourceNumber >> 8) & 0xFF);
orderBytes[1] = (byte) ((sourceNumber >> 16) & 0xFF);
orderBytes[0] = (byte) ((sourceNumber >> 24) & 0xFF);
return orderBytes;
}
private static String getXmlValue(String xml, String tag) {
String startTag = "<" + tag + "><![CDATA[";
String endTag = "]]></" + tag + ">";
int startIndex = xml.indexOf(startTag);
int endIndex = xml.indexOf(endTag);
if (startIndex == -1 || endIndex == -1) return "";
return xml.substring(startIndex + startTag.length(), endIndex);
}
private static String sha1Encode(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) sb.append("0");
sb.append(hex);
}
return sb.toString();
}
}