Статьи

Волшебство хранилища Windows Azure с сигнатурами общего доступа

При создании облачных приложений в Windows Azure всегда полезно делегировать как можно больше работы специализированным службам. Загрузки файлов могут быть хорошим примером: их можно передавать прямо из хранилища BLOB-объектов Windows Azure на клиент без необходимости передавать веб-приложение, размещенное в облачных службах Windows Azure или на веб-сайтах. Зачем занимать веб-сервер копированием данных из потока запросов в поток ответов? Пусть хранилище больших двоичных объектов справится с этим!

Обдумывая это, вы можете подумать о некоторых проблемах. Вот некоторые из них:

  • Как я могу сохранить этот BLOB-объект в безопасности? Я не хочу давать всем доступ к этому!
  • Как сохранить URL-адрес для загрузки на своем веб-сервере, отслеживать количество загрузок (или применять другие правила безопасности) и при этом выгружать загрузку в хранилище больших двоичных объектов?
  • Как можно сохранить большой двоичный объект так, чтобы он был понятен моему приложению (например, идентификатор клиента или что-то еще), но при этом дать ему понятное имя при загрузке?

Давайте ответим на это!

Встречайте подписи общего доступа

Обеспечить безопасность больших двоичных объектов в Windows Azure Blob Storage довольно просто, но это также своего рода история «все или ничего»… Либо вы делаете все большие двоичные объекты в контейнере частными, либо вы делаете их общедоступными.

Не волнуйтесь, хотя! Используя подписи общего доступа, можно предоставить временные привилегии для большого двоичного объекта для доступа на чтение и запись. Вот фрагмент кода, который предоставит доступ для чтения к BLOB- объекту с именем helloworld.txt , находящемуся в частном контейнере с именем files , в течение следующей минуты:

CloudStorageAccount account = // your storage account connection here
var client = account.CreateCloudBlobClient();
var container = client.GetContainerReference("files");
var blob = container.GetBlockBlobReference("helloworld.txt");

var builder = new UriBuilder(blob.Uri);
builder.Query = blob.GetSharedAccessSignature(
    new SharedAccessBlobPolicy
    {
        Permissions = SharedAccessBlobPermissions.Read,
        SharedAccessStartTime = new DateTimeOffset(DateTime.UtcNow.AddMinutes(-5)),
        SharedAccessExpiryTime = new DateTimeOffset(DateTime.UtcNow.AddMinutes(1)
    }).TrimStart('?');

var signedBlobUrl = builder.Uri;

Заметьте, я даю доступ начиная с 5 минут назад, просто чтобы убедиться, что любые отклонения часов на этом пути игнорируются в течение разумного промежутка времени.

Вот и все: наш BLOB- объект защищен, и, передав подписанныйBlobUrl нашему пользователю, он или она может начать загрузку нашего BLOB- объекта, вообще не имея доступа ни к каким другим BLOB-объектам.

Встречайте перенаправления HTTP

Подписи общего доступа — это действительно круто, но сгенерированные URL-адреса… «бесполезны», их нелегко или просто запомнить. Ну, есть такая вещь, которая называется HTTP перенаправления, верно? Вот метод действия ASP.NET MVC, который проверяет, аутентифицирован ли пользователь, запрашивает ли хранилище правильное имя файла, генерирует подписанную подпись доступа и перенаправляет нас к фактической загрузке.

[Authorize]
[EnsureInvoiceAccessibleForUser]
public ActionResult DownloadInvoice(string invoiceId)
{
    // Fetch invoice
    var invoice = InvoiceService.RetrieveInvoice(invoiceId);
    if (invoice == null)
    {
        return new HttpNotFoundResult();
    }
    
    // We can do other things here: track # downloads, ...

    // Build shared access signature
    CloudStorageAccount account = // your storage account connection here
    var client = account.CreateCloudBlobClient();
    var container = client.GetContainerReference("invoices");
    var blob = container.GetBlockBlobReference(invoice.CustomerId + "-" + invoice.InvoiceId);

    var builder = new UriBuilder(blob.Uri);
    builder.Query = blob.GetSharedAccessSignature(
        new SharedAccessBlobPolicy
        {
            Permissions = SharedAccessBlobPermissions.Read,
            SharedAccessStartTime = new DateTimeOffset(DateTime.UtcNow.AddMinutes(-5)),
            SharedAccessExpiryTime = new DateTimeOffset(DateTime.UtcNow.AddMinutes(1)
        }).TrimStart('?');

    var signedBlobUrl = builder.Uri;

    // Redirect
    return Redirect(signedBlobUrl);
}

Это дает нам лучшее из обоих миров: наше веб-приложение может по-прежнему проверять доступ и выполнять на нем некоторую бизнес-логику, но мы можем перенести загрузку файла в хранилище BLOB-объектов.

Заголовок расположения контента «Встречаются подписи общего доступа»

Зачастую хранение — это техническая вещь, где мы выбираем технические имена файлов для вещей, которые мы храним, вместо понятных для человека или понятных для человека имен файлов. В приведенном выше примере пользователи получат очень странное имя файла для загрузки: идентификатор клиента + идентификатор счета-фактуры, объединенные. Нет расширения .pdf , и ничего больше. Пользователи могут запутаться в этом или столкнуться с проблемами при открытии файла, потому что браузер не распознает, что это PDF.

Last November, a feature was added to blob storage which enables us to let a blob be whatever we want it to be: support for setting some additional headers on a blob through the Shared Access Signature.

The following headers can be specified on-the-fly, through the shared access signature:

  • Cache-Control
  • Content-Disposition
  • Content-Encoding
  • Content-Language
  • Content-Type

Here’s how to generate a meaningful Shared Access Signature in the previous example, where we specify a human-readable filename for the resulting download, as well as a custom content type:

builder.Query = blob.GetSharedAccessSignature(
    new SharedAccessBlobPolicy
    {
        Permissions = SharedAccessBlobPermissions.Read,
        SharedAccessStartTime = new DateTimeOffset(DateTime.UtcNow.AddMinutes(-5)),
        SharedAccessExpiryTime = new DateTimeOffset(DateTime.UtcNow.AddMinutes(1)
    },
    new SharedAccessBlobHeaders
    {
        ContentDisposition = "attachment; filename="
            + customer.DisplayName + "-invoice-" + invoice.InvoiceId + ".pdf",
        ContentType = "application/pdf"
    }).TrimStart('?');

Note: for this feature to work, the service version for the storage account must be set to the latest one, using the DefaultServiceVersion on the blob client. Here’s an example:

CloudStorageAccount account = // your storage account connection here
var client = account.CreateCloudBlobClient();
var serviceProperties = client.GetServiceProperties();
serviceProperties.DefaultServiceVersion = "2013-08-15";
client.SetServiceProperties(serviceProperties);

Combining all these techniques, we can do some analytics and business logic in our web application and offload the boring file and network I/O to blob storage.

Enjoy!