You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java调用Azure Blob Storage REST API报403签名认证错误

Azure Blob Storage SharedKey签名调用403排查方案

问题现象

  • 使用访问密钥通过REST API拉取Azure Blob Storage数据,Postman集合内置的签名生成脚本可正常生成合规签名字符串,签名验证通过后可正常调用Blob API
  • 相同逻辑迁移到Java实现后调用持续失败,返回错误:

    403 Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.: [

    return (<Code>AuthenticationFailed</Code>)
    Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly inclu... (435 bytes)]

  • 肉眼对比两边打印的待签名字符串,内容几乎一致:
    postman= "GET\n\n\n\n\napplication/json\n\n\n\n\n\n\nx-ms-date:Tue, 05 Jul 2022 11:45:00 GMT\nx-ms-version:2020-04-08\n/osmosconnectortest/blobcontainer\ncomp:list\nrestype:container"
    code = "GET\n\n\n\n\napplication/json\n\n\n\n\n\n\nx-ms-date:Tue, 05 Jul 2022 11:44:56 GMT\nx-ms-version:2020-04-08\n/osmosconnectortest/blobcontainer\ncomp:list\nrestype:container"
    
  • 实际请求携带头信息如下:
    requestHeaders.put("x-ms-date", "Tue, 05 Jul 2022 11:45:00 GMT");
    requestHeaders.put("x-ms-version", "2020-04-08");
    requestHeaders.put("Content-Type", "application/json");
    requestHeaders.put("Authorization", "SharedKey accountName:yvfdsfdssdfF0lGGtEWGU+b7BqFY0UHMkvI=");
    

参考正常逻辑:Postman签名生成脚本

// Should be UTC GMT string
pm.environment.set("utcStr", new Date().toUTCString());

// Get hash of all header-name:value
const headers = pm.request.getHeaders({ ignoreCase: true, enabled: true });

// Construct Signature value for Authorization header
var signatureParts = [
  pm.request.method.toUpperCase(),
  headers["content-encoding"] || "",
  headers["content-language"] || "",
  headers["content-length"]  || "",
  headers["content-md5"] || "",
  headers["content-type"] || "",
  headers["x-ms-date"] ? "" : (pm.environment.get("utcStr") || ""),
  headers["if-modified-since"] || "",
  headers["if-match"] || "",
  headers["if-none-match"] || "",
  headers["if-unmodified-since"] || "",
  headers["range"] || ""
];

// Construct CanonicalizedHeaders
const canonicalHeaderNames = [];
Object.keys(headers).forEach(key => {
  if (key.startsWith("x-ms-")) {
      canonicalHeaderNames.push(key);
  }
});
// Sort headers lexographically by name
canonicalHeaderNames.sort();

const canonicalHeaderParts = [];
canonicalHeaderNames.forEach(key => {
  let value = pm.request.getHeaders({ ignoreCase: true, enabled: true })[key];

  // Populate variables
  value = pm.environment.replaceIn(value);

  // Replace whitespace in value but not if its within quotes
  if (!value.startsWith("\"")) {
    value = value.replace(/\s+/, " ");
  }

  canonicalHeaderParts.push(`${key}:${value}`);
});

// Add headers to signature
signatureParts.push.apply(signatureParts, canonicalHeaderParts);

// Construct CanonicalizedResource
const canonicalResourceParts = [
  `/${pm.environment.get("storeAcc")}${pm.request.url.getPath()}`
];
const canonicalQueryNames = [];
pm.request.url.query.each(query => {
  canonicalQueryNames.push(query.key.toLowerCase());
});
canonicalQueryNames.sort();
canonicalQueryNames.forEach(queryName => {
  const value = pm.request.url.query.get(queryName);
  canonicalResourceParts.push(`${queryName}:${value}`);
});
// Add resource to signature
signatureParts.push.apply(signatureParts, canonicalResourceParts);

console.log("Signature Parts", signatureParts);

// Now, construct signature raw string
const signatureRaw = signatureParts.join("\n");

console.log("Signature String", JSON.stringify(signatureRaw));

