Help us understand the problem. What is going on with this article?

Spring Security 4.2 主な変更点

More than 3 years have passed since last update.

今回は11/10にリリースが予定されているSpring Security 4.2の主な変更点を紹介します(なんとなくリリースは伸びる気がするけど・・・)。「Spring Security 4.1 主な変更点」の時と同様に、公式リファレンスの「What’s New in Spring Security 4.2」で紹介されている内容を、サンプルコードを交えて具体的に説明していきます。(逆にいうと、「What’s New in Spring Security 4.2」にのっていない変更点は紹介しないので、あしからず・・・ :wink:

4.2.0.RELEASE (2016/11/10)

予想に反して!?11/10にちゃんとリリースされました :grin:
RC1のバグ修正や機能追加があったので反映しました。

動作検証環境

  • Spring Security 4.2.0.RC1 -> 4.2.0.RELEASE
  • Spring Boot 1.4.1.RELEASE -> 1.4.2.RELEASE

What’s New in Spring Security 4.2

Web Improvements

以下は、Web関連の主な変更点です。

No Web関連の主な変更点
1 認証情報などを保持するSpring Security提供のクラスを、JSONに変換するためのJacksonの拡張モジュールが提供されます。(分散セッション環境向け)
2 Referrer Policyヘッダを出力するための機能が追加されます。(★ 11/10追加)
3 DefaultHttpFirewallにHTTPレスポンス分割攻撃対策が追加されます。
4 @AuthenticationPrincipalexpression属性の中でBean参照できるようになります。
5 SSO連携用にRequestAttributeAuthenticationFilterが追加されます。
6 ロードバランサなどのProxy Serverの設定に関するドキュメンテーションが追加されます。
7 無効化されたセッションからのリクエストを受けた時の振る舞いを実装するためのインタフェース(SessionInformationExpiredStrategy)とデフォルト実装が追加されます。(同一ユーザーでの同時セッション数を制限している時に使われます)
8 複数のLogoutHandlerをひとつのLogoutHandlerとして扱うための実装クラス(CompositeLogoutHandler)が追加されます。

Configuration Improvements

以下は、Configuration関連の主な変更点です。

No Configuration関連の主な変更点
9 ロールであることを示すプレフィックス(デフォルト:ROLE_)を一元管理できるようになります。
10 Java Config(WebSecurityConfigurerAdapter)使用時に、独自のConfigurer(AbstractHttpConfigurerのサブクラス)を読み込む仕組みが追加されます。
11 XMLネームスペース(concurrency-control要素のmax-sessions属性)を使って同一ユーザーの同時セッション数を無制限(-1)にできるようになります。
12 XMLネームスペース(intercept-url要素にrequest-matcher-ref属性)が追加され、アクセスポリシーの適用対象を柔軟に指定できるようになります。
13 Map<String, List<String>>から階層化ロールを表現する文字列(例: ROLE_ADMIN > ROLE_STAFF)を作るユーティリティメソッドが追加されます。
14 CookieCsrfTokenRepositoryで扱うCookieのパスが指定できるようになります。
15 InvalidSessionStrategyの実装クラス(無効なったセッションからのリクエストを検出した時の動作)を簡単に適用できるようになります。
16 "Fix Exposing Beans for defaultMethodExpressionHandler can prevent Method Security"は、バグ修正のようなので説明は割愛します :sleepy:

Miscellaneous

以下は、その他の主な変更点です。

No その他の主な変更点
17 Spring 5上でも動作します(たぶん) ※試してませんけど :smirk:
18 Userクラスに、UserBuilderとファクトリメソッドが追加されます。
19 "Fix after csrf() is invoked, future MockMvc invocations use original CsrfTokenRepository"は、バグ修正のようなので説明は割愛します :sleepy:
20 依存ライブラリとかのバージョンがアップされます(そして説明は割愛しま〜す :wink:

Spring 4.2.0.RELEASEの適用する

まず、SPRING INITIALIZRから「Web」「Security」を指定してプロジェクトを作りましょう。

pom.xml
<!-- ... -->
<properties>
    <!-- ... -->
    <spring-security.version>4.2.0.RELEASE</spring-security.version> <!-- ← 追加 -->
</properties>
<!-- ... -->

分散セッション向けのJackson拡張モジュールが提供される

Spring Securityのデフォルト実装では、認証情報などをHTTPセッションに格納する仕組みになっており、複数のアプリケーションサーバでセッションを共有する場合は、以下のいずれかの方法を採用することになります。

  • アプリケーションサーバー間でセッション情報をレプリケーションする
  • RDBMSやKey/Valueストア(Redisなど)にセッション情報を保存する

いずれの方法を採用しても、セッション情報(Javaオブジェクト)をシリアライズ及びデシリアライズする仕組みが必要になります。もっとも標準的なのはJava標準の仕組み(java.io.Serializable)ですが、実データに対してシリアライズ後のデータサイズの増加率が多い+パフォーマンスにも難点があり、別のソリューションが必要になるケースも少なくないようです。この問題を解決するソリューションのひとつとして使われるのが、セッション情報をJSONデータに変換して共有する方法になります。

本投稿では、Spring Sessionを使用してセッション情報をRedisに格納する際に、Spring Security 4.2で追加されたJackson拡張モジュールを使用してセッション情報をJSONに変換する方法を紹介します。

Redisのインストールと起動

本投稿ではインストール方法や起動方法の説明は割愛します。ネットで調べてインストールしてください :bow_tone2:

Spring Session(Redis)の適用

まず、依存ライブラリとして「org.springframework.session:spring-session-data-redis」を追加します。

pom.xml
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

つぎに、セッションの保存先としてRedisを指定しましょう。

src/main/resources/application.properties
spring.session.store-type=redis

Note:

spring.session.store-typeは指定しなくても実は動くのですが・・・以下のようなWARNログが出てしまうので必ず指定するようにしましょう!!

2016-11-10 23:44:46.257  WARN 31968 --- [ost-startStop-1] o.s.b.a.s.RedisSessionConfiguration      : Spring Session store type is mandatory: set 'spring.session.store-type=redis' in your configuration

フォームログインの適用

Spring BootのデフォルトだとBasic認証が有効になており、ログインが成功しても認証情報がセッションに格納されないので、フォームログイン機能を有効化します。

package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .permitAll();
        http.authorizeRequests()
                .anyRequest().authenticated();
    }

}

