企业微信审批事件回调的安全验证与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校验与时间戳防重放机制,有效保障了企业微信审批回调接口的安全性,防止未授权调用与数据篡改,满足生产环境安全要求。

相关推荐
moonsims27 分钟前
NavCore惯性测量导航-轻量级安全惯导 / UAV 安全触发 IMU 模块-异构双IMU架构-低噪声稳定感知+高动态异常检测
安全·架构
亦暖筑序27 分钟前
AI 客服系统安全加固:JWT 鉴权 + Bucket4j 三层限流
java·架构
乐迪信息29 分钟前
乐迪信息:实时预警,秒级响应:船舶AI异常行为检测算法
大数据·人工智能·算法·安全·目标跟踪
xhuiting33 分钟前
项目技术总结
java
某人辛木33 分钟前
JDK安装配置
java·开发语言
counting money35 分钟前
Spring框架基础(依赖注入-全注解形式)
java·数据库·spring
小王师傅6636 分钟前
【Java结构化梳理】泛型-初步了解-下
java·开发语言
逝水如流年轻往返染尘44 分钟前
JAVA中的String类
java
一只叫煤球的猫1 小时前
ThreadForge 1.2.0 发布:让 Java 并发代码更好写,这次补齐了高阶编排、示例与观测能力
java·设计模式·设计
counting money1 小时前
Spring框架基础(依赖注入-半注解形式)
java·后端·spring