REST APIでJWT認証を実現するには、リクエストごとにトークンを検証するカスタムフィルターが必要です。本記事では、Spring SecurityのOncePerRequestFilterを継承したJWT認証フィルターの実装方法を解説します。Bearer Tokenの抽出からSecurityContextへの認証情報設定、トークン期限切れや不正トークンの例外ハンドリングまで、本番環境で使用できるレベルの実装を目指します。
実行環境と前提条件
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 | バージョン・要件 |
|---|---|
| Java | 17以上 |
| Spring Boot | 3.4.x |
| Spring Security | 6.4.x |
| jjwt | 0.13.0 |
| ビルドツール | Maven または Gradle |
事前に以下の知識があると理解がスムーズです。
- Spring SecurityのFilterChainアーキテクチャ
- JWTの構造と検証の仕組み
- Servlet Filterの基本概念
OncePerRequestFilterとは
OncePerRequestFilterは、Spring Frameworkが提供する抽象クラスで、リクエストごとに1回だけ実行されることを保証するFilterです。JWT認証フィルターを実装する際のベースクラスとして最適です。
なぜOncePerRequestFilterを使うのか
通常のServlet Filterは、フォワードやインクルードが発生すると同一リクエスト内で複数回実行される可能性があります。OncePerRequestFilterはこの問題を解決し、以下のメリットを提供します。
| 特徴 | 説明 |
|---|---|
| 1回実行保証 | リクエスト内で複数回呼び出されても1回のみ実行 |
| HttpServletRequestのみ対応 | ServletRequestではなくHttpServletRequestを直接扱える |
| 非同期対応 | 非同期ディスパッチでの再実行を防止 |
| スキップ条件設定 | 特定条件でフィルター処理をスキップ可能 |
OncePerRequestFilterの実行フロー
OncePerRequestFilterの内部動作を以下に示します。
flowchart TD
A[リクエスト到着] --> B{既に実行済み?}
B -->|はい| C[スキップして次のFilterへ]
B -->|いいえ| D{スキップ対象?}
D -->|はい| C
D -->|いいえ| E[doFilterInternalを実行]
E --> F[実行済みマークを設定]
F --> G[次のFilterへ]
C --> GJWT認証フィルターの設計
JWT認証フィルターが担う責務を明確にし、設計方針を定めます。
フィルターの責務
JWT認証フィルターは以下の処理を担当します。
- Authorizationヘッダーの検査: Bearer Tokenの有無を確認
- トークンの抽出: Bearer プレフィックスを除去してJWTを取得
- トークンの検証: 署名と有効期限を検証
- 認証情報の設定: SecurityContextに
Authenticationオブジェクトを設定 - 例外ハンドリング: トークン検証失敗時の適切なエラーレスポンス
認証フローの全体像
sequenceDiagram
participant Client as クライアント
participant Filter as JwtAuthenticationFilter
participant JwtService as JwtService
participant UDS as UserDetailsService
participant Context as SecurityContext
participant Chain as FilterChain
Client->>Filter: リクエスト + Authorization: Bearer xxx
Filter->>Filter: Authorizationヘッダー抽出
Filter->>Filter: Bearer プレフィックス除去
Filter->>JwtService: トークン検証・ユーザー名抽出
alt トークン有効
JwtService-->>Filter: ユーザー名
Filter->>UDS: loadUserByUsername()
UDS-->>Filter: UserDetails
Filter->>Context: Authentication設定
Filter->>Chain: doFilter()
else トークン無効
JwtService-->>Filter: 例外発生
Filter-->>Client: 401 Unauthorized
endJWT認証フィルターの実装
OncePerRequestFilterを継承したJWT認証フィルターを実装します。
基本構造
|
|
メソッドの役割
| メソッド | 役割 |
|---|---|
doFilterInternal |
フィルターのメイン処理 |
extractToken |
Authorizationヘッダーからトークンを抽出 |
isAlreadyAuthenticated |
既に認証済みかどうかを確認 |
authenticateUser |
ユーザーを認証しSecurityContextに設定 |
AuthorizationヘッダーからのBearer Token抽出
Bearer Tokenの抽出処理を詳しく解説します。
Bearer認証スキームの仕様
RFC 6750で定義されたBearer認証スキームでは、トークンは以下の形式でAuthorizationヘッダーに含められます。
|
|
堅牢なトークン抽出の実装
エッジケースを考慮したトークン抽出処理を実装します。
|
|
抽出処理のフロー図
flowchart TD
A[リクエスト受信] --> B{Authorizationヘッダーあり?}
B -->|なし| C[null返却]
B -->|あり| D{Bearer で開始?}
D -->|いいえ| C
D -->|はい| E[プレフィックス除去]
E --> F[前後の空白を除去]
F --> G{空文字列?}
G -->|はい| C
G -->|いいえ| H[トークン返却]SecurityContextへの認証情報設定
認証成功時にSecurityContextへ認証情報を設定する処理を解説します。
UsernamePasswordAuthenticationTokenの構築
JWT認証では、パスワードなしのUsernamePasswordAuthenticationTokenを使用します。
|
|
コンストラクタの違い
UsernamePasswordAuthenticationTokenには2つのコンストラクタがあります。
| コンストラクタ | 用途 | isAuthenticated |
|---|---|---|
(principal, credentials) |
認証前のトークン作成 | false |
(principal, credentials, authorities) |
認証済みトークン作成 | true |
JWT認証フィルターでは、トークン検証後に認証済みトークンを作成するため、3引数のコンストラクタを使用します。
WebAuthenticationDetailsの重要性
WebAuthenticationDetailsSourceは、HTTPリクエストから以下の情報を抽出します。
|
|
これらの情報は、監査ログやセキュリティ分析に活用できます。
例外ハンドリングの実装
JWTトークン検証で発生する各種例外を適切に処理します。
発生しうる例外の種類
jjwtライブラリが送出する主要な例外は以下のとおりです。
| 例外クラス | 発生条件 | HTTPステータス |
|---|---|---|
ExpiredJwtException |
トークンの有効期限切れ | 401 |
SignatureException |
署名が不正(改ざん検出) | 401 |
MalformedJwtException |
トークン形式が不正 | 400 |
UnsupportedJwtException |
サポートしていないJWT形式 | 400 |
IllegalArgumentException |
トークンがnullまたは空 | 400 |
例外ハンドリングメソッドの実装
|
|
エラーレスポンスの送信
統一されたエラーレスポンス形式で返却します。
|
|
エラーレスポンスDTO(オプション)
より構造化されたエラーレスポンスを返す場合は、DTOクラスを使用します。
|
|
ObjectMapperを使用した送信処理は以下のとおりです。
|
|
特定パスのスキップ設定
認証不要なパス(ログインエンドポイントなど)をフィルター処理から除外します。
shouldNotFilterメソッドのオーバーライド
|
|
AntPathMatcherを使用した柔軟な設定
|
|
設定ファイルからの読み込み
|
|
|
|
SecurityFilterChainへの組み込み
実装したJWT認証フィルターをSpring Securityに組み込みます。
設定クラス
|
|
フィルターの配置位置
flowchart TD
A[SecurityContextHolderFilter] --> B[HeaderWriterFilter]
B --> C[CsrfFilter]
C --> D[LogoutFilter]
D --> E[JwtAuthenticationFilter]
E --> F[UsernamePasswordAuthenticationFilter]
F --> G[BasicAuthenticationFilter]
G --> H[AnonymousAuthenticationFilter]
H --> I[ExceptionTranslationFilter]
I --> J[AuthorizationFilter]
style E fill:#f9f,stroke:#333,stroke-width:2pxaddFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)により、JWT認証フィルターはUsernamePasswordAuthenticationFilterの直前に配置されます。
AuthenticationEntryPointの実装
認証されていないリクエストへのレスポンスをカスタマイズします。
|
|
完全な実装コード
これまでの実装をすべて含んだ完全版のJWT認証フィルターを示します。
|
|
動作確認
実装したJWT認証フィルターの動作を確認します。
正常系:有効なトークンでのリクエスト
|
|
期待されるレスポンス:
|
|
異常系:トークンなしでのリクエスト
|
|
期待されるレスポンス(401 Unauthorized):
|
|
異常系:期限切れトークンでのリクエスト
|
|
異常系:不正なトークンでのリクエスト
|
|
期待されるレスポンス(400 Bad Request):
|
|
テストコード
JWT認証フィルターの単体テストを実装します。
|
|
まとめ
本記事では、Spring SecurityのOncePerRequestFilterを継承したJWT認証フィルターの実装方法を解説しました。
主なポイントは以下のとおりです。
OncePerRequestFilterはリクエストごとに1回だけ実行されることを保証する- Bearer Tokenの抽出ではエッジケースを考慮した堅牢な実装が重要
- SecurityContextへの認証情報設定には3引数コンストラクタを使用
- 例外の種類に応じた適切なHTTPステータスコードとエラーメッセージの返却
shouldNotFilterメソッドで認証不要パスをスキップaddFilterBeforeでUsernamePasswordAuthenticationFilterの前に配置
JWT認証フィルターは、REST APIのセキュリティにおける重要なコンポーネントです。本記事の実装をベースに、プロジェクトの要件に合わせてカスタマイズしてください。