デフォルトユーザーのパスワード変更

Spring Bootのデフォルトだと、Spring Securityのデフォルトユーザー(user)のパスワードは起動時に発行され、コンソール上に表示されます。

コンソール
...
2016-11-05 17:12:26.520  INFO 12105 --- [           main] b.a.s.AuthenticationManagerConfiguration : 

Using default security password: e825bc6d-19ef-4c76-93c7-d436a95172d2

毎回パスワードが変わるのは面倒なので、ここでは「password」に変更しちゃいましょう。

src/resources/application.properties
security.user.password=password

ログインフォームの表示

http://localhost:8080/login」にアクセスすると、Spring Security提供のログインフォームが表示されます。

spring-security-default-login.png

ログイン画面を表示すると、HTTPセッションが生成されRedisに保存されます。今回はMedisというRedisのクライアントツールを使ってRedisの中身を覗いてみます。 (ツールはなんでもOKです)

medis-bin.png

画面に表示されているのは、HTTPセッションに格納したCSRFトークンを保持するクラスの中身です。・・・・中身がバイナリデータなのがわかるとおもいます。

Jackson拡張モジュールの適用

Spring Security 4.2で追加されたJackson拡張モジュールを使用して、Spring Sessionの中で利用しているSpring Data Redisの中でオブジェクトをJSONにシリアライズするようにしてみましょう。

package com.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.security.jackson2.SecurityJackson2Modules;

@Configuration
public class HttpSessionConfig implements BeanClassLoaderAware {

    private ClassLoader classLoader;

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader)); // Spring Securityの拡張モジュールの適用
        return new GenericJackson2JsonRedisSerializer(objectMapper); // Spring Data RedisにJSON変換用のコンポーネントを適用
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

}

