XamarinでAzure StorageをShared Access Signature（SAS)で操作してみる

# はじめに

この記事は**[「Xamarin(その2) Advent Calendar 2016 - Qiita」](http://qiita.com/advent-calendar/2016/xamarin2)**の12日目になります。


# 概要

2016年10月24日に「Azure Storage Client Library for Xamarin」がGAしたので試した内容を記載していきます。

ここではアドホックなSharedAccessSignature(SAS)を発行してAzureStorageにアクセスする方法を試したいと思います。共有キー認証を利用してAzureStorageにアプリからアクセスする事は可能なのですが、公式サイトでも言及されていますが共有キーをアプリに埋め込む方法はセキュリティの観点から推奨されていません。

![storage-token-diagram.png](https://qiita-image-store.s3.amazonaws.com/0/93820/8b407be9-a2f9-9192-70e0-ba6ee973a89b.png)
[出典：Xamarin.Forms アプリで Azure Storage に接続する](https://docs.microsoft.com/ja-jp/azure/app-service-mobile/app-service-mobile-xamarin-forms-blob-storage)

推奨される方式は上記の図のようにSASトークンを発行するサービスを立て、Clietnからトークン発行依頼をかけ発行されたトークンを用いてAzureStorageにアクセスする仕組みです。

SAS発行の情報を調査していたら**[AzureFuncitonsを利用したSASトークンの発行方法](https://azure.microsoft.com/ja-jp/resources/samples/functions-dotnet-sas-token/)**を見つけたのでこちらを試します。ARMTemplateでAzureリソースの自動生成ができて、さらにGIT経由の自動デプロイでCI出来てしまう非常に有益なサンプルです。


但し、利用するには少しコツがいるのでその点も踏まえて記載していきたいと思います。

# 参考

storage-client-library-ga-for-xamarinの公式情報は下記を参照してください。

- **[storage-client-library-ga-for-xamarin](https://azure.microsoft.com/en-us/blog/announcing-storage-client-library-ga-for-xamarin/)**
- **[Xamarin から BLOB ストレージを使用する方法](https://docs.microsoft.com/ja-jp/azure/storage/storage-xamarin-blob-storage)**

SASについては下記を参照してください。

- **[AzureFuncitonsを利用したSASトークンの発行方法](https://azure.microsoft.com/ja-jp/resources/samples/functions-dotnet-sas-token/)**
- **[Shared Access Signature(SAS)トークンについて](https://docs.microsoft.com/ja-jp/azure/storage/storage-dotnet-shared-access-signature-part-1)**


AzureStorageの作成方法は下記を参照してください。

- **[Azure ストレージ アカウントについて](https://docs.microsoft.com/ja-jp/azure/storage/storage-create-storage-account)**

Azure Storage library for Xamarinのサンプルソースは下記を利用します。

- **[storage-blob-xamarin-image-uploader](https://github.com/Azure-Samples/storage-blob-xamarin-image-uploader)**


AzureStorageExploreをインストールしておくとAzureStorageに存在するファイルを確認できるので便利です。

- **[AzureStorageExplore](http://storageexplorer.com/)**

# 利用方法
## SAS発行用のAzureFunctionsを構築

構築方法は下記のサイトを参考にしています。

- **[AzureFuncitonsを利用したSASトークンの発行方法](https://azure.microsoft.com/ja-jp/resources/samples/functions-dotnet-sas-token/)**

### GitHubでソースをフォークする

下記のサイトでソースを自分のGitにフォークします。

- **[functions-dotnet-sas-token](https://github.com/Azure-Samples/functions-dotnet-sas-token)**

以降、フォークしたソースを修正します。

### ARMTemplateからFunctionsを作成する

下記のサイトで「Azureへデプロイ」ボタンを押下します。
カスタムデプロイ画面が表示されるので適宜設定を行い購入ボタンを押下します。

- **[AzureFuncitonsを利用したSASトークンの発行方法](https://azure.microsoft.com/ja-jp/resources/samples/functions-dotnet-sas-token/)**

![image](https://qiita-image-store.s3.amazonaws.com/0/93820/3130f5af-e337-2762-f328-f67c2f9c88e1.png)

設定でRepo URLをフォークしたGitのURLに変更しManualIntegrationをfalseにします。
これでソースを修正してプッシュするとWebHookにより自動的にAzureFunctionsにソースがデプロイされるようになります。

### 各種設定を行う

カスタムデプロイを行うとAzureFunctionsに紐づくAzureStorageが作成されます。
コンテンツを保存するための別のAzureStorageに変更したい場合はアプリケーション設定にAzureStorageの情報を追加します。

![image](https://qiita-image-store.s3.amazonaws.com/0/93820/fe4a8a7a-0266-986e-3b73-6a440bbf11ec.png)

Functionの画面から「Function Appの設定」→「AppServiceの設定に移動」を押下します。

![image](https://qiita-image-store.s3.amazonaws.com/0/93820/7805f84e-8b2d-334b-7f25-535fbff27957.png)

AppServiceの詳細画面が表示されるのでアプリケーション設定を選択します。
アプリの設定でAzureStorageの接続文字列を追加して登録します。

AzureStorageの接続文字列は下記を参考にしてください。

    "DefaultEndpointsProtocol=https;AccountName=storage_name;AccountKey=storage_key"


### ソースを修正する

37行目付近の接続文字列をアプリケーションで設定したKeyの名前に変更します。

    var storageAccount = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["kokonixamarin"]);

40行目付近に下記の1行を追加します。
Storageコンテナが存在しないければ作成するという処理です。

    container.CreateIfNotExists();

修正後、コミットしてGithubにプッシュすれば自動的にデプロイされているはずです。

### 接続テスト

デプロイが完了したらテストを行いましょう。
AzureFuncitonsの画面から関数のURLを取得しておきます。

![image](https://qiita-image-store.s3.amazonaws.com/0/93820/4d2645ba-475b-d48e-669e-0a3ba94d03b1.png)


ここでは**[Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=ja)**を利用したテスト方法を記載します。

![image](https://qiita-image-store.s3.amazonaws.com/0/93820/82dd5ec5-03ec-8f46-d209-329294edf59b.png)

Postmanを起動したらURLテキストボックスに関数のURLを入力しPOSTを選択します。
Bodyタブでrawを選択しContentsTypeをJSON(application/json)にします。
ContentsTypeを選択すると自動的にHeadersに選択したContentsTypeが設定されます。

POST BODYには下記のようにJSONを記述します。

    {
       "container" : "test001",
       "blobName" : null,
       "permissions" : "Read,Write,Delete,List,Add,Create"
    }

Sendボタンを押下して成功すれば下記のようにSASトークンとURIが返却されます。

    {
       "token": "?sv=2015-12-11&sr=b&sig=M845Iiuy4hNlFy%2Bf0egWN%2BCP%2BBIWXA5RiEwhpuH0tUU%3D&st=2016-12-09T07%3A28%3A26Z&se=2016-12-09T08%3A33%3A26Z&sp=rcwl",
       "uri": "https://********.blob.core.windows.net/test001?sv=2015-12-11&sr=b&sig=M845Iiuy4hNlFy%2Bf0egWN%2BCP%2BBIWXA5RiEwhpuH0tUU%3D&st=2016-12-09T07%3A28%3A26Z&se=2016-12-09T08%3A33%3A26Z&sp=rcwl"
    }

Azure Storage ExploerでPOSTしたコンテナ名でコンテナが作成されていれば成功です。

![image](https://qiita-image-store.s3.amazonaws.com/0/93820/c2deabec-5fbd-6534-6f6a-6dede14c79ff.png)


これでFunctionsの設定は完了です。

## クライアント側での利用

### サンプルソースの取得

「Azure Storage library for Xamarin」を利用したサンプルソースが公開されているのでこちらを利用します。
サンプルソースも共有キー認証を利用しているので、その部分をSASを利用した方法にカスタマイズします。

下記よりダウンロードしてください。

**[storage-blob-xamarin-image-uploader](https://github.com/Azure-Samples/storage-blob-xamarin-image-uploader)**



### SAS対応カスタマイズ

![image](https://qiita-image-store.s3.amazonaws.com/0/93820/0b280fb7-43e5-0bfd-4429-05af58971619.png)


サンプルソースはSharedで作成されています。
「Azure Storage Library for Xamarin」は現在Sharedプロジェクトにのみ対応しています。
iOSとWinPhoneも含まれていますがAndroid環境のみを利用するためプロジェクトからアンロードしています。

下記のソースをSharedプロジェクト(XamarinImageUploader)に追加します。

ファイル名：**RequestJsonModel.cs**

    using Newtonsoft.Json;
    namespace XamarinImageUploader
    {
        [JsonObject("postdata")]
        public class RequestJsonModel
        {
            [JsonProperty("container")]
            public string container { get; set; }
            [JsonProperty("blobName")]
            public string blobName { get; set; }
            [JsonProperty("permissions")]
            public string permissions { get; set; }
        }
    }

ファイル名：**ResponseJsonModel.cs**

    using Newtonsoft.Json;
    namespace XamarinImageUploader
    {
        [JsonObject("resdata")]
        public class ResponseJsonModel
        {
            [JsonProperty("token")]
            public string token { get; set; }
            [JsonProperty("uri")]
            public string uri { get; set; }
        }
    }

下記のソースをXamarinImageUploader.Droidプロジェクトに追加します。

ファイル名：**Common.cs**

    using System;
    using System.Text;
    using System.Net;
    using Newtonsoft.Json;

    namespace XamarinImageUploader.Droid
    {
        class Common
        {
           private const string SASTokenUri = "FunctionsのURL";
           public static string getSasUri()
           {
               WebClient webClient = new WebClient();
               webClient.Headers[HttpRequestHeader.ContentType] = "application/json;charset=UTF-8";
               webClient.Headers[HttpRequestHeader.Accept] = "application/json";
               webClient.Encoding = Encoding.UTF8;

               var data = new RequestJsonModel();
               data.container = "testcontainer";
               data.blobName = null;
               data.permissions = "Read,Write,Delete,List,Add,Create";

               var reqData = JsonConvert.SerializeObject(data);
               var result = webClient.UploadString(new Uri(SASTokenUri), reqData);

               ResponseJsonModel resData = JsonConvert.DeserializeObject<ResponseJsonModel>(result);

               return resData.uri;
           }
       }
    }

Sharedプロジェクトにある下記のファイルを修正します。

ファイル名：**ImageManager.cs**

    private static CloudBlobContainer GetContainer(string sas)
    {
        // Parses the connection string for the WindowS Azure Storage Account
        // var account = CloudStorageAccount.Parse(Configuration.StorageConnectionString);
        // var client = account.CreateCloudBlobClient();
        // Gets a reference to the images container
        //var container = client.GetContainerReference("images");

        CloudBlobContainer container = new CloudBlobContainer(new Uri(sas));

        return container;
    }

下記のメソッドにあるCreateIfNotExistsAsyncをコメントアウトします。
原因は不明なのですがcreate権限はつけているにも拘わらずCreateIfNotExistsAsyncを実行すると「許可されていない操作」とのことでエラーになります。Functionsの方で対応しているのでここではコメントアウトして回避します。


    public static async Task<string> UploadImage(Stream image,string sas)
    // await container.CreateIfNotExistsAsync();

下記のメソッドに引数を追加します。

    public static async Task<string> UploadImage(Stream image,string sas)
    public static async Task<string[]> ListImages(string sas)
    public static async Task<byte[]> GetImage(string name, string sas)

上記のメソッド内に記載のある下記のソースに引数を追加します。

    var container = GetContainer(sas);

修正後ビルドをすると引数の数が一致せずに参照してる箇所でエラーが発生すると思います。
その個所に下記の引数をついかします。

    Common.getSasUri()


これで修正は完了です。

アプリを起動してファイル操作を試してみてください。

![test 1.png](https://qiita-image-store.s3.amazonaws.com/0/93820/bbc631f0-71a0-af4b-ca55-144469a27971.png)


正常にアップロードができていればAzureStorageExploerでStorageにファイルが上がっていることが確認できます。


![image](https://qiita-image-store.s3.amazonaws.com/0/93820/591427f6-4726-f067-e670-ecc60815a02a.png)

# まとめ

長くなりましたがそこまで手間暇をかけずに「Azure Storage Library for Xamarin」でSAS対応することが可能です。今回はSASの設定を大雑把にしていますが詳細に設定すれば制限を厳しくかけることが可能です。
SASの取得タイミングや非同期処理など考慮すべきことは増えますがセキュリティを担保できるので業務で利用するさいの実現性は高くなります。

「Azure Storage Library for Xamarin」は現在Sharedプロジェクトにのみ対応しています。
もしPCLで利用するとなるとDependencyServiceを利用して各OS側に実装して利用する方法が考えられます。
将来的にPCLに対応してもらえるとありがたいのですがbait and switch対応とかハードルがあるのでしょうね。