使用Golang+GAE配置MS Teams出站Webhook时HMAC验证失败求助
MS Teams出站Webhook HMAC验证失败问题排查与修复
问题描述
使用Golang结合Google App Engine部署MS Teams出站Webhook时,HMAC验证始终不通过,预期签名与请求提供的签名不一致。
原始代码
package main import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" ) var secret string = "THIS_IS_A_SECRET" func handleWebhook(w http.ResponseWriter, r *http.Request) { fmt.Println("Webhook endpoint hit") body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Can't read request body", http.StatusBadRequest) return } fmt.Println("Received webhook request:", string(body)) // Log the request body for debugging fmt.Printf("Request Body: %s\n", string(body)) // Generate HMAC token from the request body mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expectedMAC := mac.Sum(nil) expectedMACBase64 := base64.StdEncoding.EncodeToString(expectedMAC) // Get the HMAC token from the request header authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "HMAC ") { fmt.Println("Invalid Authorization header") http.Error(w, "Invalid Authorization header", http.StatusUnauthorized) return } providedMACBase64 := strings.TrimPrefix(authHeader, "HMAC ") // Compare the generated HMAC token with the provided one if !hmac.Equal([]byte(providedMACBase64), []byte(expectedMACBase64)) { fmt.Println("Invalid HMAC token") fmt.Println("Expected HMAC token:", expectedMACBase64) fmt.Println("Provided HMAC token:", providedMACBase64) http.Error(w, "Invalid HMAC token", http.StatusUnauthorized) return } else { fmt.Println("Authenticated: Valid HMAC token") } // Create a response in the format expected by Microsoft Teams response := map[string]string{ "type": "message", "text": "Webhook received successfully", } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } func main() { http.HandleFunc("/teams-webhook", handleWebhook) http.ListenAndServe(":8080", nil) }
问题根源
根据微软Teams官方文档的HMAC验证规则,核心错误为:
- 密钥未正确解码:Teams提供的出站Webhook密钥是Base64编码格式的字符串,必须先解码为原始字节数组才能作为HMAC的密钥,代码中直接将原始字符串转为字节数组使用,导致密钥不匹配。
- (次要可能)请求体被中间件修改:若GAE或其他中间件对请求体进行自动解析、编码转换(如UTF-8转码、添加额外字符),也会导致HMAC计算结果不一致,但优先解决密钥解码问题即可覆盖绝大多数场景。
修复后的代码
package main import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" ) // Teams提供的Base64编码格式的密钥 var secret string = "THIS_IS_A_SECRET" func handleWebhook(w http.ResponseWriter, r *http.Request) { fmt.Println("Webhook endpoint hit") body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Can't read request body", http.StatusBadRequest) return } // 重置请求体,方便后续可能的二次读取(可选) r.Body = io.NopCloser(strings.NewReader(string(body))) fmt.Println("Received webhook request:", string(body)) // 1. 对Teams提供的密钥进行Base64解码 decodedSecret, err := base64.StdEncoding.DecodeString(secret) if err != nil { fmt.Println("Failed to decode secret:", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // 2. 使用解码后的密钥计算HMAC-SHA256 mac := hmac.New(sha256.New, decodedSecret) mac.Write(body) expectedMAC := mac.Sum(nil) expectedMACBase64 := base64.StdEncoding.EncodeToString(expectedMAC) // 获取请求头中的签名 authHeader := r.Header.Get("Authorization") if !strings.HasPrefix(authHeader, "HMAC ") { fmt.Println("Invalid Authorization header") http.Error(w, "Invalid Authorization header", http.StatusUnauthorized) return } providedMACBase64 := strings.TrimPrefix(authHeader, "HMAC ") // 安全对比两个Base64字符串(使用hmac.Equal避免时序攻击) if !hmac.Equal([]byte(providedMACBase64), []byte(expectedMACBase64)) { fmt.Println("Invalid HMAC token") fmt.Println("Expected HMAC token:", expectedMACBase64) fmt.Println("Provided HMAC token:", providedMACBase64) http.Error(w, "Invalid HMAC token", http.StatusUnauthorized) return } else { fmt.Println("Authenticated: Valid HMAC token") } // 返回Teams预期格式的响应 response := map[string]string{ "type": "message", "text": "Webhook received successfully", } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } func main() { http.HandleFunc("/teams-webhook", handleWebhook) http.ListenAndServe(":8080", nil) }
关键修改说明
- 新增
base64.StdEncoding.DecodeString(secret)步骤,将Teams提供的Base64编码密钥解码为原始字节数组,这是验证通过的核心前提。 - 添加请求体重置逻辑
r.Body = io.NopCloser(strings.NewReader(string(body))),避免后续需要再次读取请求体时出现错误。 - 保留
hmac.Equal的安全对比方式,避免时序攻击风险。
内容的提问来源于stack exchange,提问作者TaihouKai
相关产品推荐
相关产品推荐