// Hash it using HMAC-SHA256 and then encode using base64
const storageKey = pm.variables.get("accountKey");
const signatureBytes = CryptoJS.HmacSHA256(signatureRaw, CryptoJS.enc.Base64.parse(storageKey));
console.log("signatureBytes",signatureBytes);
const signatureEncoded = signatureBytes.toString(CryptoJS.enc.Base64);

console.log("signatureEncoded",signatureEncoded);
console.log("Storage Account", pm.environment.get("storeAcc"));
console.log("Storage Key", storageKey);

// Finally, make it available for headers
pm.environment.set("sigStr",
   `SharedKey ${pm.environment.get("storeAcc")}:${signatureEncoded}`); 

原有Java实现代码

public String generateOauthHeader(String method, UserDetails userFields, String baseUrl, String azureFileListUrl)  {

    String key = userFields.getAccountKey();

    DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ");
    dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
    String startDate = dateFormat.format(new Date());
    
    String stringToSign = method + "\\n\\n\\n\\n\\napplication/json\\n\\n\\n\\n\\n\\n\\nx-ms-date:" + "Tue, 05 Jul 2022 11:45:00 "
            + "GMT\\nx-ms-version:2020-04-08\\n/" + userFields.getAccountName() + "/"
            + userFields.getContainerName();

    if (null != azureFileListUrl) {
        azureFileListUrl =azureFileListUrl.replace("?", "");
        azureFileListUrl =azureFileListUrl.replace("=", ":");
        String[] paramlist = azureFileListUrl.split("&");

        stringToSign = stringToSign + "\\n" + paramlist[1] + "\\n" + paramlist[0];
    }
    stringToSign="\""+stringToSign+"\"";
    System.err.println("Signature string  =  " + stringToSign);
    String signature = getHMAC256(key, stringToSign);

    signature = "SharedKey " + userFields.getAccountName() + ":" + signature;
    System.err.println("Final Signature string  =  " + signature);
    return signature;
}

private static String getHMAC256(String accountKey, String signStr) {
    String signature = null;
    try {
        SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(accountKey), "HmacSHA256");
        Mac sha256HMAC = Mac.getInstance("HmacSHA256");
        sha256HMAC.init(secretKey);
        signature = Base64.getEncoder().encodeToString(sha256HMAC.doFinal(signStr.getBytes(StandardCharsets.UTF_8)));
    } catch (Exception e) {
        e.printStackTrace();
    }
    return signature;
}

根因定位

  • 核心错误1:换行符用错:Java代码中拼接待签名字符串时使用"\\n",这是转义后的反斜杠+字母n两个普通字符,不是ASCII码为0x0A的真实换行符。Postman脚本中用的是真实换行符拼接,这直接导致待签名串的字节内容完全不同,HMAC计算结果必然不匹配。打印时因为转义字符显示效果和真实换行一致,肉眼无法分辨差异。
  • 核心错误2:多余的引号包裹:代码中给拼接完成的待签名字符串前后手动加了双引号,Postman生成的待签名串没有这层包裹,额外增加的两个字符直接改变了签名原文内容。
  • 逻辑错误1:查询参数未排序:代码处理查询参数时直接按split后的固定下标拼接,没有按Azure规范要求将查询参数名转小写后做字典序升序排列,参数顺序不对会直接导致签名校验失败。
  • 逻辑错误2:日期值不一致:代码中定义了startDate变量存储生成的GMT时间,但实际拼接待签名串时硬编码了固定的x-ms-date值,很容易出现签名用时间和请求头实际携带的x-ms-date值不一致的问题;同时SimpleDateFormat格式末尾多了冗余空格,手动拼接GMT时容易产生多余空白字符。
  • 逻辑错误3:待签名字段计数不准:手动拼接空字段时容易数错空行数量,Azure要求签名串的12个标准头字段必须按固定顺序排列,缺项填空字符串,多一个少一个空行都会导致签名不匹配。