Note:

Spring Bootを使わない場合は、@Configurationの代わりに@org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSessionを指定してください!!

Jackson拡張モジュールを適用した後に、Redisの中身をクリアして再度ログイン画面を表示してみましょう。バイナリデータがJSON形式のデータになっていますね :v:

medis-json.png

さらに、ログイン後にRedisの中を覗くと、認証情報もJSONに変換されていることが確認できます :grin:

medis-json-after-login.png

Warning:

RC1の時点では、事前認証関連のクラス(PreAuthenticatedAuthenticationTokenなど)がサポートされていないみたいで、事前認証を使うとRedisからロードした認証情報(JSON)をパースする時にエラーになってしまいます(=認証後のアクセスでエラーになる・・・)。とりあえず、GitHubのIssueにコメントを残しておきました。

4.2.0.RELEASEで解消されました!!

Referrer Policyヘッダの出力機能が追加される

Spring Security 4.2より、Referrer Policyをブラウザに指示するためのレスポンスヘッダを出力する機能が追加されます。なお、この機能はデフォルトでは適用されないので、適用したい場合は以下のようなBean定義を行う必要があります。

JavaConfigによるBean定義例
@Override
protected void configure(HttpSecurity http) throws Exception {
    // ...
    http.headers()
            .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN);
    // ...
}
XMLによるBean定義例
<sec:http>
    <!-- ... -->
    <sec:headers>
        <sec:referrer-policy policy="same-origin"/>
    </sec:headers>
    <!-- ... -->
</sec:http>

以下のレスポンスヘッダが表示されます。

レスポンスヘッダの出力例
HTTP/1.1 200
...
Referrer-Policy: same-origin
...

HTTPレスポンス分割攻撃対策が追加される

HTTPレスポンス分割攻撃(HTTP Response Splitting Attack)は、HTTPヘッダ・インジェクション攻撃により複数のHTTPレスンポンスを作り出してキャッシュサーバ(プロキシサーバ)に偽のコンテンツをキャッシュさせることで、悪意のあるページを閲覧させるという攻撃手法です。Spring Secuirty 4.2からは、レスポンスヘッダの値に改行コードが含まれているとIllegalArgumentExceptionになります。

@AuthenticationPrincipalexpression属性の中でBean参照できる

@AuthenticationPrincipalexpression属性の中でBean参照できるようになったことで、認証情報を引数にして任意のBeanのメソッドを呼び出し、メソッドの返り値をHandlerメソッドの引数として受け取ることができるようになります。
Spring SecurityのリファレンスではJPAのEntityManagerとの連携例が紹介されていますが、本投稿では、認証情報に紐づくプロファイル情報(電話番号、メールアドレス)を検索してHandlerメソッドの引数として受け取る例を紹介します。

プロファイル検索サービスの作成

まず、ユーザ名を受け取ってプロファイル情報を返却するメソッドを作成しましょう。本来であればデータベースから検索するところですが、本投稿では固定値を返しちゃいます。

package com.example.service;

import com.example.domain.Profile;
import org.springframework.stereotype.Service;

@Service
public class ProfileService {

    public Profile find(String username) {
        Profile profile = new Profile();
        profile.setUsername(username);
        profile.setPhone("09012345678");
        profile.setEmail("test@gmail.com");
        return profile;
    }

}

プロファイル検索サービスの呼び出し

@AuthenticationPrincipalexpression属性の中からプロファイル検索サービスのBeanを参照し、検索メソッドを呼び出した結果をHandlerメソッドの引数として受け取りましょう。

package com.example.controller;

import com.example.domain.Profile;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/profile")
public class ProfileRestController {

    @GetMapping
    public Profile getProfile(
            @AuthenticationPrincipal(expression = "@profileService.find(username)") Profile profile) {
        return profile;
    }

}

ブラウザから「http://localhost:8080/profile」にアクセスして、ログインすると以下のJSONが返却されます。

{"username":"user","phone":"09012345678","email":"test@gmail.com"}

