SPAとバックエンドの認証の話
現在Spring BootでRESTなバックエンドを開発していて、かつフロントエンドのJavaScriptなどは別アプリとしてデプロイしている方に質問です。
— Tada🎉 (@suke_masa) 2023年4月27日
フロントエンド・バックエンド間の認証認可はどうしていますか?
このあたりは何がベストなんだろう、と常々考えていて、今回このアンケートを見ていいきっかけだと思ったので普段考えていることを吐き出しておく
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でいうところのクライアントの役割を担う
どちらでもない場合
上記どちらにも当てはまらない場合は、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/* など)だけバックエンドに流す
先日こちらもアンケートになっていた
現在Spring Bootで画面のあるWebアプリを開発していて、なおかつSpring側は全てREST+画面はJavaScriptフレームワークで作っている方に質問です。.jsファイルなどの静的コンテンツはどうしていますか?
— Tada🎉 (@suke_masa) 2023年4月24日
1️⃣JARに含めて同一アプリとしてデプロイしている
2️⃣別のアプリとしてデプロイしている
別でデプロイはしたいんだけど、ドメインは同じにしたいので、個人的には後者を推す
webサーバーと書いたが、AWSで構築するならCloudFrontを使う
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を使用します。
例として、開発環境用にPostgreSQLとApacheをインストールするとします。
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-postgresql、postgresql、ならびに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やなんやかんやで悩む必要がないのでぜひ。
長くなりそうなので次回に続く
SAStrutsでFormのValidationのネスト
今仕事でSeasar2 with SAStrutsを使っているんですが、そこで不便に思ったのが、
1. Formに配列の要素があると各中身のValidationができない!
2. 入力情報の一部をひとまとめにして別Formとして親Formの中の一属性にした場合、その中身のValidationができない。
3. 1, 2の複合技(子Formの配列)
1,2,3ができるようにオレオレカスタマイザを作ってみたので、公開してみます。
yosshio0426/NestValidation · GitHub
もし同じお悩みを抱えている人がいたら、ちょっと見てみてください。
細かくテストしているわけではないので、使用の際はちゃんと中身を見て理解してから使ってみてください。
ちなみに3階層目までは保証しません。