修复方案

  1. 所有待签名字符串拼接处,将"\\n"替换为真实换行符"\n"(Java字符串中单个反斜杠加n即为真实换行符)
  2. 删除给待签名字符串包裹双引号的冗余代码
  3. 日期格式使用标准RFC1123格式EEE, dd MMM yyyy HH:mm:ss 'GMT',强制设置GMT时区,生成的日期值同时用于签名拼接和请求头x-ms-date字段,保证两边值完全一致
  4. 规范化处理x-ms开头的请求头:统一转小写后按字典序排序,替换值中的多余空白为单个空格后再拼接
  5. 规范化处理查询参数:统一转小写后按字典序排序,再按参数名:参数值格式拼接到资源串末尾
  6. 不要硬编码空字段,按固定顺序组装签名部分数组,最后统一用换行符拼接,避免空行计数错误

修复后核心代码示例

private static final DateTimeFormatter RFC1123_FORMATTER = DateTimeFormatter
        .ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'")
        .withLocale(Locale.US)
        .withZone(ZoneId.of("GMT"));

public String generateAuthHeader(String method, Map<String, String> headers, String accountName, String accountKey, String path, Map<String, String> queryParams) throws Exception {
    // 1. 生成标准GMT时间
    String xMsDate = RFC1123_FORMATTER.format(Instant.now());
    headers.put("x-ms-date", xMsDate);

    // 2. 按顺序组装标准签名字段
    List<String> signatureParts = new ArrayList<>();
    signatureParts.add(method.toUpperCase());
    signatureParts.add(headers.getOrDefault("Content-Encoding", ""));
    signatureParts.add(headers.getOrDefault("Content-Language", ""));
    signatureParts.add(headers.getOrDefault("Content-Length", ""));
    signatureParts.add(headers.getOrDefault("Content-MD5", ""));
    signatureParts.add(headers.getOrDefault("Content-Type", ""));
    signatureParts.add(headers.containsKey("x-ms-date") ? "" : xMsDate);
    signatureParts.add(headers.getOrDefault("If-Modified-Since", ""));
    signatureParts.add(headers.getOrDefault("If-Match", ""));
    signatureParts.add(headers.getOrDefault("If-None-Match", ""));
    signatureParts.add(headers.getOrDefault("If-Unmodified-Since", ""));
    signatureParts.add(headers.getOrDefault("Range", ""));

    // 3. 组装规范化x-ms头
    List<String> xMsHeaders = headers.keySet().stream()
            .filter(k -> k.toLowerCase().startsWith("x-ms-"))
            .sorted(Comparator.comparing(String::toLowerCase))
            .toList();
    for (String headerName : xMsHeaders) {
        String value = headers.get(headerName).replaceAll("\\s+", " ").trim();
        signatureParts.add(headerName.toLowerCase() + ":" + value);
    }

    // 4. 组装规范化资源串
    StringBuilder resourcePart = new StringBuilder("/").append(accountName).append(path);
    if (queryParams != null && !queryParams.isEmpty()) {
        List<String> sortedQueryKeys = queryParams.keySet().stream()
                .map(String::toLowerCase)
                .sorted()
                .toList();
        for (String key : sortedQueryKeys) {
            resourcePart.append("\n").append(key).append(":").append(queryParams.get(key));
        }
    }
    signatureParts.add(resourcePart.toString());

    // 5. 拼接真实换行的待签名串,不要加任何额外字符
    String stringToSign = String.join("\n", signatureParts);

    // 6. HMAC-SHA256计算签名
    SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(accountKey), "HmacSHA256");
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(secretKey);
    String signature = Base64.getEncoder().encodeToString(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));

    return "SharedKey " + accountName + ":" + signature;
}

验证注意点

  • 签名计算用的x-ms-date值必须和请求头中实际携带的x-ms-date完全一致,时间和Azure服务器误差不能超过15分钟
  • 无请求体时Content-Length字段填空字符串,不要填0
  • 所有头名称、查询参数名称的大小写处理、排序规则必须严格和Azure规范对齐
  • 打印待签名字符串时建议直接输出字节数组,避免转义字符导致的视觉误判

内容的提问来源于stack exchange,提问作者tyde hydo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 11:09:23