RequestAttributeAuthenticationFilterが追加される

AbstractPreAuthenticatedProcessingFilterのサブクラスとして、リクエストスコープから資格情報を取得して事前認証を行うRequestAttributeAuthenticationFilterクラスが追加されます。このクラスを使うことで、

といったSSO(Single Sign-On)のプロダクトと連携することができます。

Note:

類似のクラスとしては、リクエストヘッダから資格情報を取得するクラス(RequestHeaderAuthenticationFilter)があります。

SSO(Single Sign-On)のプロダクトと実際に連携するのはちょっと面倒なので・・・本投稿では、擬似的に同じ状態を作り出して動作を確認することにします。

package com.example.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.RequestAttributeAuthenticationFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    AuthenticationManager authenticationManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .permitAll();
        http.authorizeRequests()
                .anyRequest().authenticated();
        // ↓ 事前認証用のフィルターの適用(フォーム認証より優先)
        http.addFilterBefore(preAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    // 事前認証用のフィルターをセットアップ
    private RequestAttributeAuthenticationFilter preAuthenticationFilter() {
        RequestAttributeAuthenticationFilter filter = new RequestAttributeAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationDetailsSource(
                request -> new PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails(
                        request, AuthorityUtils.createAuthorityList("ROLE_USER")));
        filter.setExceptionIfVariableMissing(false);
        return filter;
    }

    // 事前認証用の認証プロバイダーをセットアップ
    @Autowired
    void configurePreAuthenticatedAuthenticationProvider(AuthenticationManagerBuilder builder) {
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedGrantedAuthoritiesUserDetailsService());
        builder.authenticationProvider(provider);
    }

    // 擬似的にSSOプロダクトを通過した状態を作り出すためのフィルター
    // 認証が必要なリソースに対して「?user=xxxx」を付与するとSSOプロダクトを通過した状態になる
    @Component
    @Order(Ordered.HIGHEST_PRECEDENCE)
    static class SsoUsernameStoringFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String user = request.getParameter("user");
            if (user != null) {
                request.setAttribute("REMOTE_USER", user);
            }
            filterChain.doFilter(request, response);
        }
    }

}

事前認証適用後に前セクションで作成した「/profile」にアクセスすると、ログインフォームを使用した認証を行わずに「/profile」にアクセスすることができます。

$ curl -D - http://localhost:8080/profile?user=kazuki43zoo
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: SESSION=acffc6c9-d555-4da5-828d-ec4e20a937d6;path=/;HttpOnly
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 05 Nov 2016 15:07:39 GMT

{"username":"kazuki43zoo","phone":"09012345678","email":"test@gmail.com"}

事前認証が成功するとSpring Secuirtyの認証情報はセッションに格納され、以降のリクエストでは認証処理が不要になります。これを確認するために、2回目のリクエストでは、「?user=xxxxx」を省略してみましょう。

$ curl -D - http://localhost:8080/profile -H 'Cookie: SESSION=acffc6c9-d555-4da5-828d-ec4e20a937d6'
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 05 Nov 2016 15:15:46 GMT

{"username":"kazuki43zoo","phone":"09012345678","email":"test@gmail.com"}

今度は違うユーザー名を指定してみましょう。

$ curl -D - http://localhost:8080/profile?user=hoge -H 'Cookie: SESSION=acffc6c9-d555-4da5-828d-ec4e20a937d6'
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 05 Nov 2016 15:16:47 GMT

{"username":"kazuki43zoo","phone":"09012345678","email":"test@gmail.com"}

既に同一セッション内に認証情報があるため、認証処理が行われないことがわかります。

Proxy Server関連のドキュメンテーションが追加される

Spring Securityのリファレンスページに、ロードバランサなどのProxy Server利用時の設定に関するドキュメンテーションが追加されます。

SessionInformationExpiredStrategyが追加される

