Dart生成Azure Blob Storage SAS Token及相关问题排查
一、问题背景
我正在用Dart实现Azure Blob Storage的SAS Token生成,参考Python代码时不清楚policy id的获取位置,希望纯Dart完成。已写出Blob的SAS生成代码,但遇到三个问题:
- 生成的Token中时间冒号被转义为
%3A,手动替换无效 - 生成时间和Azure Portal的Token有5小时27分钟左右的差异
- 用容器级SAS(包括Portal生成的)调用接口列容器内Blob时返回403错误
以下是我编写的Blob SAS生成代码:
import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:intl/intl.dart'; String encode_signature(String signature){ return signature.replaceAll(':', '%3A'); } String generateSasToken(String accountName, String accountKey, String containerName, String blobName, int expiryHours) { var ttl = DateTime.now().add(Duration(hours: expiryHours)).millisecondsSinceEpoch ~/ 1000; var stringToSign = "r\n${DateFormat("yyyy-MM-ddTHH:mm:ssZ").format(DateTime.now().toUtc())}\n${DateFormat("yyyy-MM-ddTHH:mm:ssZ").format(DateTime.now().add(Duration(hours: expiryHours)).toUtc())}\n/$accountName/$containerName/$blobName\n\n\n\n\n\n\n"; var hmacSha256 = Hmac(sha256, base64.decode(accountKey)); var signature = base64.encode(hmacSha256.convert(utf8.encode(stringToSign)).bytes); var queryParams = { 'sp': 'r', 'st': DateFormat("yyyy-MM-ddTHH:mm:ssZ").format(DateTime.now().toUtc()), 'se': DateFormat("yyyy-MM-ddTHH:mm:ssZ").format(DateTime.now().add(Duration(hours: expiryHours)).toUtc()), 'sv': '2022-11-02', 'sr': 'b', 'sig': encode_signature(signature),//Uri.encodeComponent(signature).replaceAll('%3A', ':'), // Encode the signature component }; var queryString = Uri(queryParameters: queryParams).query; return queryString; } void main() { var accountName = "account_name"; var accountKey = "accountkey"; var containerName = "azure-webjobs-hosts"; var blobName = "0_12d6fa4202ac42bdbc33923c413c68ff_1.json"; var expiryHours = 168; var sasToken = generateSasToken(accountName, accountKey, containerName, blobName, expiryHours); print(sasToken); }
容器SAS调用代码:
import 'dart:convert'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; void main() async { final String folderURL = 'https://myblobaccount.blob.core.windows.net/azure-webjobs-hosts?sp=r&st=2024-03-22T07:21:47Z&se=2024-03-22T15:21:47Z&sv=2022-11-02&sr=c&sig=Eg%2B04lD3EWLo2wGocU9mqNjKwqKmo04zSCH0Dbsjqlg%3D';//$storageBlobEndpoint/$containerName?$sasToken'; // Make a request to list the blobs within the folder final response = await http.get(Uri.parse(folderURL)); if (response.statusCode == 200) { final List<dynamic> blobs = json.decode(response.body); print("Files in the folder:"); for (var blob in blobs) { print(blob['name']); } } else { print('Failed to fetch data: ${response.statusCode}'); } }
二、问题原因及解决方案
1. 时间冒号被转义的原因
你用Uri(queryParameters: queryParams).query生成查询字符串时,Uri会自动对所有特殊字符(包括冒号)进行URL编码,所以手动替换encode_signature里的内容完全没用——因为最后Uri生成时又重新编码了。
解决方法:不需要手动处理时间的编码,Azure SAS完全支持URL编码后的时间格式,直接使用Uri生成的结果即可。如果非要保持冒号不转义,只能手动拼接字符串,但不推荐,URL编码是标准行为,不会影响SAS的有效性。
2. 时间差异的原因
你的代码中,生成签名字符串和查询参数时调用了多次DateTime.now(),两次调用的时间差会导致生成的时间不一致;另外,DateFormat的格式yyyy-MM-ddTHH:mm:ssZ可能会生成带时区偏移的格式(比如+0800),而Azure要求的是严格的UTC格式(结尾为大写Z)。
解决方法:提前生成一次当前UTC时间和过期UTC时间,复用这两个值,同时指定正确的时间格式:
var nowUtc = DateTime.now().toUtc(); var expiryUtc = nowUtc.add(Duration(hours: expiryHours)); final dateFormat = DateFormat("yyyy-MM-ddTHH:mm:ss'Z'"); // 固定Z结尾,避免时区偏移
然后在签名字符串和查询参数里都用这两个变量。
3. 容器SAS返回403的原因
两个核心问题:
- 列容器内Blob需要
sp参数包含l(列出)权限,你当前的sp=r只有读取权限,权限不足直接导致403; - Azure列Blob接口返回的是XML格式,不是JSON,你用
json.decode会报错,但这不是403的直接原因。
解决方法:
- 生成容器SAS时,
sp要设置为rl(读取+列出); - 调用接口后解析XML而不是JSON;
- 确保容器SAS的签名字符串路径只到容器级别(
/$accountName/$containerName),不要包含Blob名称。
三、Policy ID的获取位置
Policy ID是存储容器的**存储访问策略(Stored Access Policy)**的ID,获取路径:
- 进入目标存储账户的Blob服务
- 打开目标容器的「访问策略」选项卡
- 创建或查看已有的访问策略,对应的ID就是policy id
如果不需要使用存储访问策略,生成SAS时可以不指定policy id,直接用账户密钥签名即可(你当前的代码就是这种方式)。
四、修正后的Dart代码
1. 通用SAS生成代码(支持Blob/容器)
import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:intl/intl.dart'; String generateSasToken(String accountName, String accountKey, String containerName, String? blobName, int expiryHours, {bool isContainerSas = false}) { var nowUtc = DateTime.now().toUtc(); var expiryUtc = nowUtc.add(Duration(hours: expiryHours)); final dateFormat = DateFormat("yyyy-MM-ddTHH:mm:ss'Z'"); // 构建签名字符串和参数 String resourcePath; String sr; String sp; if (isContainerSas) { resourcePath = "/$accountName/$containerName"; sr = 'c'; sp = 'rl'; // 容器级需要列出+读取权限 } else { resourcePath = "/$accountName/$containerName/$blobName"; sr = 'b'; sp = 'r'; } var stringToSign = """$sp ${dateFormat.format(nowUtc)} ${dateFormat.format(expiryUtc)} $resourcePath 2022-11-02 """; var hmacSha256 = Hmac(sha256, base64.decode(accountKey)); var signature = base64.encode(hmacSha256.convert(utf8.encode(stringToSign)).bytes); var queryParams = { 'sp': sp, 'st': dateFormat.format(nowUtc), 'se': dateFormat.format(expiryUtc), 'sv': '2022-11-02', 'sr': sr, 'sig': Uri.encodeComponent(signature), }; return Uri(queryParameters: queryParams).query; } void main() { var accountName = "account_name"; var accountKey = "accountkey"; var containerName = "azure-webjobs-hosts"; var blobName = "0_12d6fa4202ac42bdbc33923c413c68ff_1.json"; var expiryHours = 168; // 生成Blob SAS var blobSasToken = generateSasToken(accountName, accountKey, containerName, blobName, expiryHours); print("Blob SAS Token: $blobSasToken"); // 生成容器SAS var containerSasToken = generateSasToken(accountName, accountKey, containerName, null, expiryHours, isContainerSas: true); print("Container SAS Token: $containerSasToken"); }
2. 容器Blob列表调用代码
import 'package:http/http.dart' as http; import 'package:xml/xml.dart' as xml; void main() async { final String containerUrl = 'https://myblobaccount.blob.core.windows.net/azure-webjobs-hosts?sp=rl&st=2024-03-22T07:21:47Z&se=2024-03-22T15:21:47Z&sv=2022-11-02&sr=c&sig=Eg%2B04lD3EWLo2wGocU9mqNjKwqKmo04zSCH0Dbsjqlg%3D'; final response = await http.get(Uri.parse(containerUrl)); if (response.statusCode == 200) { final document = xml.XmlDocument.parse(response.body); final blobs = document.findAllElements('Blob'); print("容器内的文件:"); for (var blob in blobs) { print(blob.findElements('Name').first.text); } } else { print('请求失败:${response.statusCode},响应内容:${response.body}'); } }
注意:需要在pubspec.yaml中添加xml依赖:
dependencies: xml: ^6.5.0
内容的提问来源于stack exchange,提问作者sai

