読者です 読者をやめる 読者になる 読者になる

シスアーキ in はてな

シスアーキ(自称)の技術ブログ

Spring Securityのお話

ハーイ!僕です。

最近、Spring Securityと戦っています。今のプロジェクトでセキュリティまわりを僕が担当しているのでね。
Spring Frameworkを勉強しはじめて、最初につまづいたのがSpring Securityです。いやね、これね、ちょっと僕には難しすぎるんじゃない?

人類には早すぎたのだ!!*1

と、突然机の前で発狂しながら仕事してます。周りの人はびっくりさせてごめんなさい。
これ以上暴れてまわりの人から変人扱いされても困るので、自分なりにSpring Securityについて纏めてみようと思います。

SpringSecurityを理解する上では、

  • Filterのお話
  • Configのお話
  • Configurationのお話

がそれぞれあると思っているので、その順番で〜

Filterのお話

Spring Securityは主にサーブレットフィルターをもちいてWebアプリケーションのセキュリティ機能を実現しています。
早速UMLを書いてみましょう!

f:id:kozake:20160911203222p:plain

かっこいいですね!
なんだかSpring Securityをマスターした気分になります。
Filterはインタフェースなので、セキュリティ機能を実現するには具体的な実装が必要です。

FilterChainProxy

Spring Securityの主役は、「springSecurityFilterChain」という名前でBean定義されたサーブレットフィルターです。
そしてそのインスタンスはFilterChainProxyクラスのオブジェクトです。

f:id:kozake:20160912083230p:plain

Proxy(代理)という名前が付いているとおり、このクラス自身は具体的なセキュリティ機能を提供していません。

SecurityFilterChain

では、どのようにセキュリティ機能を提供しているのか?
SecurityFilterChainインタフェースに処理を委譲することで実現しています。

f:id:kozake:20160912082106p:plain

FilterChainProxyクラスは複数のSecurityFilterChainインタフェースを保持してます。
SecurityFilterChainはmatches()メソッドとgetFilters()メソッドを定義したインタフェースです。

FilterChainProxyがクライアントからリクエストを受けると、順番にSecurityFilterChainのmatches()メソッドを呼び出します。
matches()メソッドからtrueが返却されたSecurityFilterChainに対して、getFilters()メソッドを呼び出し、返却されたFilter群に処理を委譲することでセキュリティ機能を実現しています。

下に示すのが、そのコードの一部です。

