最新の安定バージョンについては、Spring Security 6.4.2 を使用してください! |
アーキテクチャー
このセクションでは、サーブレットベースのアプリケーション内の Spring Security の高レベルアーキテクチャについて説明します。リファレンスの認証、認可、エクスプロイトに対する保護セクション内のこの高レベルの理解に基づいています。
フィルターのレビュー
Spring Security のサーブレットサポートはサーブレットフィルターに基づいているため、一般的に最初にフィルターのロールを確認すると便利です。次のイメージは、単一の HTTP リクエストのハンドラーの一般的な階層化を示しています。
クライアントはアプリケーションにリクエストを送信し、コンテナーは FilterChain
を作成します。これには、リクエスト URI のパスに基づいて、Filter
インスタンスと HttpServletRequest
を処理する必要がある Servlet
が含まれます。Spring MVC アプリケーションでは、Servlet
は DispatcherServlet
のインスタンスです。最大で、1 つの Servlet
が 1 つの HttpServletRequest
および HttpServletResponse
を処理できます。ただし、複数の Filter
を使用して次のことができます。
ダウンストリーム
Filter
インスタンスまたはServlet
が呼び出されないようにします。この場合、Filter
は通常HttpServletResponse
を書き込みます。ダウンストリーム
Filter
インスタンスおよびServlet
によって使用されるHttpServletRequest
またはHttpServletResponse
を変更します。
Filter
のパワーは、それに渡される FilterChain
から得られます。
FilterChain
の使用例 Java
Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// do something before the rest of the application
chain.doFilter(request, response) // invoke the rest of the application
// do something after the rest of the application
}
Filter
はダウンストリームの Filter
インスタンスと Servlet
にのみ影響を与えるため、各 Filter
が呼び出される順序は非常に重要です。
DelegatingFilterProxy
Spring は、DelegatingFilterProxy
(Javadoc) という名前の Filter
実装を提供します。これにより、サーブレットコンテナーのライフサイクルと Spring の ApplicationContext
の間のブリッジが可能になります。サーブレットコンテナーでは、独自の標準を使用して Filter
インスタンスを登録できますが、Spring で定義された Bean を認識していません。標準のサーブレットコンテナーメカニズムを介して DelegatingFilterProxy
を登録できますが、すべての作業を Filter
を実装する Spring Bean に委譲します。
DelegatingFilterProxy
が Filter
インスタンスと FilterChain
にどのように適合するかの図です。
DelegatingFilterProxy
は、ApplicationContext
から Bean フィルター 0 を検索してから、Bean フィルター 0 を呼び出します。次のリストは、DelegatingFilterProxy
の擬似コードを示しています。
DelegatingFilterProxy
擬似コード Java
Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); (1)
delegate.doFilter(request, response); (2)
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val delegate: Filter = getFilterBean(someBeanName) (1)
delegate.doFilter(request, response) (2)
}
1 | Spring Bean として登録された Filter を遅延取得します。DelegatingFilterProxy の例では、delegate は Bean Filter 0 のインスタンスです。 |
2 | 作業を Spring Bean に委譲します。 |
DelegatingFilterProxy
のもう 1 つの利点は、Filter
Bean インスタンスの検索を遅らせることができることです。コンテナーを起動する前に、コンテナーが Filter
インスタンスを登録する必要があるため、これは重要です。ただし、Spring は通常 ContextLoaderListener
を使用して Spring Bean をロードします。これは、Filter
インスタンスを登録する必要があるまで実行されません。
FilterChainProxy
Spring Security のサーブレットサポートは FilterChainProxy
に含まれています。FilterChainProxy
は、SecurityFilterChain
を介して多くの Filter
インスタンスに委譲できる、Spring Security によって提供される特別な Filter
です。FilterChainProxy
は Bean であるため、通常は DelegatingFilterProxy にラップされます。
次のイメージは、FilterChainProxy
のロールを示しています。
SecurityFilterChain
SecurityFilterChain
(Javadoc) は、FilterChainProxy によって使用され、現在のリクエストに対してどの Spring Security Filter
インスタンスを呼び出す必要があるかを判別します。
次のイメージは、SecurityFilterChain
のロールを示しています。
SecurityFilterChain
のセキュリティフィルターは通常 Bean ですが、DelegatingFilterProxy ではなく FilterChainProxy
に登録されます。FilterChainProxy
には、サーブレットコンテナーまたは DelegatingFilterProxy に直接登録することで多くの利点があります。まず、Spring Security のすべてのサーブレットサポートの開始点を提供します。そのため、Spring Security のサーブレットサポートのトラブルシューティングを行う場合は、FilterChainProxy
にデバッグポイントを追加することから始めるのが最適です。
第 2 に、FilterChainProxy
は Spring Security の使用箇所の中心であるため、オプションとは見なされないタスクを実行できます。例: メモリリークを回避するために SecurityContext
をクリアします。また、Spring Security の HttpFirewall
を適用して、特定の種類の攻撃からアプリケーションを保護します。
さらに、SecurityFilterChain
をいつ呼び出すかをより柔軟に決定できます。サーブレットコンテナーでは、Filter
インスタンスは URL のみに基づいて呼び出されます。ただし、FilterChainProxy
は、RequestMatcher
インターフェースを使用して、HttpServletRequest
内のすべてに基づいて呼び出しを判別できます。
次のイメージは、複数の SecurityFilterChain
インスタンスを示しています。
複数の SecurityFilterChain の図では、FilterChainProxy
がどの SecurityFilterChain
を使用するかを決定します。一致する最初の SecurityFilterChain
のみが呼び出されます。/api/messages/
の URL がリクエストされた場合、最初に /api/**
の SecurityFilterChain0
パターンと一致するため、SecurityFilterChainn
でも一致する場合でも、SecurityFilterChain0
のみが呼び出されます。/messages/
の URL がリクエストされた場合、/api/**
の SecurityFilterChain0
パターンと一致しないため、FilterChainProxy
は各 SecurityFilterChain
の試行を続行します。他の SecurityFilterChain
インスタンスが一致しないと仮定すると、SecurityFilterChainn
が呼び出されます。
SecurityFilterChain0
には 3 つのセキュリティ Filter
インスタンスしか構成されていないことに注意してください。ただし、SecurityFilterChainn
には 4 つのセキュリティ Filter
インスタンスが構成されています。各 SecurityFilterChain
は一意であり、分離して構成できることに注意することが重要です。実際、アプリケーションが Spring Security に特定のリクエストを無視させたい場合、SecurityFilterChain
はセキュリティ Filter
インスタンスを持たない可能性があります。
セキュリティフィルター
セキュリティフィルターは、SecurityFilterChain API を使用して FilterChainProxy に挿入されます。これらのフィルターは、認証、認可、エクスプロイト保護など、さまざまな目的に使用できます。フィルターは、適切なタイミングで呼び出されることを保証するために、特定の順序で実行されます。たとえば、認証を実行する Filter
は、認可を実行する Filter
より前に呼び出す必要があります。通常、Spring Security の Filter
の順序を知る必要はありません。ただし、順序を知っておくと有益な場合もあります。順序を知りたい場合は、FilterOrderRegistration
コード [GitHub] (英語) を確認してください。
上の段落を例として、次のセキュリティ構成を考えてみましょう。
Java
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.web.servlet.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic { }
formLogin { }
}
return http.build()
}
}
上記の構成により、Filter
の順序は次のようになります。
フィルター | 追加 |
---|---|
| |
| |
| |
|
まず、CSRF 攻撃から保護するために
CsrfFilter
が呼び出されます。次に、リクエストを認証するために認証フィルターが呼び出されます。
3 番目に、
AuthorizationFilter
が呼び出されて、リクエストが承認されます。
上記にリストされていない他の |
セキュリティフィルターを出力する
多くの場合、特定のリクエストに対して呼び出されるセキュリティ Filter
のリストを確認すると便利です。例: 追加したフィルターがセキュリティフィルターのリストに含まれていることを確認したい場合。
フィルターのリストはアプリケーションの起動時に INFO レベルで出力されるため、たとえばコンソール出力に次のような内容が表示されます。
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
これにより、各フィルターチェーンに構成されているセキュリティフィルターについてかなりよく理解できるようになります。
しかし、それだけではありません。リクエストごとに個別のフィルターの呼び出しを出力するようにアプリケーションを構成することもできます。これは、追加したフィルターが特定のリクエストに対して呼び出されているかどうかを確認したり、例外の発生元を確認したりできます。これを行うには、セキュリティイベントをログに記録するようにアプリケーションを構成します。
フィルターチェーンにカスタムフィルターを追加する
ほとんどの場合、アプリケーションにセキュリティを提供するには、デフォルトのセキュリティフィルターで十分です。ただし、カスタム Filter
をセキュリティフィルターチェーンに追加したい場合があります。
例: テナント ID ヘッダーを取得する Filter
を追加し、現在のユーザーがそのテナントにアクセスできるかどうかを確認するとします。前の説明では、現在のユーザーを知る必要があるため、認証フィルターの後にフィルターを追加する必要があるため、フィルターを追加する場所についての手がかりがすでに得られています。
まず、Filter
を作成しましょう。
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); (1)
boolean hasAccess = isUserAllowed(tenantId); (2)
if (hasAccess) {
filterChain.doFilter(request, response); (3)
return;
}
throw new AccessDeniedException("Access denied"); (4)
}
}
上記のサンプルコードは次のことを行います。
1 | リクエストヘッダーからテナント ID を取得します。 |
2 | 現在のユーザーがテナント ID にアクセスできるかどうかを確認します。 |
3 | ユーザーがアクセス権を持っている場合は、チェーン内の残りのフィルターを呼び出します。 |
4 | ユーザーにアクセス権がない場合は、AccessDeniedException をスローします。 |
|
ここで、フィルターをセキュリティフィルターチェーンに追加する必要があります。
- Java
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... .addFilterBefore(new TenantFilter(), AuthorizationFilter.class); (1) return http.build(); }
- Kotlin
@Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { http // ... .addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) (1) return http.build() }
1 | HttpSecurity#addFilterBefore を使用して、AuthorizationFilter の前に TenantFilter を追加します。 |
AuthorizationFilter
の前にフィルターを追加することで、認証フィルターの後に TenantFilter
が確実に呼び出されるようになります。HttpSecurity#addFilterAfter
を使用して特定のフィルターの後にフィルターを追加したり、HttpSecurity#addFilterAt
を使用してフィルターチェーン内の特定のフィルター位置にフィルターを追加したりすることもできます。
これで、TenantFilter
がフィルターチェーンで呼び出され、現在のユーザーがテナント ID にアクセスできるかどうかを確認します。
フィルターを @Component
でアノテーションを付けるか、構成内で Bean として宣言することによって、フィルターを Spring Bean として宣言する場合は注意してください。Spring Boot がフィルターを組み込みコンテナーに自動的に登録するためです。これにより、フィルターが 2 回呼び出され、1 回はコンテナーによって、もう 1 回は Spring Security によって、異なる順序で呼び出される可能性があります。
依存性注入などを利用して重複呼び出しを回避するためにフィルターを Spring Bean として宣言したい場合は、FilterRegistrationBean
Bean を宣言し、その enabled
プロパティを false
に設定することで、フィルターをコンテナーに登録しないように Spring Boot に指示できます。:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
セキュリティ例外の処理
ExceptionTranslationFilter
(Javadoc) では、AccessDeniedException
(Javadoc) および AuthenticationException
(Javadoc) を HTTP レスポンスに変換できます。
ExceptionTranslationFilter
は、セキュリティフィルターの 1 つとして FilterChainProxy に挿入されます。
次のイメージは、ExceptionTranslationFilter
と他のコンポーネントとの関連を示しています。
まず、
ExceptionTranslationFilter
がFilterChain.doFilter(request, response)
を呼び出して、残りのアプリケーションを呼び出します。ユーザーが認証されていない場合、または
AuthenticationException
の場合は、認証を開始します。SecurityContextHolder はクリアされます。
HttpServletRequest
は、認証が成功した後に元のリクエストを再生するために使用できるように保存されます。AuthenticationEntryPoint
は、クライアントから資格情報をリクエストするために使用されます。例: ログインページにリダイレクトするか、WWW-Authenticate
ヘッダーを送信する場合があります。
それ以外の場合、それが
AccessDeniedException
の場合、アクセスが拒否されました。AccessDeniedHandler
は、拒否されたアクセスを処理するために呼び出されます。
アプリケーションが |
ExceptionTranslationFilter
の擬似コードは次のようになります。
try {
filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); (2)
} else {
accessDenied(); (3)
}
}
1 | フィルターのレビューに従って、FilterChain.doFilter(request, response) を呼び出すことは、アプリケーションの残りの部分を呼び出すことと同じです。これは、アプリケーションの別の部分(FilterSecurityInterceptor またはメソッドセキュリティ)が AuthenticationException または AccessDeniedException をスローした場合、ここでキャッチされて処理されることを意味します。 |
2 | ユーザーが認証されていないか、AuthenticationException の場合は、認証を開始します。 |
3 | それ以外の場合、アクセスは拒否されました |
認証間のリクエストの保存
セキュリティ例外の処理に示されているように、リクエストに認証がなく、認証が必要なリソースに対するものである場合、認証が成功した後に再リクエストするために、認証されたリソースのリクエストを保存する必要があります。Spring Security では、これは RequestCache
実装を使用して HttpServletRequest
を保存することによって行われます。
RequestCache
HttpServletRequest
は RequestCache
(Javadoc) に保存されます。ユーザーが正常に認証されると、RequestCache
を使用して元のリクエストが再生されます。RequestCacheAwareFilter
は、RequestCache
を使用して HttpServletRequest
を保存するものです。
デフォルトでは、HttpSessionRequestCache
が使用されます。以下のコードは、continue
という名前のパラメーターが存在する場合に、保存されたリクエストの HttpSession
をチェックするために使用される RequestCache
実装をカスタマイズする方法を示しています。
continue
パラメーターが存在する場合、RequestCache
は保存されたリクエストのみをチェックします Java
Kotlin
XML
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val httpRequestCache = HttpSessionRequestCache()
httpRequestCache.setMatchingRequestParameterName("continue")
http {
requestCache {
requestCache = httpRequestCache
}
}
return http.build()
}
<http auto-config="true">
<!-- ... -->
<request-cache ref="requestCache"/>
</http>
<b:bean id="requestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache"
p:matchingRequestParameterName="continue"/>
リクエストが保存されないようにする
ユーザーの認証されていないリクエストをセッションに保存したくない理由はいくつかあります。そのストレージをユーザーのブラウザーにオフロードするか、データベースに保存することができます。または、ユーザーがログイン前にアクセスしようとしたページではなく、常にホームページにリダイレクトする必要があるため、この機能をオフにすることもできます。
これを行うには、NullRequestCache
実装 (Javadoc) を使用できます。
Java
Kotlin
XML
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
RequestCacheAwareFilter
RequestCacheAwareFilter
(Javadoc) は RequestCache
を使用して HttpServletRequest
を保存します。
ログ
Spring Security は、DEBUG および TRACE レベルですべてのセキュリティ関連イベントの包括的なログを提供します。セキュリティ対策のため、Spring Security はリクエストが拒否された理由の詳細をレスポンス本文に追加しないため、これはアプリケーションをデバッグするときに非常に役立ちます。401 または 403 エラーが発生した場合は、何が起こっているかを理解するのに役立つログメッセージが見つかる可能性が高くなります。
ユーザーが CSRF トークンを使用せずに、CSRF の保護が有効になっているリソースに対して POST
リクエストを実行しようとする例を考えてみましょう。ログがないと、ユーザーにはリクエストが拒否された理由の説明のない 403 エラーが表示されます。ただし、Spring Security のログを有効にすると、次のようなログメッセージが表示されます。
2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
CSRF トークンが見つからないため、リクエストが拒否されていることが明らかになります。
すべてのセキュリティイベントをログに記録するようにアプリケーションを構成するには、アプリケーションに以下を追加します。
logging.level.org.springframework.security=TRACE
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- ... -->
</appender>
<!-- ... -->
<logger name="org.springframework.security" level="trace" additivity="false">
<appender-ref ref="Console" />
</logger>
</configuration>