Spring BootでOAuthログインを実現するには、Spring Security OAuthを利用するわけですが、
あまりドキュメントがなかったので備忘録を兼ねてメモっておきます。
とはいえ、大体は以下のページに書いてあるとおりですが。
この記事では、まずOAuthでのログインを実現した後、AngularJSなどのアプリケーションから利用される想定のAPIを作成し、そのAPIにユーザー権限によるアクセス制限を掛けるまでの手順を記載します。
要するに、動的画面遷移を中心に組み立てられたSPAっぽいアプリケーションで、ログインと認可を実現する方法をメモっておきます。
Spring Bootアプリケーションの作成
Spring Bootアプリケーションの作成には幾つか選択肢がありますが、ここではある程度スクラッチで作ることにします。
実際は、Webページ上から雛形プロジェクトをサクッと作成してダウンロードすることも可能です。
まずは、Bootアプリケーションのビルドや実行に利用するビルドスクリプトです。
build.gradle
をプロジェクトディレクトリ直下に用意します。
buildscript {
ext {
springBootVersion = '1.3.2.RELEASE' // Spring Bootのバージョン
}
repositories {
mavenCentral() // Gradle Spring Bootプラグインを取得するリポジトリ(ここではMavenセントラルリポジトリ)
}
dependencies {
// Gradle Spring Bootプラグイン
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
}
}
// ビルドに必要なライブラリの読み込み
apply plugin: 'java'
apply plugin: 'spring-boot'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral() // アプリケーションを実行するのに必要なライブラリの取得先リポジトリ
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.security.oauth:spring-security-oauth2')
compile("org.springframework.boot:spring-boot-devtools") //コード更新時にアプリケーションをリロードする
}
bootRun {
jvmArgs = ['-Dspring.output.ansi.enabled=always'] //コンソールに色を付ける
}
次に、Bootアプリケーションのメインクラスを作成します。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SsoSampleApplication {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
}
最後に、画面表示用のhtmlを用意します。
BootではJSPやThymeleafなどのテンプレートを利用することも簡単に出来ますが、
今回の主題と関係ないのでペラのHTMLを表示するだけにします。
Spring Bootでは、クラスパス上のstatic
やpublic
ディレクトリ以下のファイルをホストできるので、
src/main/resources/public
以下にindex.html
を配置し、クラスパス上にindex.html
がコピーされるようにします。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample</p>
</body>
</html>
以上三つのファイルを作成したら、以下のコマンドを実行してアプリケーションを起動します。
gradle bootRun
gradleコマンドをインストールする1必要があるので、まだ入っていない場合はGradleのページからダウンロードしてインストールしてください。
http://localhost:8080/ で画面が表示されることを確認します。
bootアプリケーションはSpringSecurityがクラスパスに入っているとデフォルトでアプリケーションにBASIC認証をかけます。
なので、 http://localhost:8080/ に行くとユーザー認証ダイアログが表示されます。
デフォルトのユーザー名はuser
、パスワードはgradleコマンドを実行したコンソールに表示されているので、そのとおり入力します。
次の節から、このBASIC認証をログインリンクからのOAuth2認証に変えていきます。
OAuth2でログインする設定
まず、BASIC認証をOAuth2でのログインに変更します。OAuth2プロバイダーとして今回はSlackを利用します。Facebookとかでも基本的にやることはあまり変わりません。
OAuthアプリケーションの登録
どのプロバイダーを利用するにしても、まずクライアントアプリケーションを登録する必要がありますね。
Slackの場合は下記ページの「Create a new application」からアプリケーションを登録できます。
AppNameやらは適当に良い感じの名前を入力してもらうとして、
Redirect URI(s)に http://localhost:8080 を入れておいてください。
登録すると発行される「Client ID」や「Client Secret」は後ほど使用します。
Bootアプリケーションの設定
BootアプリケーションにSpring Security OAuthでOAuthコンシューマー2の設定を追加します。
それには、まず設定用のJavaクラスを@EnableOAuth2Sso
(JavaDoc)で注釈します。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
@SpringBootApplication
@EnableOAuth2Sso // これを追加する
public class SsoSampleApplication {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
}
このアノテーションにより、OAuth2クライアントの設定(@EnableOAuth2Client
(JavaDoc))とそのクライアントを利用した認証処理(OAuth2SsoDefaultConfiguration
(JavaDoc))がBootアプリケーションに組み込まれます。
@EnableOAuth2Sso
がやっていることを自分で実装することで認証処理をカスタマイズする方法が以下に紹介されています。
@EnableOAuth2Sso
は、Springのアプリケーション設定からOAuth2の設定を読み込んで利用します3。
クラスパス上にapplication.properties
かapplication.yml
を作成して、利用するOAuthプロバイダー(今回はSlack)の設定を記載します。
プロパティファイルよりYAMLのほうが読みやすいと思うので、今回はapplication.yml
で。
security:
oauth2:
client:
clientId: 'xxxx.xxxx' # Slackのアプリケーション登録で表示された「Client ID」
clientSecret: 'xxxxx' # Slackのアプリケーション登録で表示された「Client Secret」
accessTokenUri: 'https://slack.com/api/oauth.access' # Slackを利用する場合の設定値
userAuthorizationUri: 'https://slack.com/oauth/authorize' # Slackを利用する場合の設定値
authenticationScheme: 'query' # Slackを利用する場合の設定値
scope: 'identify' # Slackを利用する場合の設定値
tokenName: 'token' # Slackを利用する場合の設定値
resource:
userInfoUri: 'https://slack.com/api/auth.test' # Slackを利用する場合の設定値
Bootアプリケーションを再起動してトップページ( http://localhost:8080/ )にアクセスすると
Slackのページに飛ばされ、OAuthログインが可能になっているはずです。
ログインリンクの作成
今の状態だと、トップページを表示したとたんにログインが要求されますが、これをログインボタンを押すことでログイン要求されるように変更してみます。
まず、html上にログインリンクを作成します。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample <a href="/login">ログイン</a></p>
</body>
</html>
/login
はSpring Bootが用意している特別なパスで、特に何も設定しなくてもここにアクセスすればログイン処理が開始されます4。
/login
に対応する画面を作成する必要はありません。
/login
に移動するとOAuthプロバイダーのログイン画面へリダイレクトされ、ログイン後はSlackに設定したRedirect URI(s)に従い/login
に遷移する前のページにリダイレクトされます。
とはいえ、このままだと/login
リンクを踏む前のトップページに行っただけでログインが要求されてしまうので、Spring Securityの設定を変更して、ログインなしでトップページにアクセスできるようにします。
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated();
}
}
これでトップページがログインなしで表示され、「ログイン」リンクを踏むとSlackの認証ページに飛んでログインできるようになります。
また、ログイン情報はセッションに保存されるので、一度ログインすれば再度ログインボタンを押してもすぐにトップページに返ってきます。
ログアウト機能の作成
ログインはできても、今のままだとログアウトができませんね。ログアウト機能も作っておきます。
ログアウト機能を追加するには、SpringSecurityの設定でセッションのログイン情報を破棄するURLパスを設定します。
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll(); // ログアウト機能の設定
}
}
ログアウト機能は、SpringSecurityのHttpSecurity設定でlogout()
を呼び出すことで設定できます。デフォルトでは、/logout
にPOSTのリクエストを送ることでログイン情報が破棄され、ログアウトできるようになります。
また、SpringSecurityではデフォルトでCSRF対策が有効になっており、POSTリクエストを正常に送るにはCSRFトークンを送信する必要があります。CSRF対策は実際には必須ですが、この記事では少し趣旨から外れるので無効化してしまいます。
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // CSRF対策を無効化
.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll();
}
}
さらに、画面のHTMLにログアウトボタンを追加します。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample <a href="/login">ログイン</a></p>
<form id="logoutForm" action="/logout" method="POST">
<input type="submit" value="ログアウト" />
</form>
</body>
</html>
トップページのログアウトボタンを押すことでログアウトされ、再度「ログイン」リンクを踏んだときにSlackへリダイレクトされるようになることを確認します。
APIの実装
さて、これで認証機能が実装されたので、簡単なAPIを実装して、現在のログイン状態に応じてレスポンスを変更できるようにしてみます。
まず、APIの実装です。以下のように、RestAPI用のコントローラーを追加します。
package com.example;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HelloController {
@RequestMapping("/api/hello")
public Map<String, String> getMessage() {
Map<String, String> response = new HashMap<>();
response.put("message", "Hello!");
return response;
}
}
APIは/api
以下のURLに実装することにします。このURLは認証なしで利用できるようにします。
@SpringBootApplication
@EnableOAuth2Sso
public class SsoSampleApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(SsoSampleApplication.class, args);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/api/**").permitAll() // /api以下のパスを認証なしで利用できるようにする
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll();
}
}
http://localhost:8080/api/hello にアクセスして、JSONが表示されることを確認します。
{"message": "Hello"}
また、画面にこのメッセージが表示されるようにしておきましょう。
<!DOCTYPE html>
<html>
<head>
<title>SSO Sample</title>
</head>
<body>
<p>SSO Sample <a href="/login">ログイン</a></p>
<form id="logoutForm" action="/logout" method="POST">
<input type="submit" value="ログアウト" />
</form>
<p id="message"></p>
</body>
<!-- 以下を追加 -->
<script src="//code.jquery.com/jquery-2.2.0.js"></script>
<script>
$.get('/api/hello').done(function(data){
$("#message").html(data.message);
}).fail(function(data){
$("#message").html(data.responseJSON.message);
});
</script>
</html>
画面に「Hello!」というメッセージが表示されることを確認します。
APIのアクセス制限
さて、作成した/api/hello
にアクセス制限を掛けてみます。Spring SecurityはサーブレットAPIと統合されているので、サーブレットAPIを通して現在の認証状態を取得できます。
HelloController
を以下のように修正して、現在のログイン状態に応じて異なるレスポンスを返すようにします。
package com.example;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.common.exceptions.UnauthorizedUserException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HelloController {
@RequestMapping("/api/hello")
public Map<String, String> getMessage(HttpServletRequest request) {
if (request.isUserInRole("ROLE_USER")) {
Map<String, String> response = new HashMap<>();
response.put("message", "Hello, " + request.getRemoteUser() + "!!!");
return response;
}
throw new UnauthorizedUserException("You don't have a required role. ");
}
@ExceptionHandler(UnauthorizedUserException.class)
public void unauthorized(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.UNAUTHORIZED.value());
}
}
このように、HttpServletRequest
から現在ログイン中のユーザー名や割り当てられたロールを取得することができます。
トップページ( http://localhost:8080/ )にアクセスして、ログイン前後で画面の表示が変わることを確認します。
まとめ
一般的なログインフローなら、足回りの機能をほとんどSpring Bootがやってくれるのでかなり楽ですね。
もちろん、ログインユーザーに付加的な情報を追加したり、ロールを調整する場合は追加の実装が必要になりますが、それほど手間でもなさそうです。
以下のリンクを参照してください。
https://spring.io/guides/tutorials/spring-boot-oauth2/#_how_to_add_a_local_user_database
https://spring.io/guides/tutorials/spring-boot-oauth2/#_generating_a_401_in_the_server