Spring SecurityでWebの認証と認可を制御する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。Webアプリケーションを書くにあたって、誰でも自由に全てのリクエストが呼べるなんていうユルユルな要件ってのはほとんど無いと思います。誰がからのアクセスかが特定できて、そのユーザが適切な権限を持っている場合に限り、アクセスが成功する必要があるでしょう。

Spring securityの導入 v14.0

Spring frameworkの世界の中で認証と認可を司るコンポーネントが Spring security です。

Spring BootプロジェクトでSpring securityを使うには、まずは依存ライブラリとして spring-boot-starter-security を追加します。え、まさかこれだけ? → GitHub diff

さぁさぁ、起動してみましょう。(以降、タイトルに示したバージョンのブランチをcheckoutして実行できるようにしてあります。)

$ git clone https://github.com/classmethod-aws/berserker.git
$ cd berserker
$ git checkout 14.0
$ ./gradlew bootRun
(略)
2016/05/26 14:14:25.960 [host-startStop-1] INFO  o.s.b.a.s.AuthenticationManagerConfiguration:170 -

Using default security password: 8dad0c63-b777-4f11-9231-30d7f221eba6
(略)
2016/05/26 14:14:28.495 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer:162 - Tomcat started on port(s): 8080 (http)
2016/05/26 14:14:28.503 [main] INFO  j.c.e.berserker.BerserkerApplication:57 - Started BerserkerApplication in 6.301 seconds (JVM running for 7.233)

細かい設定をしないと、userというユーザに対して、自動生成したパスワードを設定して起動してくれます。自動生成パスワードは、起動ログ中に見えていますね。

APIエンドポイントへのアクセス

早速アクセスしてみます。

$ curl -s http://localhost:8080/users | jq .
{
  "timestamp": 1464239892131,
  "status": 401,
  "error": "Unauthorized",
  "message": "Full authentication is required to access this resource",
  "path": "/users"
}

このように、クレデンシャルを特に指定しないと、401 Unauthorized となります。

$ curl -s \
  -H "Authorization: Basic $(echo -n "user:8dad0c63-b777-4f11-9231-30d7f221eba6" | base64)" \
  http://localhost:8080/users | jq .
[
  {
    "username": "miyamoto"
  },
  {
    "username": "yokota"
  }
]

そして、Basic認証の仕様に従って Authorization ヘッダを付けてやると、今まで通りの結果が得られました。

Web画面へのアクセス

同様に、/ にブラウザからアクセスしてみると、Basic認証ダイアログが表示されますね。

screenshot 2016-05-26 14.16.14

ここにユーザ名とパスワードを入力してやると、従来通りののページが表示できるはずです。

ユーザを静的に設定する v14.1

ここまでのような無設定ですと、パスワードは起動毎に変わってしまう状態なので、実用的ではありません。ユーザ名とパスワードを固定化する設定を書いてみましょう。 → GitHub diff

Webに関わるセキュリティ設定は、WebSecurityConfigurerAdapterを継承したクラスに@Configuration@EnableWebSecurityを付与して行います。

設定では configure(AuthenticationManagerBuilder) メソッドをオーバーライドし、下記の2ユーザを定義しました。

  • ユーザ名: miyamoto, パスワード: pass, 権限: ROLE_USER
  • ユーザ名: yokota, パスワード: word, 権限: ROLE_ADMIN

v14.1をチェックアウト&起動して試してみてください。

$ curl -s \
  -H "Authorization: Basic $(echo -n "miyamoto:pass" | base64)" \
  http://localhost:8080/users | jq .
[
  {
    "username": "miyamoto"
  },
  {
    "username": "yokota"
  }
]

ユーザをDBから読み込む v14.2

そう、MySQLにusersテーブルを用意してせっかくユーザを登録しているわけですから。。。その情報で認証したいですよね。 → GitHub diff

というわけで設定では configure(AuthenticationManagerBuilder) メソッドにおいて inMemoryAuthentication ではなく jdbcAuthentication を構成してみました。SQL等が設定されていて少々武骨ですがご容赦を。

また、パスワードはハッシュ化して記録してありますので、そのハッシュ関数に対応するBCryptPasswordEncoderを設定しておきます。

で、ここで一点お詫びなんですが、今まで使ってきたハッシュ化パスワードの平文を忘れてしまいましたw なので、flywayを使ってテーブルスキーマ更新と共にパスワードを更新しています。ご了承くださいw

v14.2をチェックアウト&起動して試してみてください。v14.1と動きは変わりませんけど。。。とりあえずDBに基いて認証をしています。

フォーム認証を行う v14.3

APIであればBasic認証もアリだと思うのですが、UIの場合はHTMLによるフォーム認証が一般的です。そのように設定してみました。 → GitHub diff

configure(HttpSecurity)メソッドをオーバーライドし、下記の設定を行いました。

1つ目は、全てのリクエストにおいて、認証が必要であること。

http.authorizeRequests()
  .anyRequest().authenticated();

2つ目は、フォームによる認証を行うこと。

http.formLogin();

さらに、index.html を修正して、ログアウトボタンも付けてみました。

v14.3をチェックアウト&起動し、ブラウザで http://localhost:8080/ にアクセスしてみてください。下記の通り、/login に自動的にリダイレクトされ、ログイン画面を表示します。

screenshot 2016-06-01 18.18.24

認証が成功すると、/ を表示します。

screenshot 2016-06-01 18.20.15

ログアウトボタンを押すと、再びログイン画面に戻ります。

screenshot 2016-06-01 18.20.55

パス毎にセキュリティ要件を設定する v14.4

さて、ここまでの設定では全てのエンドポイントに対するアクセスが要認証となってしまいます。そこでエンドポイントを幾つか追加し、パス毎に必要な条件を設定してみました。 → GitHub diff

パス 条件
/public 認証なしで誰でもアクセスできる。
/admin 認証済みで、ROLE_ADMIN 権限を持っている場合のみアクセスできる。
/、というか上記以外全部 認証していれば権限に関わらずアクセスできる。未認証はダメ。

というのが、これです。

http.authorizeRequests()
  .antMatchers("/public").permitAll()
  .antMatchers("/admin").hasAuthority("ROLE_ADMIN")
  .anyRequest().authenticated();

v14.4をチェックアウト&起動して試してみてください。miyamoto(ROLE_ADMINを持っていない)でログインして/adminにアクセスした場合の画面だけ、示しておきます。

screenshot 2016-06-01 18.25.43

フォームログインしたりBasic認証したり…。

さて、ここで本連載に関する決断を一つさせていただこうと思います。この先、この連載でHTMLによるWeb-UIアプリケーションを書いていくのか、JSONによるWeb-APIアプリケーションを書いていくのか。

それぞれをご紹介できれば、Springの魅力をより広く皆様にお届けできると思うのですが、併記していくのも読みづらいものです。例えば、未認証でUIにアクセスした場合は「ログインページにリダイレクト」、未認証でAPIにアクセスした場合は「401 Unauthorized」を返すべき等の複雑な要件に対しては、設定も複雑になってしまいます。

ここは断腸の思いで一旦「JSONによるWeb-APIアプリケーション」の方に倒そうと思います。その方針に従ってフォーム認証は廃止し、ひとまずBasic認証のみとして整理したものが、v14.5 になります。

よろしくお願いします。