企业微信机器人 API 踩坑记录:接收消息总是提示“签名验证失败“怎么办?
签名验证失败,是企微回调接入里出现频率最高的问题,没有之一。这篇把我自己踩过的坑和排查方法整理出来,按概率从高到低排。
坑一:密钥用错了
这是最常见的原因,占签名失败的一半以上。
企微回调验签用的是签名密钥,不是 App Token,也不是 appid。三者在控制台里是不同的字段,长得也不一样:
-
App Token:
eyk_xxxx,用于 API 调用鉴权 -
签名密钥:一串随机字符串,用于回调验签
-
appid:
we_xxx,实例标识
很多人把 App Token 当成签名密钥用,算出来的签名当然对不上。去控制台确认一下,你用的是不是"回调配置"里的那个密钥。
坑二:请求体被读过一次
这是第二常见的原因,而且特别隐蔽。
很多 Web 框架在中间件里已经反序列化过 body,业务代码再读 request.body 拿到的是空字节流。空 body 算出来的签名,和收到的签名当然不一样。
Spring Boot:@RequestBody 已经读过一次,再用 request.getInputStream() 是空的。解决办法是用 ContentCachingRequestWrapper 或者自定义 filter 提前缓存。
Flask:request.get_data() 和 request.json 都会消费 body。解决办法是用 request.get_data(cache=True) 或者自己存一份。
Express:body-parser 已经解析过,再用 req.body 是对象不是原始字节。解决办法是用 express.raw() 或者自己实现 raw body 中间件。
# Flask 正确做法
@app.route("/wx-api/webhook/", methods=["POST"])
def webhook():
raw_body = request.get_data(cache=True) # 先拿原始字节
signature = request.headers.get("X-Eyun-Signature")
expected = hmac.new(
SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return "forbidden", 403
data = request.get_json() # 再解析 JSON
# ...
坑三:编码不一致
报文里有中文,验签的字节编码必须和发送方一致。
企微回调用 UTF-8 编码。如果你的服务在接收时按 GBK 或其他编码转了一次,字节流就变了,签名自然对不上。
检查点:
-
服务器默认编码是不是 UTF-8
-
框架有没有自动转码
-
中间件有没有做字符集转换
# 确保按 UTF-8 取字节
raw_body = request.get_data(cache=True)
# 不要这样做:raw_body = request.get_data().decode('gbk').encode('utf-8')
坑四:header 大小写问题
HTTP header 名是大小写不敏感的,但不同框架、不同语言的取值方式不一样。
-
X-Eyun-Signature和x-eyun-signature是同一个 header -
但有的框架里
request.headers.get("X-Eyun-Signature")能拿到,request.headers.get("x-eyun-signature")拿不到
排查时把 header 名打出来看看,确认取到的是不是预期的值。
# 打印所有 header,确认签名 header 的名字
print(dict(request.headers))
坑五:echostr 验证和消息推送的混用
配置回调地址时,平台会发一个带 echostr 的验证请求,要求你原样返回。这个请求不需要验签,直接返回 echostr 就行。
但很多人生成的验签逻辑对验证请求也跑了一遍,结果签名对不上,验证失败。
@app.route("/wx-api/webhook/", methods=["POST", "GET"])
def webhook():
# 验证请求:直接返回 echostr,不验签
echostr = request.args.get("echostr") or (
request.json.get("echostr") if request.is_json else None
)
if echostr:
return echostr
# 消息推送:验签
signature = request.headers.get("X-Eyun-Signature")
raw_body = request.get_data(cache=True)
expected = hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return "forbidden", 403
# ...
一套快速排查的顺序
-
确认用的是签名密钥,不是 App Token
-
打日志:收到的签名、算出来的签名、原始 body 的前 100 字节
-
确认 body 没有被提前读过
-
确认编码是 UTF-8
-
确认 header 名大小写
-
区分验证请求和消息推送
按这个顺序走,一般两三轮就能定位。签名验证失败不是玄学,是输入不对。把输入打出来比对,问题一目了然。
签名密钥在 Eyun 控制台的实例配置里生成,回调字段说明在Eyun 开发文档里能查到。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)