Java で Azure BLOB オブジェクト を 30秒間 だけ Read 可能な SAS トークン を ライブラリを使わないで生成する

## tl;dr

SAS トークンの構造を学習するために、ライブラリを使わないで、 Java で生成しました。
( Node.js 版は [こちら](https://qiita.com/exabugs/items/59efb4dacf193bfb4f5d) )

公式ドキュメント : [サービス SAS を作成する](https://docs.microsoft.com/ja-jp/rest/api/storageservices/create-service-sas
)

## コード

BLOB オブジェクト を 30秒 だけ Read 可能なURL


```java:Main.java
public class Main {
    public static void main(String[] args) {
        String name = "ストレージアカウント名";
        String key = "キー";
        String container = "コンテナ名";
        String blob = "ブロブ名";
        int expiry = 30; // 30 秒間有効
        String filename = "ファイル名";

        SAS sas = new SAS(name, key);
        String url = sas.getUrl(container, blob, expiry, filename);
        System.out.println(url);
    }
}
```

```java:SAS.java
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.StringJoiner;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class SAS {
    String account_name;
    String account_key;

    // コンストラクタ
    // @param name ストレージアカウント名
    // @param key ストレージアカウントキー
    public SAS(String name, String key) {
        account_name = name;
        account_key = key;
    }

    // トークン付きURLを生成する
    // @param container コンテナ名
    // @param blob ブロブ名
    // @param expiry_second トークン有効期間(秒)
    // @param filename ファイル名
    public String getUrl(String container, String blob, int expiry_second, String filename) {
        String token = getToken(container, blob, expiry_second, filename);
        String url = "https://" + account_name + ".blob.core.windows.net/" + container + "/" + blob;
        return url + "?" + token;
    }

    // トークンを生成する
    // @param container コンテナ名
    // @param blob ブロブ名
    // @param expiry_second トークン有効期間(秒)
    // @param filename ファイル名
    String getToken(String container, String blob, int expiry_second, String filename) {

        // パラメータ
        Date now = new Date();
        String start = ""; // isoDate(now, start_second);
        String expiry = isoDate(now, expiry_second);

        String version = "2018-11-09";
        String resource = "b";
        String permission = "r";
        String ip = ""; // "0.0.0.0/0";
        String protocol = ""; // "https";

        String canonicalizedResource = "/blob/" + account_name + "/" + container + "/" + blob;

        String rscc = "";
        String rscd = "attachment; filename=\"" + filename + "\"";
        String rsce = "";
        String rscl = "";
        String rsct = "";

        String identifier = "";
        String snapshot = "";

        // シグネチャ (順番は厳守)
        String[] arr = { //
                permission, //
                start, //
                expiry, //
                canonicalizedResource, //
                identifier, //
                ip, //
                protocol, //
                version, //
                resource, //
                snapshot, //
                rscc, // Cache-Control
                rscd, // Content-Disposition
                rsce, // Content-Encoding
                rscl, // Content-Language
                rsct, // Content-Type
        };
        String stringToSign = String.join("\n", arr);

        // HMAC256
        String signature = getHMAC256(stringToSign);

        // クエリパラメータ (順番は任意)
        String params[][] = { //
                { "sp", permission }, //
                { "sr", resource }, //
                // { "st", start }, //
                { "se", expiry }, //
                // { "sip", ip }, //
                // { "spr", protocol }, //
                { "sv", version }, //
                { "sig", signature }, //
                { "rscd", rscd }, //
        };
        StringJoiner sas = new StringJoiner("&");
        Arrays.asList(params).forEach(v -> sas.add(v[0] + "=" + encode(v[1])));

        return sas.toString();
    }

    String encode(String str) {
        try {
            return URLEncoder.encode(str, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new InternalError(e.toString());
        }
    }

    String isoDate(Date date, int second) {
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.add(Calendar.SECOND, second);
        return fmt.format(cal.getTime());
    }

    String getHMAC256(String input) {
        try {
            String algo = "HmacSHA256";
            // 1. キー は バイト形式 (Base64 で デコード)
            byte[] key = Base64.getDecoder().decode(account_key);
            SecretKeySpec secretKey = new SecretKeySpec(key, algo);
            Mac sha256HMAC = Mac.getInstance(algo);
            sha256HMAC.init(secretKey);
            // 2. 入力 は バイト形式 (UTF8 で デコード)
            byte[] inp = input.getBytes("UTF-8");
            // 3. 出力 は バイト形式 を Base64 で エンコード
            byte[] out = sha256HMAC.doFinal(inp);
            return Base64.getEncoder().encodeToString(out);
        } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
            throw new InternalError(e.toString());
        }
    }
}
```