企业微信回调验证失败,而测试入口似乎通过,先比较实际 HTTP 响应。2022 年项目中的关键差异是:原本应返回的解密字符串,被统一返回处理变成了带双引号的 JSON 字符串。

字符串值相同,响应内容可能不同

用非真实回调值说明:

原始文本:example
JSON 字符串:"example"
统一包装:{"data":"example"}

这三种响应不是相同的字节。协议要求原样返回验证结果时,额外引号、字段包装、BOM 或多余输出,都可能改变验证内容。企业微信接收消息文档入口

旧稿把测试与正式配置的差异判断为平台 bug。没有保存完整请求和版本,不能据此认定平台缺陷;本文保留的是当时“响应被二次序列化”的定位结果。

先排除统一返回包装

ControllerAdvice 只是扩展机制的入口,不会天然把所有字符串变成 JSON。需要检查项目中的 ResponseBodyAdvice、转换器、过滤器和网关究竟做了什么。

协议回调应从业务 JSON 包装中排除,保留自己的响应契约。仅把返回类型改成 String,不能保证已有包装逻辑不再介入。

下面是 Spring 5.x / Servlet 的纯文本传输演示,不是企业微信验签解密实现:

import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class RawTextDemo {
    @GetMapping("/raw-text-demo")
    public void rawText(HttpServletResponse response) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("example");
    }
}

这里使用普通 Controller 并直接处理响应,展示“原始文本”与 JSON 序列化的区别。真实回调只有通过签名验证并正确解密后,才能输出协议规定的结果;不要把示例固定字符串当作回调答案。

仍需返回对象时,检查转换器顺序

若 Fastjson 转换器排在 StringHttpMessageConverter 前,且两者都能处理当前响应,字符串可能被按 JSON 处理。以下为 Spring 5 的增量配置示例:

import java.nio.charset.StandardCharsets;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class TextConverterConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.removeIf(c -> c instanceof StringHttpMessageConverter);
        converters.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
    }
}

这会影响应用中的字符串响应,需要同时检查正常业务接口;它也不会自动撤销 Advice 已经做过的包装。不要为修一个接口清空全部转换器,或无意加上 @EnableWebMvc 接管整套 Boot MVC 配置。Spring 消息转换器

检查时比较响应状态、Content-Type 与原始字节,而不是只看控制器调试器中的 Java 字符串。相关的序列化选择问题见JSONField 与 Web MVC。