SpringはJavaエコシステムにおいて信頼性の高いフレームワークとして広く利用されています。現在ではSpringを単なるフレームワークと呼ぶのは適切ではなく、様々なフレームワークを包含する包括的な用語となっています。その中の一つがSpring Securityで、強力でカスタマイズ可能な認証・認可フレームワークです。これはSpringベースのアプリケーションを保護するための事実上の標準となっています。
その人気にもかかわらず、シングルページアプリケーションの場合、設定は単純ではなく直感的ではありません。その理由は、サーバーサイドでWebページのレンダリングが行われ、通信がセッションベースであるMVCアプリケーション向けに始まったためと考えられます。
バックエンドがJavaとSpringベースの場合、Spring Securityを使用して認証/認可を設定し、ステートレス通信を構成するのが理にかなっています。この設定方法を説明する記事は多くありますが、私にとっては初めて設定する際には依然として苦労しましたし、複数の情報源から情報を収集してまとめる必要がありました。そこで、この記事を書くことにしました。ここでは、設定プロセス中に遭遇する可能性のあるすべての必要な詳細と注意点をまとめ、説明します。
用語の定義
技術的な詳細に入る前に、Spring Securityの文脈で使用される用語を明確に定義しておきましょう。これにより、私たちが同じ言語で話していることを確認できます。
以下の用語について説明します:
- 認証(Authentication) は、提供された資格情報に基づいてユーザーの身元を確認するプロセスを指します。一般的な例は、ウェブサイトにログインする際にユーザー名とパスワードを入力することです。これは「誰ですか?」という質問への回答と考えることができます。
- 認可(Authorization) は、ユーザーが正常に認証されたことを前提に、ユーザーが特定のアクションを実行したり、特定のデータを読み取ったりする適切な権限を持っているかどうかを判断するプロセスを指します。これは「このユーザーはこれを実行/読み取りできますか?」という質問への回答と考えることができます。
- プリンシパル(Principle) は、現在認証されているユーザーを指します。
- 付与された権限(Granted authority) は、認証されたユーザーの許可を指します。
- ロール(Role) は、認証されたユーザーの権限のグループを指します。
基本的なSpringアプリケーションの作成
Spring Securityフレームワークの設定に入る前に、基本的なSpring Webアプリケーションを作成しましょう。これにはSpring Initializrを使用してテンプレートプロジェクトを生成できます。シンプルなWebアプリケーションの場合、Spring Webフレームワークの依存関係のみで十分です:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
プロジェクトを作成したら、次のようにシンプルなRESTコントローラーを追加できます:
@RestController @RequestMapping("greeting")
public class GreetingRestController {
@GetMapping("member")
public String greetMember() {
return "こんにちは、メンバー";
}
@GetMapping("manager")
public String greetManager() {
return "こんにちは、マネージャー";
}
}
これでプロジェクトをビルドして実行すると、Webブラウザで次のURLにアクセスできます:
- http://localhost:8080/greeting/member は文字列 "こんにちは、メンバー" を返します。
- http://localhost:8080/greeting/manager は文字列 "こんにちは、マネージャー" を返します。
次に、Spring Securityフレームワークをプロジェクトに追加しましょう。これを行うには、pom.xmlファイルに次の依存関係を追加します:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
他のSpringフレームワークの依存関係を追加しても、対応する設定を提供するまでアプリケーションに即時効果は通常ありませんが、Spring Securityは例外で、即時効果があります。これは、Spring SecurityフレームワークがすべてのURLに対してデフォルトで認証を要求するためのデフォルトの動作です。これを追加した後、プロジェクトを再ビルドして実行し、上記のURLのいずれかにアクセスしようとすると、結果の表示ではなくhttp://localhost:8080/loginにリダイレクトされます。
認証を通過するには、デフォルトのユーザー名userを使用し、コンソールで自動生成されたパスワードを見つけます:
生成されたセキュリティパスワード: 1fc15145-dfee-4bec-a009-e32ca21c77ce
アプリケーションを再実行するたびにパスワードが変更されることに注意してください。この動作を変更してパスワードを静的にしたい場合は、次の設定をapplication.propertiesファイルに追加できます:
spring.security.user.password=SecurePass123!
ログインフォームに資格情報を入力すると、URLにリダイレクトされ、正しい結果が表示されます。デフォルトの認証プロセスはセッションベースであることに注意してください。ログアウトするには、次のURLにアクセスできます:http://localhost:8080/logout
このデフォルトの動作は、クライアント側でレンダリングが行われ、JWTベースのステートレス認証が一般的であるシングルページアプリケーションのほとんどのユースケースでは通常役に立ちません。この場合、Spring Securityフレームワークを大幅にカスタマイズする必要があります。これについては、この記事の残りの部分で行います。
例として、クラシックな書店Webアプリケーションを実装し、著者と書籍を作成するCRUD API、ユーザー管理API、および認証APIを提供するバックエンドを作成します。
Spring Securityアーキテクチャの概要
カスタマイズの設定を開始する前に、まずSpring Security認証が内部的にどのように機能するかを説明しましょう。
次の図は、フローを示し、認証リクエストがどのように処理されるかを示しています:
Spring Securityアーキテクチャ
この図をコンポーネントに分解し、それぞれを個別に説明しましょう。
Spring Securityフィルターチェーン
Spring Securityフレームワークをアプリケーションに追加すると、すべての着信リクエストをインターセプトするフィルターチェーンが自動的に登録されます。このチェーンは様々なフィルターで構成され、それぞれが特定のユースケースを処理します。
例えば:
- 構成に基づいて、要求されたURLが公開アクセス可能かどうかを確認します。
- セッションベースの認証の場合、ユーザーが現在のセッションで既に認証されているかどうかを確認します。
- ユーザーが要求されたアクションを実行する権限があるかどうかを確認します。などです。
ここで言及したい重要な詳細は、Spring Securityフィルターは最も低い順序で登録され、最初に呼び出されるフィルターであることです。一部のユースケースでは、カスタムフィルターをそれらの前に配置したい場合、フィルターの順序にパディングを追加する必要があります。これは次の設定で行うことができます:
spring.security.filter.order=10
この設定をapplication.propertiesファイルに追加すると、Spring Securityフィルターの前に10個のカスタムフィルターを配置するスペースができます。
AuthenticationManager
AuthenticationManagerは、複数のプロバイダーを登録できるコーディネーターと考えることができます。リクエストタイプに基づいて、正しいプロバイダーに認証リクエストを配信します。
AuthenticationProvider
AuthenticationProviderは特定のタイプの認証を処理します。そのインターフェースは2つの関数のみを公開します:
- authenticateはリクエストを使用して認証を実行します。
- supportsは、このプロバイダーが指定された認証タイプをサポートしているかどうかをチェックします。
サンプルプロジェクトで使用しているインターフェースの重要な実装はDaoAuthenticationProviderで、UserDetailsServiceからユーザー詳細を取得します。
UserDetailsService
UserDetailsServiceは、Springドキュメントでユーザー固有のデータをロードするコアインターフェースとして説明されています。
ほとんどのユースケースでは、認証プロバイダーはデータベースから資格情報に基づいてユーザーID情報を抽出し、検証を実行します。このユースケースは非常に一般的であるため、Spring開発者はそれを別のインターフェースとして抽出することにしました。このインターフェースは単一の関数を公開します:
- loadUserByUsernameはユーザー名をパラメーターとして受け取り、ユーザーIDオブジェクトを返します。
JWTを使用したSpring Securityでの認証
Spring Securityフレームワークの内部について説明した後、ステートレス認証用にJWTトークンを使用して設定を構成しましょう。
Spring Securityをカスタマイズするには、クラスパスに@EnableWebSecurityアノテーションで注釈が付けられた構成クラスが必要です。また、カスタマイズプロセスを簡素化するために、フレームワークはWebSecurityConfigurerAdapterクラスを公開しています。このアダプターを拡張し、その両方の関数をオーバーライドして次のことを行います:
- 正しいプロバイダーで認証マネージャーを構成する
- Webセキュリティ(公開URL、プライベートURL、認可など)を構成する
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 認証マネージャーの構成
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// Webセキュリティの構成
}
}
サンプルアプリケーションでは、ユーザーIDをMongoDBデータベースのusersコレクションに保存します。これらのIDはUserエンティティによってマッピングされ、そのCRUD操作はUserRepo Spring Dataリポジトリによって定義されます。
認証リクエストを受け入れる際、データベースから正しいIDを提供された資格情報を使用して取得し、検証する必要があります。これには、UserDetailsServiceインターフェースの実装が必要です。これは次のように定義されています:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}
ここで、UserDetailsインターフェースを実装するオブジェクトを返す必要があることがわかります。私たちのUserエンティティはそれを実装しています(実装の詳細については、サンプルプロジェクトのリポジトリを参照してください)。単一の関数プロトタイプのみを公開することを考慮すると、関数インターフェースとして扱い、ラムダ式として実装を提供できます。
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserRepository userRepo;
public SecurityConfiguration(UserRepository userRepo) {
this.userRepo = userRepo;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(username -> userRepo
.findByUsername(username)
.orElseThrow(
() -> new UsernameNotFoundException(
format("ユーザー: %s, が見つかりません", username)
)
));
}
// 簡潔にするための詳細は省略
}
ここで、auth.userDetailsService関数呼び出しは、UserDetailsServiceインターフェースの実装を使用してDaoAuthenticationProviderインスタンスを初期化し、認証マネージャーに登録します。
認証プロバイダーに加えて、資格情報検証に使用される正しいパスワードエンコーディングスキーマで認証マネージャーを構成する必要があります。これには、PasswordEncoderインターフェースの好ましい実装をBeanとして公開する必要があります。
サンプルプロジェクトでは、bcryptパスワードハッシュアルゴリズムを使用します。
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final UserRepository userRepo;
public SecurityConfiguration(UserRepository userRepo) {
this.userRepo = userRepo;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(username -> userRepo
.findByUsername(username)
.orElseThrow(
() -> new UsernameNotFoundException(
format("ユーザー: %s, が見つかりません", username)
)
));
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 簡潔にするための詳細は省略
}
認証マネージャーを構成した後、Webセキュリティを構成する必要があります。REST APIを実装しており、JWTトークンを使用したステートレス認証が必要であるため、次のオプションを設定する必要があります:
【元記事】https://www.toptal.com/spring/spring-security-tutorial