Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9299

aws-sdk を使わずにS3からデータを取得する

$
0
0
はじめに タイトルの通り、 aws-sdk を使わずに s3 からのデータ取得するやり方をまとめます。具体的には、AWS Signature Version 4 による署名を含む authorization ヘッダーの作成手順を示したものになります。 本記事で紹介しているコードは node.jsで実装しています。できるだけ、ビルドインモジュールのみを使って実装しましたが、http リクエストだけめんどくさかったので axios を使っています。 コードだけ知りたい方はこちらをどうぞ。 準備 iam ユーザの作成 AmazonS3ReadOnlyAccess ポリシーのアタッチされたユーザーを作成してください。この時に発行される、access_key_idと secret_access_key をメモしておいてください。後で使います。 バケットを作成 東京リージョン(ap-northeast-1)にバケットを作成してください。この時のバケット名をメモしておいて下さい。後で使います。 バケットにファイルを保存 先ほど作成したバケットにファイルを保存します。ファイル名やディレクトリ名は何でも構いません。本記事では、以下の内容のテキストファイルをバケット直下に test.txt という名前で保存します。 test.txt s3 fetchObject test 環境変数の設定 実行環境の環境変数を設定します。先ほどメモしておいた値を使います。 AWS_ACCESS_KEY=<access_key_id> AWS_SECRET_KEY=<secret_access_key> BUCKET=<bucket名> REGION=ap-northeast-1 axiosのインストール npm install axios 完成コード S3Service.js const crypto = require("crypto") const axios = require("axios").default const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY const BUCKET = process.env.BUCKET const REGION = process.env.REGION const AWS_SERVICE = "s3" const HOST = `${AWS_SERVICE}.${REGION}.amazonaws.com` const HASH_ALGORHYZM = "SHA256" class S3Service { constructor() { this.requestDate = new Date() } async fetchObject(params) { this.setRequestDate() const url = this.getUrl(params.filepath) const headers = this.getHeader(params) const res = await axios.get(url, { headers }) return res } createAuthorization(params) { const credential = this.getCredential() const signedHeaders = this.getSignedHeaders(params.headers) const signature = this.createSignature(params) return `AWS4-HMAC-SHA256 Credential=${credential},SignedHeaders=${signedHeaders},Signature=${signature}` } createSignature(params) { const formattedDate = this.formatRequestDate("YYYYMMDD") const dateKey = this.hmac("AWS4" + AWS_SECRET_KEY, formattedDate) const dateRegionKey = this.hmac(dateKey, REGION) const dateRegionServiceKey = this.hmac(dateRegionKey, AWS_SERVICE) const signingKey = this.hmac(dateRegionServiceKey, "aws4_request") const sign = this.createStringToSign(params) return this.hmacHex(signingKey, sign) } createStringToSign(params) { const formattedDate = this.formatRequestDate("ISO8601") const scope = this.getScope() const canonicalRequest = this.createCanonicalRequest(params) const signStrings = [ "AWS4-HMAC-SHA256", formattedDate, scope, this.hashHex(canonicalRequest) ] return signStrings.join("\n") } createCanonicalRequest(params) { const requests = [] const hasquery = params.query && typeof params.query === "object" const method = this.getMethod(params.method) requests.push(method) const canonicalUri = this.getCanonicalUri(params.filepath, hasquery) requests.push(canonicalUri) const canonicalQueryString = this.getCanonicalQueryString(params.query, hasquery) requests.push(canonicalQueryString) const canonicalHeaders = this.getCanonicalHeaders(params.headers) requests.push(canonicalHeaders) const signedHeaders = this.getSignedHeaders(params.headers) requests.push(signedHeaders) const hashedPayload = this.getHashedPayload(params.payload) requests.push(hashedPayload) return requests.join("\n") } getScope() { const formattedDate = this.formatRequestDate("YYYYMMDD") return `${formattedDate}/${REGION}/${AWS_SERVICE}/aws4_request` } getUrl(filepath) { return `https://${HOST}/${BUCKET}/${filepath}` } getEssentialHeaders(params) { return { host: HOST, "x-amz-content-sha256": this.hashHex(params.payload), "x-amz-date": this.formatRequestDate("ISO8601") } } getHeader(params) { const essentailHeaders = this.getEssentialHeaders(params) params.headers = Object.assign(essentailHeaders, params.headers) const authorization = this.createAuthorization(params) return Object.assign({ authorization }, params.headers) } getCredential() { const scope = this.getScope() return `${AWS_ACCESS_KEY}/${scope}` } getMethod(method) { return method.toUpperCase() } getCanonicalUri(filepath, hasquery) { const uri = `/${BUCKET}/${filepath}` return hasquery ? uri + "?" : uri } getCanonicalQueryString(query, hasquery) { if(!hasquery) return "" const queryStrings = this.objectToString(query, "=") return encodeURI(queryStrings.join("&")) } getCanonicalHeaders(header) { const headerStrings = this.objectToString(header, ":") return headerStrings.join("\n") + "\n" } getSignedHeaders(header) { return Object.keys(header).map(k => k.toLowerCase()).sort().join(";") } getHashedPayload(payload) { return this.hashHex(payload) } setRequestDate() { this.requestDate = new Date() } objectToString(query, sep) { const _query = {} Object.keys(query).forEach(k => { _query[k.toLowerCase()] = query[k] }) const queryStrings = [] Object.keys(_query).sort().forEach(k => { queryStrings.push(k + sep + _query[k]) }) return queryStrings } _hash(data) { return crypto.createHash(HASH_ALGORHYZM).update(data) } hashHex(data) { return this._hash(data).digest("hex") } _hmac(secretKey, data) { return crypto.createHmac(HASH_ALGORHYZM, secretKey).update(data) } hmac(secretKey, data) { return this._hmac(secretKey, data).digest() } hmacHex(secretKey, data) { return this._hmac(secretKey, data).digest("hex") } formatRequestDate(type) { const Y = this.requestDate.getUTCFullYear() const M = this.requestDate.getUTCMonth() + 1 const D = this.requestDate.getUTCDate() const h = this.requestDate.getUTCHours() const m = this.requestDate.getUTCMinutes() const s = this.requestDate.getUTCSeconds() const YYYY = Y.toString().padStart(4, "0") const MM = M.toString().padStart(2, "0") const DD = D.toString().padStart(2, "0") const hh = h.toString().padStart(2, "0") const mm = m.toString().padStart(2, "0") const ss = s.toString().padStart(2, "0") switch(type) { case "YYYYMMDD": return `${YYYY}${MM}${DD}` case "ISO8601": return `${YYYY}${MM}${DD}T${hh}${mm}${ss}Z` } } } コードの実行・結果 上記の s3 からデータを取得するためのクラスをインポートして使います。 fetchObject.js const S3Service = require("./S3Service").default async function fetchObject() { try { const s3 = new S3Service() const res = await s3.fetchObject({ method: "GET", filepath: "test.txt", payload: "" }) console.log(res.data) } catch (e) { console.log(e) } } fetchObject() console ❯ node fetchObject.js s3 fetchObject test 無事 s3 からデータの取得ができていることが確認できました! AWS Signature Version 4 による署名を含むauthorization ヘッダーの作成手順 ここからは上記で作成したコードの説明となります。 概要 下記の記事を node.js で実装したものになります。 https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sig-v4-header-based-auth.html canonical request の作成 canonical request を含む署名に必要な文字列の作成 署名の作成 署名を含む authorization ヘッダーの作成 authorization ヘッダーを設定して http リクエスト canonical request の作成 フォーマット canonical request のフォーマットは下記のような構成になっております。 <HTTPMethod>\n <CanonicalURI>\n <CanonicalQueryString>\n <CanonicalHeaders>\n <SignedHeaders>\n <HashedPayload> それでは詳細を見ていきます。 HTTPMethod uppercase の http メソッドになります。 例)GET, PUT, POST, DELETE ... CanonicalURI 取得したい s3 オブジェクトまでのURIエンコード(パーセントエンコード)されたホスト部以降の絶対パス。クエリーストリングがある場合は ? まで入れる。? 以降は CanonicalQueryString で指定。 例)http://s3.amazonaws.com/examplebucket/myphoto.jpg?hoge=x の場合 値 /examplebucket/myphoto.jpg? CanonicalQueryString URIエンコード(パーセントエンコード)されたクエリーストリング。 例)http://s3.amazonaws.com/examplebucket?prefix=somePrefix&marker=someMarker&max-keys=20 の場合 code encodeURI("marker") + "=" + encodeURI("someMarker") + "&" + encodeURI("max-keys")+ "=" + encodeURI("20") + "&" + encodeURI("prefix") + "=" + encodeURI("somePrefix") 値 marker=someMarker&max-keys=20&prefix=somePrefix 値を持っていないクエリーストリングも指定できる 例)http://s3.amazonaws.com/examplebucket?acl の場合 code encodeURI("acl") + "=" + "" 値 acl= CanonicalHeaders リクエストヘッダーのリスト。以下の条件を満たす必要がある。 \n で区切る ヘッダー名は lowwercase ヘッダー名でアルファベット順にソート 必須要素 host Content-Type (request header で指定している場合) request header に指定している x-amz-* header x-amz-content-sha256 は 全ての AWS Signature Version 4 requestsで必要 payload (body) を hash 化した値を入れる 値がない場合は空文字を hash 化 ※ 最後にも \n が必要なので注意 例) header が以下の場合 Host:s3.amazonaws.com X-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b785 2b855 X-amz-date:20130708T220855Z code "Host".toLowerCase() + ":" + host.trim() + "\n" + "X-amz-content-sha256".toLowerCase() + ":" + xAmzContentSha256.trim() + "\n" + "X-amz-date".toLowerCase() + ":" + xAmzDate.trim() + "\n" 値 host:s3.amazonaws.com x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b785 2b855 x-amz-date:20130708T220855Z // 末尾にも改行が入っている SignedHeaders CanonicalHeaders のヘッダー名のリスト。以下の条件を満たす必要がある。 ; で区切る ヘッダー名は lowwercase ヘッダー名でアルファベット順にソート 例)host, x-amz-content-sha256, x-amz-date の場合 値 host;x-amz-content-sha256;x-amz-date HashedPayload request payload(body) を sha256 で hash 化した値を16進数で表した値 例)crypto モジュールを使った場合 code crypto .createHash("SHA256") .update(payload) .toString(16) コード 完成コードの中の、下記の部分が canonical request の作成をしています。 const crypto = require("crypto") const BUCKET = process.env.BUCKET const HASH_ALGORHYZM = "SHA256" class S3Service { (中略) createCanonicalRequest(params) { const requests = [] const hasquery = params.query && typeof params.query === "object" const method = this.getMethod(params.method) requests.push(method) const canonicalUri = this.getCanonicalUri(params.filepath, hasquery) requests.push(canonicalUri) const canonicalQueryString = this.getCanonicalQueryString(params.query, hasquery) requests.push(canonicalQueryString) const canonicalHeaders = this.getCanonicalHeaders(params.headers) requests.push(canonicalHeaders) const signedHeaders = this.getSignedHeaders(params.headers) requests.push(signedHeaders) const hashedPayload = this.getHashedPayload(params.payload) requests.push(hashedPayload) return requests.join("\n") } (中略) getMethod(method) { const [isValid, msg] = this.validateMethod(method) if(!isValid) throw Error(msg) return method.toUpperCase() } getCanonicalUri(filepath, hasquery) { const [isValid, msg] = this.validateFilepath(filepath) if(!isValid) throw Error(msg) const uri = `/${BUCKET}/${filepath}` return hasquery ? uri + "?" : uri } getCanonicalQueryString(query, hasquery) { if(!hasquery) return "" const [isValid, msg] = this.validateQuery(query) if(!isValid) throw Error(msg) const queryStrings = this.objectToString(query, "=") return encodeURI(queryStrings.join("&")) } getCanonicalHeaders(header) { const [isValid, msg] = this.validateHeader(header) if(!isValid) throw Error(msg) const headerStrings = this.objectToString(header, ":") return headerStrings.join("\n") + "\n" } getSignedHeaders(header) { const [isValid, msg] = this.validateHeader(header) if(!isValid) throw Error(msg) return Object.keys(header).map(k => k.toLowerCase()).sort().join(";") } getHashedPayload(payload) { return this.hashHex(payload) } (中略) objectToString(query, sep) { const _query = {} Object.keys(query).forEach(k => { _query[k.toLowerCase()] = query[k] }) const queryStrings = [] Object.keys(_query).sort().forEach(k => { queryStrings.push(k + sep + _query[k]) }) return queryStrings } _hash(data) { return crypto.createHash(HASH_ALGORHYZM).update(data) } hashHex(data) { return this._hash(data).digest("hex") } } 署名に必要な文字列の作成 フォーマット StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest Algorithm AWS Signature Version 4 では AWS4-HMAC-SHA256 を使用します。 RequestDateTime ISO8601に準拠したフォーマットを用います。(- がないもの) https://ja.wikipedia.org/wiki/ISO_8601 "AWS4-HMAC-SHA256" + "\n" + timeStampISO8601Format + "\n" + <Scope> + "\n" + Hex(SHA256Hash(<CanonicalRequest>)) Scope スコープは以下のようなフォーマットになっている。 date.Format(<YYYYMMDD>) + "/" + <region> + "/" + <service> + "/aws4_request" 例)2021/10/05、東京リージョンの場合 20211005/ap-northeast-1/s3/aws4_request Hex(SHA256Hash()) canonical request を SHA256 でハッシュ化し、16進数で表したもの コード 完成コードの中の、下記の部分が 署名に必要な文字列の作成をしています。 S3Service.js class S3Service { constructor() { this.requestDate = new Date() }    (中略) createStringToSign(params) { const formattedDate = this.formatRequestDate("ISO8601") const scope = this.getScope() const canonicalRequest = this.createCanonicalRequest(params) const signStrings = [ "AWS4-HMAC-SHA256", formattedDate, scope, this.hashHex(canonicalRequest) ] return signStrings.join("\n") } createCanonicalRequest(params) { (中略) } (中略) hashHex(data) { (中略) } formatRequestDate(type) { const Y = this.requestDate.getUTCFullYear() const M = this.requestDate.getUTCMonth() + 1 const D = this.requestDate.getUTCDate() const h = this.requestDate.getUTCHours() const m = this.requestDate.getUTCMinutes() const s = this.requestDate.getUTCSeconds() const YYYY = Y.toString().padStart(4, "0") const MM = M.toString().padStart(2, "0") const DD = D.toString().padStart(2, "0") const hh = h.toString().padStart(2, "0") const mm = m.toString().padStart(2, "0") const ss = s.toString().padStart(2, "0") switch(type) { case "YYYYMMDD": return `${YYYY}${MM}${DD}` case "ISO8601": return `${YYYY}${MM}${DD}T${hh}${mm}${ss}Z` } } } 署名の作成 フォーマット 以下のようなフォーマットになっています。最終的に Signature を作成します。 DateKey = HMAC-SHA256("AWS4"+"<SecretAccessKey>", "<YYYYMMDD>") DateRegionKey = HMAC-SHA256(<DateKey>, "<aws-region>") DateRegionServiceKey = HMAC-SHA256(<DateRegionKey>, "<aws-service>") SigningKey = HMAC-SHA256(<DateRegionServiceKey>, "aws4_request") Siganture = HMAC-SHA256(SigningKey, StringToSign) HMAC-SHA256 は SHA256 をハッシュ関数に使用して HMAC を計算しています。簡単に HMAC の説明をすると、HMAC はメッセージ認証コードの計算方法の一つで、計算にハッシュ関数と共通鍵を持ちます。 HMAC-SHA256 をコードで表すと以下のようになります。 code function hmac(key, data) { return crypto.createHmac("SHA256", key).update(data).digest() } コード 完成コードの中の、下記の部分が 署名の作成をしています S3Service const crypto = require("crypto") const axios = require("axios").default const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY const BUCKET = process.env.BUCKET const REGION = process.env.REGION const AWS_SERVICE = "s3" const HASH_ALGORHYZM = "SHA256" class S3Service { (中略) createSignature(params) { const formattedDate = this.formatRequestDate("YYYYMMDD") const dateKey = this.hmac("AWS4" + AWS_SECRET_KEY, formattedDate) const dateRegionKey = this.hmac(dateKey, REGION) const dateRegionServiceKey = this.hmac(dateRegionKey, AWS_SERVICE) const signingKey = this.hmac(dateRegionServiceKey, "aws4_request") const sign = this.createStringToSign(params) return this.hmacHex(signingKey, sign) } createStringToSign(params) { (中略) } (中略) _hmac(secretKey, data) { return crypto.createHmac(HASH_ALGORHYZM, secretKey).update(data) } hmac(secretKey, data) { return this._hmac(secretKey, data).digest() } hmacHex(secretKey, data) { return this._hmac(secretKey, data).digest("hex") } formatRequestDate(type) { (中略) } } 署名を含む authorization ヘッダーの作成 上記で作成したものを authorization ヘッダーに設定すれば s3 からデータを取得することができます。 フォーマット Algorithm Credential=<Credential>,SignedHeaders=<SignedHeaders>,Signature=<Signature> Algorithm こちらと同様、AWS4-HMAC-SHA256 となります。 Credential 以下のようなフォーマットになっています。 AWS_ACCESS_KEY は aws の iam ユーザーのアクセスキーになっています。 Scope はこちらで説明した通りです。 <AWS_ACCESS_KEY>/<Scope> // 例 AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request SignedHeaders SignedHeaders はこちらで説明した通りです。 Signature Signature はこちらで説明したものを使います。 コード 完成コードの中の、下記の部分が署名を含む authorization ヘッダーの作成をしています。 S3Service const crypto = require("crypto") const axios = require("axios").default const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY const BUCKET = process.env.BUCKET const REGION = process.env.REGION const AWS_SERVICE = "s3" const HOST = `${AWS_SERVICE}.${REGION}.amazonaws.com` const ALLOWED_HTTP_METHOD = ["GET", "PUT", "POST", "DELETE"] const HASH_ALGORHYZM = "SHA256" class S3Service { (中略) createAuthorization(params) { const credential = this.getCredential() const signedHeaders = this.getSignedHeaders(params.headers) const signature = this.createSignature(params) return `AWS4-HMAC-SHA256 Credential=${credential},SignedHeaders=${signedHeaders},Signature=${signature}` } createSignature(params) { (中略) }    (中略) getScope() { const formattedDate = this.formatRequestDate("YYYYMMDD") return `${formattedDate}/${REGION}/${AWS_SERVICE}/aws4_request` } (中略) getCredential() { const scope = this.getScope() return `${AWS_ACCESS_KEY}/${scope}` } getSignedHeaders(header) { return Object.keys(header).map(k => k.toLowerCase()).sort().join(";") } } authorization ヘッダーを設定して http リクエスト やっていることは以下の通りです。 setRequestDate で リクエストに使う共通の date の値を生成 url を取得 ヘッダーを設定 CanonicalHeaders で設定したもの authorization で作成したもの http リクエスト const axios = require("axios").default const BUCKET = process.env.BUCKET const AWS_SERVICE = "s3" const HOST = `${AWS_SERVICE}.${REGION}.amazonaws.com` class S3Service {   constructor() { this.requestDate = new Date() } async fetchObject(params) { this.setRequestDate() const url = this.getUrl(params.filepath) const headers = this.getHeader(params) const res = await axios.get(url, { headers }) return res } createAuthorization(params) { (中略) } (中略) getUrl(filepath) { return `https://${HOST}/${BUCKET}/${filepath}` } getEssentialHeaders(params) { return { host: HOST, "x-amz-content-sha256": this.hashHex(params.payload), "x-amz-date": this.formatRequestDate("ISO8601") } } getHeader(params) { const essentailHeaders = this.getEssentialHeaders(params) params.headers = Object.assign(essentailHeaders, params.headers) const authorization = this.createAuthorization(params) return Object.assign({ authorization }, params.headers) } setRequestDate() { this.requestDate = new Date() } } これで、AWS Signature Version 4 による署名を用いて s3 からデータを取得することができます!

Viewing all articles
Browse latest Browse all 9299

Trending Articles