無効化されたセッションからのリクエストを受けた時の振る舞いを実装するためのインタフェース(SessionInformationExpiredStrategy)とデフォルト実装が追加されます。追加されたインタフェースやクラスは、同一ユーザーでの同時セッション数を制限していて+同時セッション数を超えた時にもっともアクセスがないセッションを無効化する設定になっている時です。

デフォルトで使われる実装クラスは、ConcurrentSessionFilterの内部クラスとして実装されているResponseBodySessionInformationExpiredStrategyで、レスポンスBODYに「This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).」が出力されます。

また、無効化されたセッションからのリクエストを受けた際に、指定したURLにリダイレクトする設定にするとSimpleRedirectSessionInformationExpiredStrategyというクラスが使われます。

@Override
protected void configure(HttpSecurity http) throws Exception {
    // ...
    http.sessionManagement()
            .maximumSessions(5)
            .maxSessionsPreventsLogin(false)
            .expiredUrl("/login?session-expired");
    // ...
}

なお、Spring Securityが用意している実装で要件が充たせない場合は、独自の実装を適用することもできます。

@Override
protected void configure(HttpSecurity http) throws Exception {
    // ...
    http.sessionManagement()
            .maximumSessions(5)
            .maxSessionsPreventsLogin(false)
            .expiredSessionStrategy(expiredSessionStrategy());
    // ...
}

private SessionInformationExpiredStrategy expiredSessionStrategy() {
    return event -> {
        // 要件を満たすための独自処理を実装
    };
}

CompositeLogoutHandlerが追加される

複数のLogoutHandlerをひとつのLogoutHandlerとして扱うための実装クラス(CompositeLogoutHandler)が追加されます。おそらく・・・このクラスを直接使うことはあまりないと思うので、本投稿では説明は割愛させてもらいます。

ロールであることを示すプレフィックスが一元管理できる

Spring Securityのデフォルトでは、ロールベースの権限を"ROLE_"ではじまる文字列として管理しています。
たとえば、あるユーザーに「利用者」と「管理者」のロールを割り当てる場合は、「ROLE_USER」「ROLE_ADMIN」といった感じのロール名を権限情報として保持するようにします。

デフォルト状態でのロールの付与例
@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User(username, "N/A", AuthorityUtils.createAuthorityList("ROLE_USER")); // "ROLE_"が必要
   }
}
アクセスポリシーの指定例
@Override
protected void configure(HttpSecurity http) throws Exception {
    // ...
    http.authorizeRequests()
            .anyRequest().hasRole("USER"); // "ROLE_"は省略できる(Spring Security 4.0〜)
    // ...
}

デフォルトのプレフィックス("ROLE_")文字はカスタマイズすることができ、Spring Security 4.2からは、org.springframework.security.config.core.GrantedAuthorityDefaultsのBeanを定義するだけで実現できます。ここでは、プレフィックス("ROLE_")をなくす場合の設定例を紹介しましょう。

プレフィックをなくす際の設定例
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
    return new GrantedAuthorityDefaults("");
}

上記の設定を行うと、認証情報に保持するロールにプレフィックス(デフォルトでは"ROLE_")を指定する必要がありません。

プレフィックスをなくした時のロールの付与例
@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User(username, "N/A", AuthorityUtils.createAuthorityList("USER")); // "ROLE_"は不要
   }
}

Spring Security 4.0より、リソースに対してアクセスポリシーを指定する際にロール名からプレフィックスを省略できるように改善されており、認証情報で保持するロール名からもプレフィックス("ROLE_")を外したいと思った方もいるのではないでしょうか? Spring Security 4.2から導入されたこの仕組みを使うと簡単にプレフィックスをなくすことができます!

Warning:

認証情報で保持するロール名からプレフィックスをなくすことができると紹介しましたが・・・、hasRoleメソッドを使ってアクセスポリシーを指定していると正しく動作しないことがわかっています・・・:cold_sweat:

http.authorizeRequests()
       .anyRequest().hasRole("USER"); // ← hasRoleメソッドの中で"ROLE_"を固定で付与している・・・

この問題の回避策としては・・・、accessメソッドを使用し、ExpressionとしてhasRoleメソッドを指定することで正しく動作させることができます。