private void doFilterInternal(
    ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {
            :

    List<Filter> filters = getFilters(fwRequest);
            :

    VirtualFilterChain vfc = new VirtualFilterChain(
        fwRequest, chain, filters);
    vfc.doFilter(fwRequest, fwResponse);
}

private List<Filter> getFilters(HttpServletRequest request) {
    for (SecurityFilterChain chain : filterChains) {
        if (chain.matches(request)) {
            return chain.getFilters();
        }
    }

    return null;
}


この内容からわかるとおり、複数のSecurityFilterChainを用いることで、リクエスト毎に異なるセキュリティ機能を適用することが可能となります。
例えば「public/**」に一致するURLに送信されたリクエストにはSecurity機能は適用しないけど、「private/**」に一致するURLに送信されたリクエストにはセキュリティ機能を適用するなどです。

なるほど、かっこいいですね!

コードからわかるように、最初にmatchしたSecurityFilterChainに処理を委譲する為、順番が重要になります。ここら辺の話はWebSecurityConfigurerAdapterのところで出てきます。
なお、上のクラス図の「AnySecurityFilter」は実際の存在するクラスではなく、認証・認可など、各種セキュリティ機能を提供するFilter実装クラスの総称です。

HttpFirewall

少し話はそれますが、FilterChainProxyクラスはHttpFirewallを保持しています。このインタフェースの機能を用いることで、ディレクトリトラバーサル攻撃やHTTPレスポンス分割攻撃に対するセキュリティを実現しています。このインタフェースのデフォルト実装はDefaultHttpFirewallクラスです。これらの機能はサーブレットリクエストやサーブレットレスポンスのラッパーを用いることで実現しているようです*2

DelegatingFilterProxy

FilterChainProxyはspringSecurityFilterChainというフィルタ名で定義されたDelegatingFilterProxyクラスから呼ばれます。

f:id:kozake:20160911214358p:plain

ちょっとここら辺は混乱しました。通常、Bean定義されたサーブレットフィルタは自動的にサーブレットコンテナに登録されるみたいですが、どうも同じフィルタ名で定義されたフィルタがすでに存在している場合、自動登録の対象にはならないようです。
DelegatingFilterProxyクラスは自身のフィルタ名でBean定義されたサーブレットフィルタをDIコンテナから取り出して、そのサーブレットフィルタに処理を委譲するみたいです。
ややこしいですね。
おそらく、Servlet 3.0以降からサーブレットコンテナの初期化処理をJavaのコードで行うことが出来るようになったので、それ以前はDelegatingFilterProxyのようなBean定義されたサーブレットフィルタを取り出して実行するような仕組みが必要だったのだと思います。

Filterまわりの全体図

今までの話の全体図が次のとおりです。

f:id:kozake:20160913205625p:plain

Configのお話

WebSecurityとHttpSecurity

FilterChainProxyはWebSecurityクラスでインスタンス生成されます。
SecurityFilterChainの実装はDefaultSecurityFilterChainクラスで、WebSecurityクラスやHttpSecurityクラスから作成されます。WebSecurityクラスもHttpSecurityクラスもクラス名からは分かりづらいですが、ビルダークラスです。

f:id:kozake:20160911220005p:plain

WebSecurityクラスやHttpSecurityクラスには、サーブレットを生成する上で便利なメソッドがたくさん用意されています。このクラスの操作を理解することで、自由自在にシステム要件にあったセキュリティ機能が構築できるのですよ!

そして、WebSecurityクラスやHttpSecurityクラスのメソッドを実際に呼び出して設定するのがConfigureクラスになります。

WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapterクラスは1つのHttpSecurityクラスのインスタンスを生成します。このクラスは継承して使われることを想定されていますが、このクラスの継承クラスの数だけ、SecurityFilterChainが出来上がると思っていいでしょう。なお、SecurityFilterChainは順番が重要だという話をしました。WebSecurityConfigurerAdapterはOrderアノーテーションなどを用いることで、順番を定義します。その順番に従い、SecurityFilterChainのmatches()メソッドが呼ばれる順番が決まります。なお、デフォルトの順序は100になっています。

f:id:kozake:20160913211941p:plain

Configurationのお話

最後にセキュリティ環境の話。
WebSecurityConfigurationクラスがspringSecurityFilterChainという名前でBean定義しており、FilterChainProxyのインスタンスをwebSecurityのbuild()メソッドで生成しています。また、このクラスがWebSecurityConfigurerAdapterの継承クラスと連携して、webSecurityの設定処理を呼び出します。

f:id:kozake:20160913220113p:plain

下に示すのが、そのコードの一部です。AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAMEは、"springSecurityFilterChain"という文字列のコンスタント値です。

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
            :

     return webSecurity.build();
}

このクラスがConfiguration定義されることで、セキュリティ機能が有効になるんですね!そして、EnableWebSecurityアノーテーションがこのConfiguration定義をimportしています。つまり、EnableWebSecurityアノーテーションを定義すると、セキュリティ機能全般が有効になるということなのです!

まるで魔法のようだ!

さらにさらに、SpringBootWebSecurityConfigurationがこのEnableWebSecurityアノーテーションを自動定義する機能を備えています。SpringBootで自動的にセキュリティ機能が有効にできるのは、SpringBootWebSecurityConfigurationのおかげなのです!

まとめ

今回作成した全体のクラス図です。

f:id:kozake:20160913213406p:plain

今回はSpring Securityのアーキテクチャ部分に特化した記事です。Spring Securityを抑えるなら、UserDetailsServiceやAuthenticationProviderまわりの話も抑えておいたほうがいいようです。
俺たちの戦いは始まったばかりなので、気が向いたら次はSpring Security OAuth2まわりの話を書きます。

それでは、バイナリー!

*1:たんに僕の精進不足なだけです

*2:ここら辺はSpring Securityを使う時にほとんど意識する必要がありません