Java调用Azure Blob Storage REST API报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.: [
Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly inclu... (435 bytes)]return (<Code>AuthenticationFailed</Code>) - 肉眼对比两边打印的待签名字符串,内容几乎一致:
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个标准头字段必须按固定顺序排列,缺项填空字符串,多一个少一个空行都会导致签名不匹配。
修复方案
- 所有待签名字符串拼接处,将
"\\n"替换为真实换行符"\n"(Java字符串中单个反斜杠加n即为真实换行符) - 删除给待签名字符串包裹双引号的冗余代码
- 日期格式使用标准RFC1123格式
EEE, dd MMM yyyy HH:mm:ss 'GMT',强制设置GMT时区,生成的日期值同时用于签名拼接和请求头x-ms-date字段,保证两边值完全一致 - 规范化处理x-ms开头的请求头:统一转小写后按字典序排序,替换值中的多余空白为单个空格后再拼接
- 规范化处理查询参数:统一转小写后按字典序排序,再按
参数名:参数值格式拼接到资源串末尾 - 不要硬编码空字段,按固定顺序组装签名部分数组,最后统一用换行符拼接,避免空行计数错误
修复后核心代码示例
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
相关产品推荐
相关产品推荐