http.authorizeRequests()
       .anyRequest().access("hasRole('USER')"); // Expression経由だとプレフィックスのカスタマイズ内容が反映される

これはSpring Securityのバグ?な気がするので・・・
とりあえず・・・GitHubのIssueにコメントを残しておきました。

11/16追記
Robから別途Issueを作って!!とコメントが返ってきたので作りました。
https://github.com/spring-projects/spring-security/issues/4134

独自のSecurityConfigurerをデフォルトで適用できるようになる

Java Configを使ってSpring Securityのコンフィギュレーションを行う場合、WebSecurityConfigurerAdapterを継承したJava Configクラスを作成し、WebSecurityConfigurerAdapterクラスのメソッドをオーバライドしてコンフィギュレーションするのが一般的です。具体的には、以下のようなコンフィギュレーションクラスを作成してコンフィギュレーションを行います。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .permitAll();
        http.logout()
                .permitAll();
        http.authorizeRequests()
                .anyRequest().authenticated();
    }

}

では、SecurityConfigurerとは何者なのでしょうか?
実は・・・いつも何気なくつかっている、formLoginlogoutauthorizeRequestsメソッドなどから返却されるオブジェクトがSecurityConfigurerインタフェースを実装しており、開発者が指定した設定内容に応じてSpring Securityの各機能のセットアップを行う役割を担います。この仕組みのおかげで、我々開発者はたった数行のコンフィギュレーションを行うだけで、Spring Securityが提供する様々な機能を簡単に利用することができるのです。

本投稿では、Spring Security 4.2でサポートされた仕組みを紹介する前に、SecurityConfigurerの作成方法と適用例を簡単に紹介します。

独自のSecurityConfigurerの作成

Spring Securityから様々なSecurityConfigurerインタフェースの実装クラスが提供されており、多くのアプリケーションではそれらのクラスを使ってSpring Securityのセットアップができると思います。とはいえ・・・Spring Securityも万能ではないため、どうしても独自のカスタマイズが必要になることがあります。たとえば・・・本投稿で紹介しているRequestAttributeAuthenticationFilterを使った事前認証を行う際には、事前認証を適用するために独自のBean定義が必要になります。ひとつ又は数個のアプリケーションに対して適用するだけならアドホックにBean定義してしまってもよいでしょうが、これが数十、数百というアプリケーションに対して同じ設定を適用したい場合は、独自のSecurityConfigurerの作成を検討してみるのがよいと思います。

ここでは、RequestAttributeAuthenticationFilterを使った事前認証のところでアドホックにBean定義した部分を、SecurityConfigurerの実装クラスに置き換えてみます。

package com.example.config;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.RequestAttributeAuthenticationFilter;

public class MyPreAuthenticationConfigurer extends AbstractHttpConfigurer<MyPreAuthenticationConfigurer, HttpSecurity> {

    @Override
    public void init(HttpSecurity http) throws Exception {
        // 事前認証用の認証プロバイダーをセットアップ
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedGrantedAuthoritiesUserDetailsService());
        http.authenticationProvider(provider);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // ↓ 事前認証用のフィルターの適用(フォーム認証より優先)
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        http.addFilterBefore(createPreAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
    }

    private RequestAttributeAuthenticationFilter createPreAuthenticationFilter(AuthenticationManager authenticationManager) {
        RequestAttributeAuthenticationFilter filter = new RequestAttributeAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationDetailsSource(
                request -> new PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails(
                        request, AuthorityUtils.createAuthorityList("ROLE_USER")));
        filter.setExceptionIfVariableMissing(false);
        return filter;
    }

}

Note:

開発者にカスタマイズポイントを提供する場合は、setterメソッドなどを提供し、外部から設定された内容に応じてBean定義するのがよいでしょう。(本投稿では説明をシンプルにするために、カスタマイズポイントはあえて提供していません)

独自のSecurityConfigurerのマニュアル適用

作成したSecurityConfigurerインタフェースの実装クラスを使ってSpring Securityをセットアップしたい場合は、HttpSecurityapplyメソッドを使います。(Spring Security 4.1以前でも利用可能な方法です)

