
本文详解 Azure 账户级 SAS Token 签名失败(“Signature did not match”)的根本原因,重点指出 REST API 2021-12-02 及更高版本中 signedEncryptionScope 字段为必填项,并提供可直接运行的 Java 修复代码与关键注意事项。
本文详解 azure 账户级 sas token 签名失败(“signature did not match”)的根本原因,重点指出 rest api 2021-12-02 及更高版本中 **`signedencryptionscope` 字段为必填项**,并提供可直接运行的 java 修复代码与关键注意事项。
在使用 Azure Storage REST API(如 2021-12-02 版本)手动构造账户级 SAS(Shared Access Signature)Token 时,签名验证失败(HTTP 403 + Signature did not match)是最常见却极易被忽视的问题。错误日志中显示的 String to sign used was ... 实际就是 Azure 服务端用于校验的原始字符串——而你的代码若未严格遵循该版本的签名规范,哪怕仅缺失一个换行或字段为空,HMAC-SHA256 计算结果即完全失效。
核心原因:signedEncryptionScope 字段缺失
根据 Azure 官方文档,自 API 版本 2020-08-04 起,账户级 SAS 的 StringToSign 必须包含 11 行(含末尾换行),其中第 10 行为 signedEncryptionScope,第 11 行必须是空行(即 \n)。你原代码中仅拼接了 9 行,且遗漏了该字段及结尾换行符,导致签名计算与 Azure 服务端预期不一致。
✅ 正确的 StringToSign 结构(共 11 行,每行以 \n 结尾):
accountname signedpermissions signedservice signedresourcetype signedstart signedexpiry signedIP signedProtocol signedversion signedEncryptionScope ← 新增字段(若不启用加密范围,填空字符串 "") ← 必须存在的空行(即最后一个 \n)
以下是修复后的 Java 示例代码(已通过 Azure Files 资源实测验证):
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;
import java.util.Base64;
public class AzureSasGenerator {
public static void main(String[] args) throws Exception {
String accountName = "<your-account-name>";
String accessKey = "<your-base64-encoded-account-key>"; // 注意:必须是 Base64 解码前的原始密钥字符串
String signedPermissions = "rwdlacupiy";
String signedService = "bfqt"; // 'b': blob, 'f': file, 'q': queue, 't': table
String signedResType = "o"; // 'o': service-level (account SAS)
String signedIp = ""; // 可选:限制客户端 IP
String protocol = "https";
String apiVersion = "2021-12-02";
String signedEncryptionScope = ""; // 若未启用 Encryption Scope,必须显式设为空字符串
// ✅ 严格按 11 行格式构建 StringToSign(含末尾空行)
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
Calendar cal = Calendar.getInstance();
cal.add(Calendar.HOUR, -2); // start: 2 hours ago
String start = fmt.format(cal.getTime());
cal.add(Calendar.HOUR, 4); // expiry: 2 hours from now
String expiry = fmt.format(cal.getTime());
String stringToSign = String.join("\n",
accountName,
signedPermissions,
signedService,
signedResType,
start,
expiry,
signedIp,
protocol,
apiVersion,
signedEncryptionScope, // ← 关键:不可省略!
"" // ← 必须的空行(第11行)
);
// 使用原始密钥(Base64解码后)生成 HMAC-SHA256 签名
byte[] decodedKey = Base64.getDecoder().decode(accessKey);
SecretKeySpec secretKey = new SecretKeySpec(decodedKey, "HmacSHA256");
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(secretKey);
String signature = Base64.getEncoder()
.encodeToString(hmac.doFinal(stringToSign.getBytes("UTF-8")));
// 构建 SAS Token(注意:所有参数需 URL 编码,但 sig 本身已 Base64 编码,无需再 URLEncode)
String sasToken = String.join("&",
"sv=" + apiVersion,
"ss=" + signedService,
"srt=" + signedResType,
"sp=" + signedPermissions,
"se=" + URLEncoder.encode(expiry, "UTF-8"),
"st=" + URLEncoder.encode(start, "UTF-8"),
"spr=" + protocol,
"sig=" + URLEncoder.encode(signature, "UTF-8")
);
System.out.println("SAS Token: " + sasToken);
// 使用示例:https://<account>.file.core.windows.net/share/file.txt?{sasToken}
}
}⚠️ 关键注意事项:
- 密钥处理:accessKey 必须是你在 Azure 门户复制的 原始账户密钥(Base64 字符串),而非其 Base64 解码后的二进制字节 —— 代码中 Base64.getDecoder().decode(accessKey) 即完成此步,切勿二次解码。
- 时间格式:HH(24 小时制)而非 hh(12 小时制),否则 00:00–23:59 会被解析为 12:00–11:59,导致时间窗口错乱。
- 字段顺序与换行:StringToSign 每行末尾必须有 \n,且最后一行必须是空字符串 "" 后接 \n(即总共 11 个 \n 分隔符)。
- Encryption Scope:即使未启用,也必须显式传入空字符串 "";若传 null 或完全省略,将导致签名不匹配。
- 协议一致性:spr=https 必须与实际请求协议一致;若用 http 请求但 spr=https,同样返回 403。
总结:Azure SAS 签名失败几乎总是源于 StringToSign 格式与当前 API 版本规范不一致。牢记「11 行规则」,逐字段核对、严格保留换行,并优先使用 Azure SDK for Java(如 Azure.Storage.Files.Shares)替代手动生成,可大幅降低出错风险并提升安全性。

















