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

Dart生成Azure Blob Storage SAS Token及相关问题排查

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,获取路径:

  1. 进入目标存储账户的Blob服务
  2. 打开目标容器的「访问策略」选项卡
  3. 创建或查看已有的访问策略,对应的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 07:24:51