SecurityConfigurerの適用例
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.apply(new MyPreAuthenticationConfigurer());
    }
}

Note:

Spring Securityのリファレンスページで紹介されているように・・・ファクトリメソッドを作って(+staticインポートして)以下のようなスタイルにしてもよいでしょう。(好みの問題かなと・・・)

http.apply(preAuth());

独自のSecurityConfigurerのデフォルト適用

Spring Security 4.2より、デフォルトで適用したいSecurityConfigurerインタフェースの実装クラスを、クラスパス内の/META-INF/spring.factoriesに指定できるようになります。ただし・・・この仕組みを利用する場合は、SecurityConfigurerの実装クラスはorg.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurerを継承して作成する必要があります。さもないと・・・起動時にエラーになってしまいます。

src/resources/META-INF/spring.factories
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = com.example.config.MyPreAuthenticationConfigurer

なお、デフォルト適用される設定をカスタマイズしたい場合や、何かしらの理由でデフォルト適用外にしたい場合は・・・前述のマニュアル適用を行うことで対応することができます。

デフォルト設定をカスタマイズする際の設定例
http.apply(new MyPreAuthenticationConfigurer())
        .principalEnvironmentVariable("AUTH_USER"); // カスタマイズ用のsetterメソッドなどを別途用意しておく
デフォルト適用外にする設定例
http.apply(new MyPreAuthenticationConfigurer())
        .disable(); // disableメソッドを呼ぶと無効化できる

XMLのBean定義で同一ユーザーの同時セッション数を無制限にできる

Spring Security 4.2より、XMLネームスペース(concurrency-control要素のmax-sessions属性)を使って同一ユーザーの同時セッション数を無制限(-1)にできるようになります。Spring Security 4.1以前から機能的には無制限にすることができましたが、XMLネームスペースを使って無制限(-1)にすることができませんでした。

<sec:http>
    <!-- ... -->
    <sec:session-management>
        <sec:concurrency-control max-sessions="-1" />
    </sec:session-management>
</sec:http>

intercept-url要素にrequest-matcher-ref属性が追加される

XMLネームスペース(intercept-url要素にrequest-matcher-ref属性)が追加され、アクセスポリシーの適用対象を柔軟に指定できるようになります。Java Configでは以前からサポートされていましたが、アクセスポリシーの適用対象を指定する際に、XMLでもRequestMatcherを指定できるようになります。

<sec:http>
    <!-- ... -->
    <sec:intercept-url request-matcher-ref="adminRolePathMatcher" access="hasRole('ADMIN')"/>
</sec:http>

<bean id="adminRolePathMatcher" class="org.springframework.security.web.util.matcher.AndRequestMatcher">
    <constructor-arg>
        <list>
            <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
                <constructor-arg value="/admin/**" />
            </bean>
            <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
                <constructor-arg value="/config/**" />
            </bean>
            <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
                <constructor-arg value="/monitor/**" />
            </bean>
        </list>
    </constructor-arg>
</bean>

Mapから階層化ロールを表現する文字列を簡単に作れる

Map<String, List<String>>から階層化ロールを表現する文字列(例: ROLE_ADMIN > ROLE_STAFF)を作るユーティリティメソッドが追加されます。これは、Spring BootのType-safe Configuration Propertiesとの連携を強く意識仕組みで、以下のようなプロパティクラスを作成してRoleHierarchyのBeanを定義を行うだけで、簡単に階層化ロールを利用することができます。
本投稿では、Spring Security 4.2で追加された仕組みを活用して、以下のような階層化ロールを表現する文字列を生成するための定義例を紹介します。

ROLE_ADMIN > ROLE_MANAGER
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST
プロパティクラスの作成例
package com.example.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@ConfigurationProperties("security.roles")
@Component
public class SecurityRolesProperties {

    private Map<String, List<String>> hierarchyMap = new LinkedHashMap<>();

    public Map<String, List<String>> getHierarchyMap() {
        return hierarchyMap;
    }

