Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Spring/Jackson】Rest API : JSON RequestBodyのプロパティをEnum型としてマッピングする

f:id:rennnosukesann:20181220183020p:plain

メモ。

SpringのControllerでは、 @RequestBody を使用することでHTTPリクエストボディに設定されたパラメータをJSONで受け取ることができます。

@RestController
public class FluitController {
    // Request Body にJSONを指定するAPI
    @RequestMapping(value = "/fluits", method = RequestMethod.POST)
    public FluitResponse post(@RequestBody FluitRequest req) {
        ....
    }
}

このとき、 HogeRequest が以下のような構造で定義されているとします。

@Data
public class FluitRequest {
    private String name;
    private Integer price;
    private Integer category;
}

FluitRequest::category は以下の値域のみとり得るとします

HogeRequest req = new HogeRequest();
req.setCategory(1); // 1 : りんご
req.setCategory(2); // 2 : みかん
req.setCategory(3); // 3 : ぶどう

FluitRequest::category のような有限集合の場合、Javaであれば列挙型を使用して定義したいところです。

public enum FluitCategory {
    APPLE, ORANGE, GRAPE
}

FluitRequestAPI /fluits のPOSTリクエストボディがマッピングされるとき、category プロパティの値がそのまま FluitCategory 列挙型としてマッピングされたら便利ですね。

Jacksonが提供する JsonDeserializer を拡張定義することで、独自のマッピングを実装することができます。これを利用し、リクエストボディマッピング用クラス FluitRequest に型としてFluitCategory を持つプロパティを定義できるようにします。


まず、列挙型 FluitCategory を以下のように整数型IDとの紐付けます。

// 各列挙型オブジェクトがidを持つ
@Getter
public enum FluitCategory {
    APPLE(1), ORANGE(2), GRAPE(3);

    private int id;

    private FluitCategory(int id) {
        this.id = id;
    }

    public static FluitCategory value(int id) {
        // idとマッチするFluitCategoryオブジェクトがない場合独自例外を送出
        return Arrays.stream(values()).filter(x -> x.id == id).findFirst().orElseThrow(() -> new HogeException());
    }
}

次に、以下のような JsonDeserializer 拡張クラスを定義します。 拡張する JsonDeserializerジェネリクス型には変換先の型である FluitCategory を指定しています。 変換元の値は deserialize メソッドの引数 jsonParser から取得できます。 今回は元々整数型の値であった category を変換したかったので、 getIntValue で値を取得します。この値を元に FluitCategory::value()FluitCategory に変換し返り値とします。


public class FluitCategoryDeserializer extends JsonDeserializer<FluitCategory> {

  @Override
  public FluitCategory deserialize(JsonParser jsonParser, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
    return FluitCategory.value(jsonParser.getIntValue());
  }

}

最後に、@Bean として以下のような ObjectMapper を定義します。 ここで定義されたObjectMapperがRequest BodyのJSONパース時に使用され、クライアントから整数型で送られたプロパティを FluitCategory 型プロパティとしてJava側で受け取った際に変換が実行されるようになります。

@Configuration
public class JsonConfiguration {

  @Bean
  public ObjectMapper jsonObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    SimpleModule simpleModule = new SimpleModule();
    simpleModule.addDeserializer(FluitCategory.class, new FluitCategoryDeserializer());
    mapper.registerModule(simpleModule());
    return mapper;
  }

【Spring】オリジナルの `.properties` ファイルを作成・読み込み

f:id:rennnosukesann:20181220183020p:plain

メモ。

Spring では application.properties 以外に独自の .properties を生成し、その中で定義した値を使用することができます。

オリジナル .properties ファイルの作成とプロパティの参照

例えば、オリジナルのプロパティファイル myconfig.properties を以下のように作成したとします。

myconfig.properties
config.hoge=1
config.fuga='https://www.fuga.com'

config.piyo.foo='foo.'
config.piyo.bar=2

このプロパティファイルの設定をSpring内のコードにマッピングするための、 以下のようなクラスを定義します。

