PHPでSAS tokenを使ってAzure Blob Storageにファイルをアップロードする

現在開発中の案件で、Shared Access Signature(SAS)を使ってBlobにデータを上げる必要があって、若干ハマったのでメモ。

先日プレビュー版がリリースされた user delegation SASってのもありますが、今回はストレージアカウントのキーを使う感じで。user delegation SASに関しては、亀淵さんのブログが参考になると思います。

今回使用するバックエンドはPHPなので、SASの作成には、microsoft/azure-storage-blobを使うことにします。

ざっとコード書くてみたのがこんな感じ...

<?php
...
$accountName = getenv('STORAGE_ACCOUNT_NAME');
$accountKey = getenv('STORAGE_ACCOUNT_KEY');
$containerName = getenv('CONTAINER_NAME');

$helper = new BlobSharedAccessSignatureHelper($accountName, $accountKey);
$sas = $helper->generateBlobServiceSharedAccessSignatureToken(
    Resources::RESOURCE_TYPE_BLOB,
    "{$containerName}/composer.json",
    'w',
    Chronos::now(new \DateTimeZone('UTC'))->addSeconds(60)->format(DATE_ATOM),
    Chronos::now(new \DateTimeZone('UTC'))->subSeconds(10)->format(DATE_ATOM),
    '', // リクエスト元のIPアドレス
    'https'
);

echo "sas: {$sas}\n";

これで試すと、 403 Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. こんなエラーがでてしまいます。

散々悩んだ挙げ句、ブラウザーで JavaScript と HTML を使用して BLOB をアップロード、一覧表示、および削除するを実際に動かして、リクエスト内容などを確認した結果ようやく原因がわかりました。

PHPの日付フォーマット DATE_ISO8601 がiso 8601準拠ではないので、 DATE_ATOM を使っていたのですが、TimeZoneの書式が違うことが原因でした。

$ cat date.php
<?php
$date = new \DateTime('now', new \DateTimeZone('UTC'));

echo $date->format(DATE_ISO8601)."\n";
echo $date->format(DATE_ATOM)."\n";
echo $date->format('Y-m-d\TH:i:s\Z')."\n";
?>

$ php date.php
2019-11-01T01:46:38+0000
2019-11-01T01:46:38+00:00
2019-11-01T01:46:38Z     // -> この形式でないとだめ

'Y-m-d\TH:i:s\Z' を使うことで正常にアップロードできました。めでたしめでたし。

最終的にできあがったサンプルがこちら

<?php
require './vendor/autoload.php';

use Cake\Chronos\Chronos;
use GuzzleHttp\Client;
use MicrosoftAzure\Storage\Blob\BlobSharedAccessSignatureHelper;
use MicrosoftAzure\Storage\Blob\Internal\BlobResources;
use MicrosoftAzure\Storage\Common\Internal\Resources;

$accountName = getenv('STORAGE_ACCOUNT_NAME');
$accountKey = getenv('STORAGE_ACCOUNT_KEY');
$containerName = getenv('CONTAINER_NAME');

$helper = new BlobSharedAccessSignatureHelper($accountName, $accountKey);
$sas = $helper->generateBlobServiceSharedAccessSignatureToken(
    Resources::RESOURCE_TYPE_BLOB,
    "{$containerName}/composer.json",
    'w',
    Chronos::now(new \DateTimeZone('UTC'))->addSeconds(60)->format('Y-m-d\TH:i:s\Z'),
    Chronos::now(new \DateTimeZone('UTC'))->subSeconds(10)->format('Y-m-d\TH:i:s\Z'),
    '', // リクエスト元のIPアドレス
    'https'
);

echo "sas: {$sas}\n";

try {
    $client = new Client(['base_uri' => sprintf('https://%s.%s/%s/', $accountName, Resources::BLOB_BASE_DNS_NAME, $containerName)]);

    $response = $client->put('composer.json', [
        'body' => file_get_contents('./composer.json'),
        'headers' => [
            'x-ms-version' => BlobResources::STORAGE_API_LATEST_VERSION,
            'x-ms-blob-type' => 'BlockBlob',
        ],
        'query' => $sas,
    ]);

    echo sprintf("StatusCode: %d\n", $response->getStatusCode());
} catch(\Exception $e) {
    echo sprintf("StatusCode: %d\n", $e->getCode());
    echo $e->getMessage() . "\n";
}

サンプルコードは、こちらにもあげてあります。