微信服务号模板消息发送和自动回复

微信服务号常见三大配置区别

        配置项         用途                  作用对象        是否必须公网          是否限制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();
    }
}
暂无评论

发送评论 编辑评论


				
上一篇