@Configuration
@PropertySource("classpath:myconfig.properties")
@ConfigurationProperties(prefix = "config")
@Getter
public final class MyConfig {

    public static class Piyo {
        private String foo;
        private Integer bar;
    }
    
    private Integer hoge;
    private String fuga;
    private Piyo piyo

}

@PropertySource

指定したクラスにどのプロパティファイルの内容をマッピングするのかを指定します。 上の例では MyConfig クラスに myconfig.properties ファイルをマッピングするよう指定しています。 classpath: はクラスパス配下であることを表し、 myconfig.properties がクラスパスを通したディレクトリ下にあれば良いことを示しています。

@ConfigurationProperties

myconfig.properties でどのようなプレフィックスを受け入れるかを設定しています。ここでは config を指定しているので、MyConfigクラスはmyconfig.properties の各パラメータ名の前に config. を付けることを要求します。

マッピングの定義が完了したMyConfigクラスには @Configuration (@Conponentが付加されたアノテーション)が付加されているので、下記のように他コンポーネントへDIによるインスタンス化を行うことができます。

@Service
public class HogeService {

 private static final Logger logger = LoggerFactory.getLogger(LogUtils.class);

    @Autowired
    private MyConfig config;

    public void printFuga() {
        logger.info(config.getFuga());
    }

}

環境変数を参照する

下記のように @PropertySource に渡すvalue値には外部環境変数への参照を記述することができます。

@PropertySource("classpath:myconfig-${ENV}.properties") // 環境変数を追

これにより、例えばデプロイ環境によって適用したいカスタム properties ファイルの切り替えを行うことが可能です。

参考文献

PropertySource (Spring Framework 5.1.3.RELEASE API)

Configuration (Spring Framework 5.1.3.RELEASE API)

【Angular】Angular CLIでライブラリの追加・ビルドを行う

f:id:rennnosukesann:20181224221629p:plain

Angular CLI

詳細は下記記事にて。

rennnosukesann.hatenablog.com

ng add : 外部ライブラリを追加する

ng add を使用すると、Angularプロジェクトへの外部ライブラリの追加を行えます(複数指定可能)。 Installing packages for tooling via npm. の文言から、結局 npm 経由でパッケージインストールしていることがわかります。

$ ng add @angular/material @angular/cdk @angular/animations
Installing packages for tooling via npm.

package.json にも依存関係が記述されていることがわかります。

$ cat package.json | grep -E "material|cdk|animations" 
    "@angular/animations": "~7.1.0",
    "@angular/cdk": "~7.2.0",
    "@angular/material": "^7.2.0",

ng build : アプリをビルドする

ng build でAngularプロジェクトをビルドし、 /dist ディレクトリ配下にビルド済みファイルを出力します。

$ ng build
                                                                                          
Date: 2018-12-24T16:20:03.523Z
Hash: bb05af5cc5e75024ef83
Time: 13417ms
chunk {main} main.js, main.js.map (main) 10.9 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 223 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 179 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.69 MB [initial] [rendered]

 $ ls dist
my-angular-app

ng update : Angularプロジェクト及び依存ライブラリのバージョンをアップデートする

ng update コマンドを実行すると、Angularアプリ及びアプリが依存するライブラリのバージョンをアップデートします。

$ ng update
    We analyzed your package.json and everything seems to be in order. Good work!

参考文献

Angular Docs

【webpack】webpackを使用してWebアプリケーションをバンドルする

f:id:rennnosukesann:20181225231948p:plain:w600

webpackとは

webPackとはモジュールバンドラと呼ばれるツールです。アプリーケーションが依存するjsモジュールを一つのjsファイルにまとめ上げ、依存関係の解決やトランスパイルを自動実行してくれます。内部的にモジュール同士の依存関係を表すグラフを自動で構築し、それに基づいて依存関係の解決をしてくれるので、設定ファイルなどなしに使用することができます(詳細な設定を記述したファイルを使用することもできます)。

・・・とはいったものの、「モジュールのバンドル」がいまいちピンと来なかったので、こちらチュートリアルを実践してみました。

以下、チュートリアルのメモになります。

webpackのインストール

まず、webpack対象となる node プロジェクトを作成します。

$ mkdir hello-webpack
$ cd hello-webpack
$ npm init -y

npm でwebpackと、webpackのCLIツールをインストールします。

$ npm install webpack webpack-cli --save-dev

webpackを試しに使用する

実験環境準備

webpackを使用する前に、テスト用のモジュールを作ってみます。

最初のディレクトリ構成は以下の通り。

$ ls
node_modules        package.json
package-lock.json

プロジェクトルートに以下のような index.html を作成します。HTMLを見ると、 index.html<script> タグ中でCDNによって茶刈 lodash.js スクリプトをロードしていることがわかります。

<!doctype html>
<html>
  <head>
    <title>Hello, webpack !!1!</title>
    <script src="https://unpkg.com/lodash@4.16.6"></script>
  </head>
  <body>
    <script src="./src/index.js"></script>
  </body>
</html>

次に内部ロジック記述用スクリプトである src/index.js を用意します。このスクリプトでは、先程 index.html でロードしていた lodash モジュールを使用しようとしているとします。が、 lodash に対する依存関係が暗黙的で、 _ を未定義のグローバル変数として参照してしまっています。

function component() {
  let element = document.createElement('div');
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  return element;
}

document.body.appendChild(component());

上記の index.js のような、参照できているようで参照できていない暗黙の依存関係は様々なバグを生んでしまいます。 このようなバグをなくすため、lodash をnpmでのパッケージインストール & import に変更してしまいましょう!

npm での lodash のインストール

先程用意した index.html を、新たに生成した /dist ディレクトリ配下に配置します。

dist
└── index.html

次に、 lodash をnpm経由でインストール。

$ npm install --save lodash

src/index.js を修正します。明示的に lodash を参照できるよう、下記一文をファイル先頭に追加します。

import _ from 'lodash';

dist/index.html を編集します。 <script> タグによる lodash のロードをやめ、 参照先スクリプトmain.js としました。これは後ほど使用するwebpackのバンドル後ファイルへの参照です。

webpackによるモジュールバンドル

上記の import 文によって、webpackが依存関係のグラフを自動作成可能になりました。

それでは、実際にwebpackでモジュールバンドルを行います。

npx コマンドを使用し、 webpack を実行します。

npx についてはこちらを参照。

rennnosukesann.hatenablog.com

$ npx webpack 
Hash: 53680323beef520a74cd
Version: webpack 4.28.2
Time: 445ms
Built at: 2018/12/25 23:10:03
  Asset      Size  Chunks             Chunk Names
main.js  1.03 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 189 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

webpackが成功し、 dist/main.js が生成されました!

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){document.body.appendChild(function(){let e=document.createElement("div");return e.innerHTML=_.join(["Hello","webpack"]," "),e}())}])

どうやら「モジュールのバンドル」 とは

といった操作のようです。


webpackを使ってみての感想ですが、「アプリの最適化」を行うのには素晴らしいツールだなと思いました。 アプリの複雑な依存関係を解決し一つのファイルにアプリのスクリプトをまとめることで、ビルド・デプロイするアプリを軽量化・高速化できるので、アプリの総仕上げとして良いのではないでしょうか。 (多くのフロントエンドCLIでもデフォルトで採用されているみたいですね)

参考文献

webpack - Getting Started

【Angular】Angular CLIで簡単にAngularプロジェクトの作成・デプロイ・テストを自動化する

f:id:rennnosukesann:20181224221629p:plain

Angular CLI

Angular CLIGoogleが提供するフロントエンドフレームワークAngularのためのCLIツールです。Angularプロジェクトの作成やデプロイ、テスト自動化などをより簡単に実行することができます。

インストール

npm でインストールします。

$ npm install -g @angular/cli

2018/12/24現在のバージョンだと7系のCLIがインストールされると思います。