    public void setHierarchyMap(Map<String, List<String>> hierarchyMap) {
        this.hierarchyMap = hierarchyMap;
    }

    public RoleHierarchy getRoleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy(CollectionUtils.isEmpty(hierarchyMap) ? ""
                : RoleHierarchyUtils.roleHierarchyFromMap(hierarchyMap)); // Mapから階層化ロール文字列に変換
        return roleHierarchy;

    }

}
RoleHierarchyのBean定義例
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Bean
    RoleHierarchy roleHierarchy(SecurityRolesProperties rolesProperties) {
        return rolesProperties.getRoleHierarchy();
    }
}

プロパティやYAMLファイルを使用して、ロールの階層化定義を行います。

プロパティファイルを使用した定義例(src/resources/application.properties)
security.roles.hierarchyMap.ROLE_ADMIN = ROLE_MANAGER,ROLE_STAFF
security.roles.hierarchyMap.ROLE_STAFF = ROLE_USER
security.roles.hierarchyMap.ROLE_USER = ROLE_GUEST

or

YAMLファイルを使用した定義例(src/resources/application.yml)
security:
  roles:
    hierarchyMap:
      ROLE_ADMIN:
        - ROLE_MANAGER
        - ROLE_STAFF
      ROLE_STAFF:
        - ROLE_USER
      ROLE_USER:
        - ROLE_GUEST

CookieCsrfTokenRepositoryで扱うCookieのパスが指定できる

Spring Security 4.1で追加されたCookieCsrfTokenRepositoryですが、CookieのパスにはWebアプリケーションのコンテキストパスが固定で設定される仕組みになっていました。Spring Security 4.2からは、Cookieのパスに設定する値を明示的に指定することができるようになります。

@Override
protected void configure(HttpSecurity http) throws Exception {
    // ...
    http.csrf()
            .csrfTokenRepository(csrfTokenRepository());

}

private CsrfTokenRepository csrfTokenRepository() {
    CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository();
    repository.setCookiePath("/foo"); // 明示的にパスを指定できる
    return repository;
}

InvalidSessionStrategyの実装クラスを簡単に適用できる

Spring Security 4.2から、InvalidSessionStrategyの実装クラス(無効なったセッションからのリクエストを検出した時の動作)を簡単に適用できるようになります。これまでは、無効なったセッションからのリクエストを検出した時に表示するURLの指定しかできませんでした。

Note:

Java Configの方は4.1からサポートされていたような気がするけど・・・・

JavaConfigを使用した定義例
@Override
protected void configure(HttpSecurity http) throws Exception {
    // ...
    http.sessionManagement()
            .invalidSessionStrategy(invalidSessionStrategy());
    // ...
}

private InvalidSessionStrategy invalidSessionStrategy() {
    return (request, response) -> {
        // 要件を満たすための独自処理を実装
    };
}
XMLを使用した定義例
<sec:http>
    <!-- ... -->
    <sec:session-management invalid-session-strategy-ref="invalidSessionStrategy" />
    <!-- ... -->
</sec:http>

<bean id="invalidSessionStrategy" class="com.example.CustomInvalidSessionStrategy" />

UserBuilderとファクトリメソッドが追加される

Userクラスに、UserBuilderとファクトリメソッドが追加されます。

@Bean
public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
    userDetailsManager.createUser(User.withUsername("kazuki43zoo").password("password").roles("USER").build());
    return userDetailsManager;
}

まとめ

今回はこれからリリースされるSpring Security 4.2の主な変更点を紹介しました。Spring Security 4.1に引き続きコンフィギュレーションが簡単になったり、いくつか新機能も追加されています!!(Spring 4.1にくらべるとインパクトのある変更少ない気はしますが・・・)。
既に「Spring Security Reactive」の開発も始まっているので、そちらの動向も見逃せないところです :wink:

参考サイト

kazuki43zoo
Javaエンジニアで、SpringやMyBatisらへんにそれなりに詳しいです。お仕事のつながりで「Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発」を共著させてもらいました!
https://kazuki43zoo.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした