Azure Monitor数据收集器API签名一致却返回InvalidAuthorization问题
我将Azure Monitor数据收集器API文档中的Python签名示例转换为Rust代码后,Rust程序生成的SharedKey <workspace_id>:<signature>调用API时被返回403状态码,响应体内容为:
{"Error":"InvalidAuthorization","Message":"An invalid signature was specified in the Authorization header"}
但如果把Rust程序运行时生成的RFC1123格式日期硬编码到Python示例脚本中,生成的Authorization头与Rust程序完全一致,且能成功调用API发送日志。既然两者签名完全一致,说明问题并非出在签名本身,想了解还有哪些原因会导致该API返回此类错误。
补充:可正常运行的Python示例
import requests import datetime import hashlib import hmac import base64 WORKSPACE_ID = "<my workspace ID>" SHARED_KEY = "<my primary key>" def build_signature(message, secret): key_bytes = base64.b64decode(secret) message_bytes = bytes(message, encoding="utf-8") hmacsha256 = hmac.new(key_bytes, message_bytes, digestmod=hashlib.sha256).digest() encoded_hash = base64.b64encode(hmacsha256).decode() return encoded_hash def post_data(): data = '{"hello":"world"}' date_string = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') # date_string = "Thu, 14 Sep 2023 01:27:12 GMT" content_length = len(data) string_to_hash = f"POST\n{content_length}\napplication/json\nx-ms-date:{date_string}\n/api/logs" hashed_string = build_signature(string_to_hash, SHARED_KEY) signature = f"SharedKey {WORKSPACE_ID}:{hashed_string}" print(signature) query = "api-version=2016-04-01"; url = f"https://{WORKSPACE_ID}.ods.opinsights.azure.com/api/logs?{query}" headers = { 'content-type': "application/json", 'Authorization': signature, 'Log-Type': "my-event-type", 'x-ms-date': date_string } response = requests.post(url, data=data, headers=headers) if (response.status_code >= 200 and response.status_code <= 299): print('Accepted') else: print("Response: {}".format(response.content)) print("Response code: {}".format(response.status_code)) if __name__ == "__main__": post_data()
生成相同签名但返回错误的Rust代码
use base64; use chrono::Utc; use ring::hmac; use reqwest; use std::error::Error; #[derive(Clone, Debug)] enum MyError { MessageSendError(String), } impl std::fmt::Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MyError::MessageSendError(msg) => write!(f, "{}", msg), } } } impl Error for MyError {} struct SentinelClient { azure_collector_url: String, workspace_id: String, shared_key: String, http_client: reqwest::Client, } impl SentinelClient { fn new(workspace_id: String, shared_key: String) -> Self { let query = "api-version=2016-04-01"; let azure_collector_url = format!("https://{workspace_id}.ods.opinsights.azure.com/api/logs?{query}"); Self { azure_collector_url, workspace_id, shared_key, http_client: reqwest::Client::new(), } } async fn send(&self, data: String) -> Result<(), MyError> { let workspace_id = &self.workspace_id; let date_string = format!("{}", Utc::now().format("%a, %d %b %Y %H:%M:%S GMT")); println!("date string {}", date_string); let content_length = data.to_string().len(); let string_to_hash = format!("POST\n{content_length}\napplication/json\nx-ms-date:{date_string}\n/api/logs"); let hashed_string = Self::build_signature(&string_to_hash, &self.shared_key)?; let signature = format!("SharedKey {workspace_id}:{hashed_string}"); println!("{}", signature); let url = &self.azure_collector_url; let request = self .http_client .post(url) .json(&data) .header("Authorization", signature) .header("Log-Type", "my-event-type") .header("x-ms-date", date_string); match request.send().await { Ok(resp) => { if resp.status().is_success() { println!("request successful: {:?}", &resp); Ok(()) } else { let body = resp .text() .await .map_err(|e| MyError::MessageSendError(e.to_string()))?; println!("request unsuccessful: {body}"); Err(MyError::MessageSendError(body)) } } Err(e) => Err(MyError::MessageSendError(e.to_string())), } } /// Build the API signature for the Azure Monitor HTTP Data Collector /// authorization header. fn build_signature(message: &str, secret: &str) -> Result<String, MyError> { let key_bytes = base64::decode(secret).map_err(|e| { MyError::MessageSendError(format!("could not decode shared key: {e}")) })?; let message_bytes = message.as_bytes(); let hmacsha256 = hmac::Key::new(hmac::HMAC_SHA256, &key_bytes); let encoded_hash = base64::encode(hmac::sign(&hmacsha256, message_bytes).as_ref()); Ok(encoded_hash) } }
(注:修复了原Rust代码中缺失的async关键字、错误枚举的Display和Error实现)
可能的原因及修复方案
1. 请求体序列化方式导致Content-Length不匹配
Python代码中使用data=data直接发送JSON字符串,Content-Length是原字符串的实际长度;而Rust代码中使用.json(&data),会将传入的String类型序列化为转义后的JSON字符串(比如原数据{"hello":"world"}会被转成"{\"hello\":\"world\"}"),导致实际发送的Content-Length远大于签名计算时的长度。
Azure的签名验证会严格检查签名中的Content-Length与实际请求的Content-Length是否一致,不一致就会返回授权错误。
修复方案:
将Rust中的.json(&data)改为.body(data),并手动添加content-type请求头:
let request = self .http_client .post(url) .body(data) .header("content-type", "application/json") .header("Authorization", signature) .header("Log-Type", "my-event-type") .header("x-ms-date", date_string);
2. 请求头的自动处理差异
reqwest可能会自动添加一些额外请求头(如Accept、Accept-Encoding),但这通常不会影响授权验证。不过需要确保:
x-ms-date头的大小写完全正确(必须是小写的x-ms-date)- Authorization头没有多余的空格或换行符
3. 系统时间偏差
虽然你用了UTC时间,但如果Rust程序运行的系统时间与真实UTC时间偏差超过5分钟,Azure的授权验证也会失败。不过你将Rust生成的日期硬编码到Python中能成功,说明该日期是有效的,这个可能性较低,但仍可确认系统时间是否同步。
内容的提问来源于stack exchange,提问作者user4601931