$ ng --version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 7.1.4
Node: 10.3.0
OS: darwin x64
Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.11.4
@angular-devkit/core         7.1.4
@angular-devkit/schematics   7.1.4
@schematics/angular          7.1.4
@schematics/update           0.11.4
rxjs                         6.3.3
typescript                   3.1.6
    

ng new : プロジェクトを作成する

ng new でAngularプロジェクトディレクトリを作成することができます。

 $ ng new

コマンドを実行すると、対話形式で下記項目について聞いてくるので、それぞれ入力or選択。

  • プロジェクト名
  • Angularルーティングの有効化
  • スタイルシートのフォーマットの選択
? What name would you like to use for the new workspace and initial project? my-angular-app
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ http://sass-lang.com   ]

これでプロジェクトは完成です。

$ cd my-angular-app
$ ls
README.md       e2e         package-lock.json   src         tslint.json
angular.json        node_modules        package.json        tsconfig.json

生成したプロジェクトには、既にテスティングフレームワークやLinterなどのモジュールがインストールされています。Angularが2系より採用したTypeScriptも確認できます。

$ cat package.json
{
  "name": "my-angular-app",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~7.1.0",
    "@angular/common": "~7.1.0",
    "@angular/compiler": "~7.1.0",
    "@angular/core": "~7.1.0",
    "@angular/forms": "~7.1.0",
    "@angular/platform-browser": "~7.1.0",
    "@angular/platform-browser-dynamic": "~7.1.0",
    "@angular/router": "~7.1.0",
    "core-js": "^2.5.4",
    "rxjs": "~6.3.3",
    "tslib": "^1.9.0",
    "zone.js": "~0.8.26"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.11.0",
    "@angular/cli": "~7.1.4",
    "@angular/compiler-cli": "~7.1.0",
    "@angular/language-service": "~7.1.0",
    "@types/node": "~8.9.4",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~3.1.1",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~7.0.0",
    "tslint": "~5.11.0",
    "typescript": "~3.1.6"
  }
}

ng serve : Angularプロジェクトからアプリをローカルにデプロイする

ng serve で、ローカルサーバを立ち上げアプリをデプロイできます。

$ cd my-angular-app
$ ng serve
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
                                                                                          
Date: 2018-12-24T13:31:23.110Z
Hash: 5c43a53e1dad2b4bcc2c
Time: 11797ms
chunk {main} main.js, main.js.map (main) 11.5 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 223 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 16.6 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.67 MB [initial] [rendered]
ℹ 「wdm」: Compiled successfully.

http://localhost:4200/ にアクセスすると、以下のようなデフォルトアプリが表示されます。

f:id:rennnosukesann:20181224223348p:plain

ng generate : プロジェクトに必要なモジュールを追加する

ng generate を使用すると、classやdirective、serviceといったモジュールを特定のディレクトリ配下(デフォルトで src/apps )に生成してくれます。

 $ ng generate class HttpHelper
CREATE src/app/http-helper.ts (28 bytes)
$ ls src/app/http-helper.ts 
src/app/http-helper.ts

--force -f をつけると、既存のファイルを上書きして生成します。

$ ng generate class HttpHelper --force

ng lint : linterでコードを検査する

ng lint を実行すると既存のソースコードに対しLinterが起動し、lintの設定に則ったルールのもと文法ミスやアンチパターンを指摘してくれます。

 $ ng lint
Linting "my-angular-app"...


All files pass linting.
Linting "my-angular-app-e2e"...


All files pass linting.

ng test : 単体テスト実行

ng test で、プロジェクト内で単体テストを実行します。 デフォルトでは、テストランナーとしてKarmaによるテストが実行されます。 なおテストコード自体にはデフォルトでテスティングフレームワークJasmineが使われているようです。

$ ng test
 11% building modules 9/9 modules 0 active24 12 2018 22:47:10.440:WARN [karma]: No captured browser, open http://localhost:9876/
