SPAとバックエンドの認証の話

このあたりは何がベストなんだろう、と常々考えていて、今回このアンケートを見ていいきっかけだと思ったので普段考えていることを吐き出しておく

まず分類

最初に以下の軸で分類する必要があると思う

  1. そのAPIは3rdパーティに公開する可能性があるか
  2. ユーザー管理はそのサービスだけで閉じているのか

3rdパーティに公開する可能性があるか

3rdパーティに公開する可能性がある場合、APIはOAuth2のリソースサーバーとして振る舞い、アクセストークンで保護する

その際、SPAアプリにアクセストークンやリフレッシュトークンを持たせたくないので、別途OAuth2のクライアントの役割をするバックエンドが欲しい(BFFと言っていいのか?)
SPAとBFFの間はcookieで繋ぐという形にする
( SPA -cookie-> BFF -access-token-> API
BFFはcookieに紐づけてアクセストークンやリフレッシュトークンを管理する

別にBFFとAPIは同じアプリケーションにまとめてもいいと思う

アクセストークンが必要なので、認証にはOAuth2の認可コードフローを使うことになる

ユーザー管理がそのサービス内で閉じない場合

これは

  • 別のサービスも公開していて(あるいは今後その予定)、そちらとIDを統合したい
  • ソーシャルログインしたい
  • 自分でパスワードとかを保有したくない(外部のIDaaSに任せたい)

などのパターンがあると思う

この場合OIDCを使って、認証は外部に移譲する
バックエンド側はOAuth2でいうところのクライアントの役割を担う

バックエンドは認可コードフローでIDトークンを受け取り、SPAとの間のセッションはcookieで維持する

両方に当てはまる場合

上2つを組み合わせる
( SPA -cookie-> BFF -access-token-> API
BFF側で認証時にIDトークンの検証を追加するだけ

どちらでもない場合

上記どちらにも当てはまらない場合は、OAuth2やOIDCを採用するのは過剰に感じる
ID/パスワードを受け付けてcookieを発行するエンドポイントをAPIに用意すればいいと思う

cookieについて

どのパターンでもcookieを使っているが、色々考えた結果cookieが最高、という結論に落ち着いた
結局SPAとバックエンドの間のセッションは自前で維持しなくてはいけないわけで、そのために毎回API実行時になにかしらのトークンを付与しないといけない
HTTPヘッダーにつけるのも選択肢なのだが、以下の3点でcookieに比べて劣位があると感じている

  • cookieならブラウザが自動でつけてくれる
  • javascriptから見れない(http only)のでXSSされても抜き取られない
  • 最初の受け渡しに困らない

特に3つ目が悩みポイントで、cookieを使わない場合、バックエンドからSPAへのトークンの受け渡しは

  • index.htmlとかにレンダリングしちゃう
  • SPAにリダイレクトする際にquery parameterで渡す

ぐらいしか方法が思いつかない

前者だとnextjsとか使ってSSR必須になるし、後者だとimplicitフローの問題点ふたたび、という感じがある
かといってそこをガチガチに固めていくと結局SPA側をクライアントにするのと何も変わらない、ということになる

さて、cookieを使おうとすると、SPAとバックエンドを同じドメインで配信しないといけない
以下の選択肢がとれる

  • バックエンドにSPAアプリも同梱する
  • 前段にwebサーバーをおいて特定のパス(/api/* など)だけバックエンドに流す

先日こちらもアンケートになっていた

別でデプロイはしたいんだけど、ドメインは同じにしたいので、個人的には後者を推す
webサーバーと書いたが、AWSで構築するならCloudFrontを使う

SPAと認可コードフローとcookie

じゃあSPAとバックエンドで認可コードフローってどんな感じにやるの?というのが次の問題になると思う
特にバックエンドにSPAを同梱しない場合

SPA, バックエンドそれぞれの視点でこんなことをすればいいと思っている

バックエンド視点

  • cookie検証用のエンドポイントを用意する
  • 認証開始エンドポイントを用意する
  • 認証開始エンドポイントが呼ばれたら認可コードフローを開始し、認証が完了したらSPAにリダイレクトする

SPA視点

  • ロードされたらcookie検証用エンドポイントを叩く
  • 401ならバックエンドの認証開始エンドポイントにリダイレクトする
  • 認証済みなら画面をレンダリングする


以上!

Javaでファイル操作(nio2)のユニットテストを書く

Javaでファイル操作をするときに、nio2とjimfsを使えばいい感じにファイル操作のユニットテストが書けることがわかったのでメモ

nio2
Javaファイル関連メモ2(Hishidama's Java Files Memo)

jimfs
Maven Repository: com.google.jimfs » jimfs
GitHub - google/jimfs: An in-memory file system for Java 7+

nio2はファイル操作を抽象化しており、zipやFTP先のサーバなども仮想的なファイルシステムとして利用できる
そのため、オンメモリな仮想ファイルシステムで差し替えることができれば、ローカルのファイルシステムを操作するコードも、
ローカルファイルを汚さずにテストすることができる

例:/rootdirectory/output/result.out ファイルに"Hello nio2"と出力するコード

プロダクトコード

public void method() {
    Path outputDirectory = Paths.get("/rootdirectory/output");
    if (!Files.exists(outputDirectory)) {
        Files.createDirectories(outputDirectory);
    }

    Files.write(outputDirectory.resolve("result.out"), "Hello nio2".getBytes());
}

テストコード
JMockitでモックしてファイルシステムを差し替えている

@Mocked
Paths paths;

@Test
public void 中間ディレクトリがなければ作ってファイルを作成する() throws IOException {

    try(FileSystem dummyFS = Jimfs.newFileSystem(Configuration.unix())) {

        // JMockitでPaths.getでファイルシステムを差し替える
        new NonStrictExpectations(){{
            Paths.get("/rootdirectory/output");
            result = dummyFS.getPath("/rootdirectory/output");
        }};
            
        testTarget.method();
        
        // 差し替えられたファイルシステムにファイルが作られているかアサーションする
        Path createdFilePath = dummyFS.getPath("/rootdirectory/output/result.out");
        assertThat(Files.exists(createdFilePath), is(true));
        assertThat(new String(Files.readAllBytes(createdFilePath)), is("Hello nio2"));
    }
}

以上

自前でnewしたオブジェクトにSpringのBeanをinjectする方法

タイトルの通りです。
トランザクションスクリプトではなくドメインモデル的に作ろうと思うと、ドメインオブジェクトの中からSpringコンテキスト配下のBeanにアクセスしたいケースが出てきます。
こうするとできるよ、というのがわかったので記録しておきます

AutowireCapableBeanFactory の autowireBeanを通せばいいようです

class Entity {
  @Autowired
  private SomeService someService;
}

@Component
class EntityFactory {
  @Autowired
  private AutowireCapableBeanFactory beanFactory;
  
  Entity create() {
    Entity entity = new Entity();
    beanFactory.autowireBean(entity);
    return entity;
  } 
}

chef事始め 依存関係の解決

前回の続き

cookbook・recipe間の依存関係

chefでサーバに適用するcookbookには、Chef Supermarketからダウンロードするコミュニティcookbookと自作cookbookとがあります。
ここを見ると、Environment cookbookをエントリポイントにして、wrapper cookbook→コミュニティcookbookと呼び出していくのが推奨されるやり方のようです。
ただ、具体的なやり方がよくわからなかったので悩みました。
自分なりに得た結論を以下に書いておきます。

なお、chef-serverを使うことは想定していません。
かといってchef-soloは今後開発が停止するようなのでこれも使用していません。
chef-zeroで実行することとします。
また、依存関係の解決には、Berkshelfを使用します。

例として、開発環境用にPostgreSQLApacheをインストールするとします。
cookbookとしては、以下の3つを用意します。

pj-dev (開発環境用)
pj-postgresql (対象プロジェクトでのPostgreSQL設定用)

自身で作成し、バージョン管理対象とするディレクトリ・ファイルは、
以下のような構成となります。

  • カレントディレクトリ

 - site-cookbooks
  - pj-dev
   - recipes
    - default.rb
   - Berksfile
   - metadata.rb
  - pj-postgresql
   - recipes
    - default.rb
   - Berksfile
   - metadata.rb

なお、chefDKの以下のコマンドをつかうと、必要なファイルをごそっと
作ってくれるので大変便利です。

chef generate cookbook pj-dev

上記で生成したcookbook達を上のディレクトリ構成に収めたら、
実際の処理を書いていきます。

まずは、各Berksfile から見ていきます。
Berksfileはberkshelfの設定ファイルです。
これを開くと、以下のようになっているかと思います。

source "https://supermarket.getchef.com"

metadata

1行目のsourceは、このサイトからコミュニティクックブックを落としてね、という意味。
3行目は、依存関係についてはmetadataを見てね、という意味になります。

というわけで次にmetadata.rbを見ます。
これを開くとcookbook名やらバージョン番号、ライセンスなど、自動生成された値が出てきます。
この辺は適宜書き換えてください。

依存関係は、このファイルの末尾に以下のように記載します。
site-cookbook/pj-postgresql/metadata.rb の場合

…
depends  'postgresql', '~> 3.4.14'

postgresql クックブックのv 3.4.14に依存しまっせという意味です

同じように、pj-dev クックブックのmetadata.rbにも次のように書きます。

…
depends  'pj-postgresql'

pj-postgresql クックブックに依存しまっせという意味です
ところがこれだけではberkshelfがpj-postgresqlをopscodeのサイトから探そうと
してしまうので、pj-postgresqlの場所を別途指定してあげる必要があります。

そのため、pj-devクックブックのBerksfileに以下を追記します。

cookbook 'pj-postgresql', path: '../pj-postgresql'

これで、metadata内にあるpj-postgresqlの場所が解決できます。
ほかに場所の指定方法として、gitリポジトリgithubも指定できます。

cookbook "mysql", git: "https://github.com/opscode-cookbooks/mysql.git", branch: "foodcritic"
cookbook "artifact", github: "RiotGames/artifact-cookbook", tag: "0.9.8"

設定が完了したところでberksfileを実行します。

berks vendor cookbooks -b site-cookbooks/pj-dev/Berksfile

berks vendor [PATH]はPATH以下にクックブックを展開する、というコマンドです。
-bオプションは実行するBerksfileのパスを指定します。

上記の例でこれを実行すると、cookbooksディレクトリに、
pj-dev、pj-postgresqlpostgresql、ならびにpostgresqlが依存するcookbook群が
ダウンロード&展開されます。

最終的にサーバで実行するのは、この展開されたcookbooksフォルダ以下のクックブックになります。
cookbooks以下はberks vendorされるたびにまるっと上書きされてしまうので、
クックブックの編集はsite-cookbooks以下で行い、その後berks vendorして実行という流れを忘れないようにしてください。

では、また次回

Chef事始め インストール

最近chefを始めました。
詰まったところがいくつかあったので書いておくことにします。

なお、Server-Client構成はやっていません。
chef-soloも将来的になくなるそうなので、chef-zeroでローカルモードで実行しています。
また、chefを実行するnodeはCentOS6.6です。

インストール

いきなりインストールでハマりました。
chefはruby1.9.3以上が必要なんですが、yumでは1.8.7しかインストールできないようでまず困りました。
rubyenvというツールでバージョン指定してインストールできるみたいなんですが、
いろいろ試してもうまくいかず。。。(自分がrubyに詳しくないのが問題なんですが)
で、途方にくれていたところ、chefDKというツール(しかもchef公式)の存在を知り、
試してみたところ、以下のコマンド一つであっさりインストールできました。

sudo rpm -ivh https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chefdk-0.3.6-1.x86_64.rpm

ChefDK公式

追記:chefdk0.4.0が出たようです。
今後もバージョンは上がっていくと思いますのでバージョン番号のところは適宜読み替えてください

これでchefと周辺ツール(BerkshelfやTestKitchenなど)を使うことができます。
rubyやgemやbundlerやなんやかんやで悩む必要がないのでぜひ。

長くなりそうなので次回に続く

テーマ決めました

今後勉強していくテーマを決めました。
簡単なウェブアプリを色々な言語、FWで作るです。

宣言することでやる気になろうという作戦です。

今のところ考えている環境は以下の通り。
SpringBoot
JavaEE
上記2つをRESTにしてJS-MVC(AngularJS)の組み合わせ
Groovy Grails
.netC#
Scala
NodeJS
cakePHP
ruby on rails

その中でTDDやアジャイル・CIツール、不変インフラなど興味のある分野に少しづつ触れていこう。

頑張れ俺

SAStrutsでFormのValidationのネスト

今仕事でSeasar2 with SAStrutsを使っているんですが、そこで不便に思ったのが、
1. Formに配列の要素があると各中身のValidationができない!
2. 入力情報の一部をひとまとめにして別Formとして親Formの中の一属性にした場合、その中身のValidationができない。
3. 1, 2の複合技(子Formの配列)

1,2,3ができるようにオレオレカスタマイザを作ってみたので、公開してみます。

yosshio0426/NestValidation · GitHub

もし同じお悩みを抱えている人がいたら、ちょっと見てみてください。
細かくテストしているわけではないので、使用の際はちゃんと中身を見て理解してから使ってみてください。
ちなみに3階層目までは保証しません。