企业微信审批事件回调的安全验证与Java HMAC-SHA256校验实现

企业微信审批事件回调的安全验证与Java HMAC-SHA256校验实现

企业微信在审批状态变更时,会向配置的回调URL推送事件通知。为防止伪造请求,企业微信要求接收方必须校验请求头中的msg_signature,该签名基于tokentimestampnonce和消息体内容,使用HMAC-SHA256算法生成。若校验失败,应拒绝处理。本文基于wlkankan.cn.verify包,完整实现安全验证逻辑,并提供可复用的工具类。

企业微信签名生成规则

根据官方文档,msg_signature计算公式为:

复制代码
msg_signature = SHA1( token + timestamp + nonce + encrypted_msg )

但注意:实际使用的是 HMAC-SHA256 ,密钥为 token,数据为 timestamp + "\n" + nonce + "\n" + msg(含换行符)。这是常见误区。

签名验证工具类

java 复制代码
package wlkankan.cn.verify;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class WeComSignatureVerifier {

    public static boolean verify(String token, String timestamp, String nonce, String body, String signature) {
        if (token == null || timestamp == null || nonce == null || body == null || signature == null) {
            return false;
        }

        try {
            String expectedSignature = generateSignature(token, timestamp, nonce, body);
            return java.security.MessageDigest.isEqual(
                expectedSignature.getBytes(StandardCharsets.UTF_8),
                signature.getBytes(StandardCharsets.UTF_8)
            );
        } catch (Exception e) {
            return false;
        }
    }

    private static String generateSignature(String token, String timestamp, String nonce, String msg) 
            throws Exception {
        String data = String.join("\n", timestamp, nonce, msg);
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(token.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(secretKey);
        byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hash);
    }
}

Spring Boot控制器集成

wlkankan.cn.controller中接收回调并校验:

java 复制代码
package wlkankan.cn.controller;

import wlkankan.cn.verify.WeComSignatureVerifier;
import wlkankan.cn.service.ApprovalEventService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/wecom/callback")
public class ApprovalCallbackController {

    @Value("${wecom.callback.token}")
    private String callbackToken;

    private final ApprovalEventService eventService;

    public ApprovalCallbackController(ApprovalEventService eventService) {
        this.eventService = eventService;
    }

    @PostMapping("/approval")
    public String handleApprovalEvent(
            @RequestHeader("msg_signature") String msgSignature,
            @RequestParam("timestamp") String timestamp,
            @RequestParam("nonce") String nonce,
            @RequestBody String requestBody) {

        if (!WeComSignatureVerifier.verify(callbackToken, timestamp, nonce, requestBody, msgSignature)) {
            // 签名不合法,返回空或错误码(企业微信要求非200即重试)
            return ""; // 企业微信文档建议非法请求直接返回空
        }

        // 解析XML或JSON(企业微信审批事件为XML)
        ApprovalEvent event = parseApprovalEvent(requestBody);
        eventService.process(event);
        return "success"; // 必须返回"success"字符串
    }

    private ApprovalEvent parseApprovalEvent(String xml) {
        // 使用Dom4j或Jackson XML解析
        return XmlParser.parse(xml, ApprovalEvent.class);
    }
}

ApprovalEvent模型示例

java 复制代码
package wlkankan.cn.model;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "xml")
public class ApprovalEvent {
    private String ToUserName;
    private String FromUserName;
    private Long CreateTime;
    private String MsgType;
    private String Event;
    private String SpNo; // 审批单号
    private String SpStatus; // APPROVED/REJECTED

    // getters/setters
    public String getSpNo() { return SpNo; }
    public String getSpStatus() { return SpStatus; }
}

安全增强:防重放攻击

即使签名正确,攻击者仍可能重放旧请求。需校验timestamp是否在合理窗口内(如5分钟):

java 复制代码
package wlkankan.cn.verify;

public class TimestampValidator {
    private static final long MAX_TIME_DIFF_MS = 5 * 60 * 1000; // 5分钟

    public static boolean isValidTimestamp(String timestampStr) {
        try {
            long timestamp = Long.parseLong(timestampStr) * 1000L; // 企业微信时间戳为秒
            long now = System.currentTimeMillis();
            return Math.abs(now - timestamp) <= MAX_TIME_DIFF_MS;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}

在控制器中增加校验:

java 复制代码
// 在 handleApprovalEvent 方法中
if (!TimestampValidator.isValidTimestamp(timestamp)) {
    return "";
}

配置与部署

application.yml中配置token:

yaml 复制代码
wecom:
  callback:
    token: "your_secure_token_here"

确保该token与企业微信管理后台【应用管理】->【接收消息】中配置的Token完全一致。

通过wlkankan.cn.verify模块实现的HMAC-SHA256校验与时间戳防重放机制,有效保障了企业微信审批回调接口的安全性,防止未授权调用与数据篡改,满足生产环境安全要求。

相关推荐
程序猿大帅5 小时前
别再只当调包侠了:用 Spring AI 落地 Function Calling,我被大模型硬生生砸出了三个大坑
java
程序员晓琪6 小时前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
Flittly6 小时前
【AgentScope Java新手村系列】(11)中断与恢复
java·spring boot·spring
众少成多积小致巨7 小时前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
东坡白菜7 小时前
破局全栈:前端开发的Java入门实战记录—JPA(2)
java·后端
SimonKing13 小时前
艹,维护AI写的代码,我心态崩了......
java·后端·程序员
用户2986985301413 小时前
Java Word 文档样式进阶:段落与文本背景色设置完全指南
java·后端
小bo波1 天前
从"任意文件复制"深挖Java I/O:字符流与字节流的本质抉择
java·nio·io流·后端开发·文件复制
nanxun8862 天前
记一次诡异的 Docker 容器"串包"故障排查
java
用户1563068103512 天前
Day01 | Java 基础(Java SE)
java