24 12 2018 22:47:10.447:INFO [karma-server]: Karma v3.1.4 server started at http://0.0.0.0:9876/
24 12 2018 22:47:10.447:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
24 12 2018 22:47:10.464:INFO [launcher]: Starting browser Chrome
24 12 2018 22:47:18.729:WARN [karma]: No captured browser, open http://localhost:9876/    
24 12 2018 22:47:19.418:INFO [Chrome 71.0.3578 (Mac OS X 10.14.1)]: Connected on socket U_QKpHniSB3g0GIZAAAA with id 75357283
Chrome 71.0.3578 (Mac OS X 10.14.1): Executed 3 of 3 SUCCESS (0.288 secs / 0.245 secs)
TOTAL: 3 SUCCESS
TOTAL: 3 SUCCESS

f:id:rennnosukesann:20181224224926p:plain

Karmaについてはこちらの記事参照。

rennnosukesann.hatenablog.com

ng e2e E2Eテスト実行

ng e2e で、E2Eテストが開始されます。

$ ng e2e
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
                                                                                          
Date: 2018-12-24T13:51:46.733Z
Hash: 5c43a53e1dad2b4bcc2c
Time: 11117ms
chunk {main} main.js, main.js.map (main) 11.5 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 223 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 16.6 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.67 MB [initial] [rendered]
[22:51:47] I/file_manager - creating folder /Users/aa367417/hoge/my-angular-app/node_modules/protractor/node_modules/webdriver-manager/selenium
[22:51:47] I/config_source - curl -o/Users/aa367417/hoge/my-angular-app/node_modules/protractor/node_modules/webdriver-manager/selenium/chrome-response.xml https://chromedriver.storage.googleapis.com/
ℹ 「wdm」: Compiled successfully.
[22:51:48] I/downloader - curl -o/Users/aa367417/hoge/my-angular-app/node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.45.zip https://chromedriver.storage.googleapis.com/2.45/chromedriver_mac64.zip
[22:51:49] I/update - chromedriver: unzipping chromedriver_2.45.zip
[22:51:49] I/update - chromedriver: setting permissions to 0755 for /Users/aa367417/hoge/my-angular-app/node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.45
[22:51:50] I/launcher - Running 1 instances of WebDriver
[22:51:50] I/direct - Using ChromeDriver directly...
Jasmine started

  workspace-project App
    ✓ should display welcome message

Executed 1 of 1 spec SUCCESS in 2 secs.
[22:51:58] I/launcher - 0 instance(s) of WebDriver still running
[22:51:58] I/launcher - chrome #01 passed

簡単ですが、Angular CLIをいじってみました。 既に使用しているVue CLIと比較すると、フレームワークフルスタックなのもあり、プロジェクト生成時に入れるパッケージに悩まなくていいですね。

参考文献

Angular CLI

github.com

【Node.js】npxでローカルにパッケージを一時的にインストールして実行する

f:id:rennnosukesann:20181223181823p:plain

npx

npx はローカルにインストールしたnpmパッケージバイナリを直接実行できるコマンドです。npm5.2.0より導入されました。

このnpx にはパッケージを一時的にだけインストールしてその機能を実行する機能が備わっているので、 今回はその機能について紹介します。

npx のインストール

まずは npm 自体のアップデートを行います。

$ npm install -g npm 

5.2.0以上であることを確認。

$ npm --version
6.2.0

確認できたら、 npx をインストールします。

$ npm install -g npx

インストールしていないパッケージを実行

ローカルにインストールしていないパッケージを実行することができる npx ですが、一旦はパッケージのインストールが走ります。 が、コマンド実行後インストールされたままにならず削除されるので、ローカルの node_modulespackage.json を汚しません。

以下はAngular CLI のバージョンチェックを実行している例です。 

$ npx @angular/cli --version


     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 7.1.4
Node: 8.11.3
OS: darwin x64
Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.11.4
@angular-devkit/core         7.1.4
@angular-devkit/schematics   7.1.4
@schematics/angular          7.1.4
@schematics/update           0.11.4
rxjs                         6.3.3
typescript                   3.1.6

実行したパッケージのコマンドを実行してみるとできないことがわかります。 パッケージがインストールされず、そのまま破棄されていることがわかります。

# インストールはされていない
$ ng
zsh: correct 'ng' to 'bg' [nyae]? n

npmパッケージを手軽に、試しに実行したい場合などに便利です。

参考文献

www.npmjs.com

【IBM COS】IBM Cloud Object Storageで署名付きURLを発行する

f:id:rennnosukesann:20181216025439p:plain:w200

IBM Cloud Object Storageとは

IBM Cloud Object Storage(COS)はIBMクラウド上で提供するストレージサービスです。IBM Cloud上でAWSのS3のようなオブジェクトベースでリソースを保存できるストレージ環境を提供します。IBM Cloud上でサービスとして提供されているので、クラウド上に立ち上げたアプリケーションサーバと連携して画像や音声の保存用ストレージとして利用する・・・といったことが可能です。それだけでなく、オンプレミス環境でホスト型クラウド環境を構築することでもCOSを利用することができます。

COSが使われるシーン

多くの場合、Webアプリケーションやシステムで使われるDB(RDBMS)では、文字列や数値といったデータと、それらのデータからなるいわゆる構造化データが扱われます。例えばRDBの場合、テーブルに挿入されるレコードが構造化データになります。このような構造化データはスキーマによって明確に定義されるため、RDBで管理しやすい傾向にあります。

しかし、画像や音声といったいわゆる非構造化データは、構造化されたデータを扱うシステムとはあまり相性がよくありません。例えば画像バイナリをRDBに保存する場合、数MBに及ぶバイナリを文字列(Base64など)に変換してどこかのテーブルの1カラムの値として保存する、といったことなどをやらなければいけません。このような実装は、RDBの設計にいびつな構造をもたらします。

そこで、そのような非構造化データは一旦別の場所に追いやり、そのデータへのアクセスポイントをRDBレコードのカラム値として保存するやり方が考えられます。そのような場合にCOSが威力を発揮します。

COSはKVSライクにデータを保存するオブジェクトベースのストレージです。特定のオブジェクトデータに一意なキーを対応付け、それをもとにアクセスポイントを発行してデータの更新/参照を可能にします。例えば画像であれば、COSに保存した画像のオブジェクトキーからURLを発行することができます。

IBM COSの署名付きURL

COSに保存したリソースを公開するとき、特定サービスのユーザに対してのみ限定公開したい場合があると思います。COSのエンドポイントをprivateにする方法もありますが、スマートフォンなどパブリックなネットワークに接続するクライアントに対してデータを提供したい場合など、適用しにくい場合も考えられます。

そのような場合、署名付きURLが使えます。COSの設定でエンドポイントに対するアクセスを原則禁止としつつ、署名付きのURL経由でのみリソースにアクセスさせるといったことが可能なため、パブリックなネットワークに接続するクライアントに対してもリソースの提供に対応できます。

署名付きURLの発行

AWS CLI

署名付きURLは、AWS CLIを利用して発行することができます。 Macの場合、 Homebrewでインストールすることができます。

IBM Cloud Docs

$ brew install awscli

Homebrew、およびAWS CLIのインストールには下記も参考にしてください。

brew.sh

docs.aws.amazon.com


インストール後、aws configure でCOSエンドポイントの設定を行います。

$ aws configure
AWS Access Key ID [None]: {Access Key ID}
AWS Secret Access Key [None]: {Secret Access Key}
Default region name [None]: {Provisioning Code}
Default output format [None]: json

設定後、下記コマンドを実行することで署名付きURLを発行できます。 --endpoint-url には署名付きURLを跛行したいしたいリソースのCOSエンドポイントを、 {bucketName} にはリソースのバケット名、 {objKey} にはオブジェクトキーを入力してください。

$ aws --endpoint-url=https://{endpoint} s3 presign s3://{bucketName}/{objKey}

バケットはCOS内で一意な保存領域を表し、複数のバケットを一つのCOSサービス内に生成することでリソースの保存領域を論理的に分割することができます。オブジェクトキーはCOS内リソースを一意に識別するためのキーになります。

また、下記のように署名付きURLには有効期限を任意に設定することができます。

$ aws --endpoint-url=https://{endpoint} s3 presign s3://bucket-1/new-file --expires-in 604800000

Java

COSのSDKを利用して、プログラム中でも署名付きURLを発行することができます。 今回はJavaを題材としてコード例を示します。

build.gradle
dependencies {
    ...  
    implementation('com.ibm.cos:ibm-cos-java-sdk:latest.release')
    ...
}
Java
private String getPreSignedUrl(String bucketName, String objectKey) {

    // COS認証情報(実際の運用では環境変数等で外出ししてください)
    String accessKey = "XXXXXX";
    String secretKey = "XXXXXXXXX";
    String endpoint = "XXXXXXXXXX";
    String region = "XXXXXXXX";

    // 署名有効期限までの時間
    long signExpiration = 604800000; // 1week

    // 認証情報
    AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

    // クライアント設定
    ClientConfiguration clientConfig = new ClientConfiguration();
    clientConfig.setProtocol(Protocol.HTTPS); // プロトコル
    clientConfig.setConnectionTimeout(10000); // 接続タイムアウト(ms)

    // COSエンドポイント設定
    EndpointConfiguration endpointConfiguration = new EndpointConfiguration(endpoint, region);

    // クライアント生成
    AmazonS3 client = AmazonS3ClientBuilder.standard()
        .withCredentials(new AWSStaticCredentialsProvider(credentials)).withClientConfiguration(clientConfig)
        .withEndpointConfiguration(endpointConfiguration).build();

    // 署名有効期限
    Date expiration = new Date();
    long msec = expiration.getTime() + signExpiration;
    expiration.setTime(msec);

    // 署名つきURL生成
    GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectKey);
    generatePresignedUrlRequest.setMethod(HttpMethod.GET);
    generatePresignedUrlRequest.setExpiration(expiration);
    String url = client.generatePresignedUrl(generatePresignedUrlRequest).toString();
 
    return url;   
}

上記メソッドでは既にCOS上に保存されているリソースが置かれているバケット名と、リソースオブジェクトキーを受け取り、署名付きURLを生成しています。

署名付きURLによるリソースへのアクセス

生成した署名付きURLは以下のようになります(CredentialやSignatureなど、一部ぼかした表現にしています)。

https://hogehoge.us-south.objectstorage.softlayer.net/fugafuga.JPEG?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20181222T101615Z&X-Amz-SignedHeaders=host&X-Amz-Expires=604800&X-Amz-Credential=[Credential...]&X-Amz-Signature=[Signature...]

署名付きURLを生成すると、以下のようなパラメータがURLに付加されます。これらはURLに対する署名情報を表しています。

X-Amz-Algorithm ダイジェスト計算のためのハッシュアルゴリズム
X-Amz-Date URL生成時刻
X-Amz-SignedHeaders 署名対象ヘッダ情報
X-Amz-Expires URL署名有効期限
X-Amz-Credential 認証情報
X-Amz-Signature 署名

これら署名情報のないURLにアクセスするとHTTPステータスコード403が返され、「Access Denied」となりリソースにアクセスすることができません。

f:id:rennnosukesann:20181222193737p:plain

また署名付きURLの有効期限は任意に設定することができます。今回は有効期限を一週間としています。 有効期限を過ぎた署名付きエンドポイントにアクセスすると、下記のようなレスポンスが返され、リソースにアクセスできなくなります。

f:id:rennnosukesann:20181222193800p:plain

このように、リソースへのアクセス手段を署名付きURL経由で提供することで、COSに保存されたリソースをより安全に共有できるようになります。

参考文献

www.ibm.com

www.ibm.com

https://console.bluemix.net/docs/services/cloud-object-storage/hmac/presigned-urls.html#create-a-presigned-url