62
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

Spring Security 4.1 主な変更点

今回はリリースから約1ヶ月経過したSpring Security 4.1の主な変更点を紹介します。「Spring 4.3の変更点」と同様に、公式リファレンスの「What’s New in Spring Security 4.1」で紹介されている内容を、サンプルコードを交えて具体的に説明していきます。(逆にいうと、「What’s New in Spring Security 4.1」にのっていない変更点は紹介しないので、あしからず・・・ :wink:

そもそもSpring Securityって・・・という方は、「TERASOLUNA Server Framework (5.x)のガイドライン」を読もう!!Spring Secuirty 4.0ベースでのSpring Securityの基本的な使い方、スタートアップ用のチュートリアル、具体的なセキュリティ要件を充たすための実装サンプルが提供されており、体系的にSpring Securityのアーキテクチャと使い方が学べます :thumbsup:
Spring Boot前提ではない+XMLベースのコンフィギュレーション例しか載ってないのが少しだけ残念ですが、そこはSpring BootやSpring Securityの公式リファレンスを参照して読み替えましょう!!

4.1.1.RELEASE (4.1.2.RELEAE)

  • Spring 4.1.1.REALEASEで追加されたMvcRequestMatcherの説明を追加しました。(差分は、「★8/13追加」でマークしてあります)

動作検証環境

  • Spring Security 4.1.0.RELEASE
  • Spring Boot 1.4.0.BUILD-SNAPSHOT (2016/6/5時点)

What’s New in Spring Security 4.1

Spring Securityの公式リファレンスでは、以下の6つのセクションで改善点が紹介されています。Spring 4.3の投稿ではセクション毎に投稿をわけましたが、今回はいっきに説明します!!

Java Configuration Improvements

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

No JavaConfig関連の主な変更点
1 カスタマイズしたUserDetailsServiceをSpring Securityに適用するためのBean定義がちょ〜簡単になりました。(ってかDIコンテナに登録しちゃえば自動検出されちゃう :v:
2 カスタマイズしたAuthenticationProviderをSpring Securityに適用するためのBean定義がちょ〜簡単になりました。(こちらもDIコンテナに登録しちゃえば自動検出されちゃう :v:
3 LogoutSuccessHandlerをリクエストの種類毎(RequestMatcher毎)に適用するためのメソッドが追加されました。この変更により、クライアントの種類に応じてログアウト成功時のレスポンス(ステータスコードのみ、HTML、JSONなど)を切り替えられます。
4 カスタマイズしたInvalidSessionStrategyをSpring Securityに適用するためのメソッドが追加されました。
5 サーブレットフィルタを指定した登録済みのフィルタと同じポジション(優先順)に追加するためのメソッドが追加されました。

Web Application Security Improvements

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

No Webアプリ関連の主な変更点
6 Webリソースへの認可にて、Spring MVCのリクエストマッピングのルールと連動したRequestMatcher(MvcRequestMatcher)が追加されました。 「★8/13追加」
7 XSS攻撃対策として、「Content Security Policy (CSP)」用のHTTPレスポンスヘッダの出力がサポートされました。
8 偽造された証明書による中間者攻撃対策として、「HTTP Public Key Pinning (HPKP)」用のHTTPレスポンスヘッダの出力がサポートされました。
9 CSRFトークンの保存先としてCookieがサポートされました。これは、AngularJSのCSRF対策機能のデフォルト動作をサポートするために追加されたようです。
10 AuthenticationSuccessHandlerAuthenticationFailureHandlerにて、遷移先にフォワードする実装クラスが追加されました。この変更により、遷移先でリクエストスコープの情報にアクセスすることが可能になります。
11 @AuthenticationPrincipalexpression属性が追加されました。expression属性を利用すると、Spring MVCのHandlerメソッドの引数に、認証ユーザー情報(Authentication#getPrincipal()メソッドの返り値)が保持する任意のプロパティ値をインジェクションすることができます。

Authorization Improvements

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

No 認可関連の主な変更点
12 Webリソースへの認可にて、(待望の・・・)パス変数値を参照した認可制御が可能になりました :thumbsup:
13 Javaメソッドへの認可時に使用するアノテーション(@PreAuthorizeなど)がメタアノテーションとして使用できるようになりました。

Crypto Module Improvements

以下は、暗号化関連の主な変更点です。

No 暗号化関連の主な変更点
14 SCryptアルゴリズムをサポートしたPasswordEncoderの実装クラスが追加されました。
15 PBKDF2アルゴリズムをサポートしたPasswordEncoderの実装クラスが追加されました。

Testing Improvements

以下は、テスト関連の主な変更点です。

No テスト関連の主な変更点
16 匿名ユーザーの認証情報をセットアップするためのアノテーション(@WithAnonymousUser)が追加されました。
17 @WithUserDetailsuserDetailsServiceBeanName属性が追加されました。userDetailsServiceBeanName属性を使用すると、テスト時に使用するUserDetailsServiceのBeanを明示的に指定することができます。
18 認証情報などを指定するアノテーション(@WithMockUserなど)をメタアノテーションとして使用できるようになりました。
19 SecurityMockMvcResultMatchers#withAuthoritiesメソッドで、GrantedAuthorityのサブクラスをキャストなしで扱えるようになりました??(公式リファレンスに記載されている内容となんとなく違い気がするけど・・・たぶんあっていると思う・・・ :sweat_smile:

General Improvements

以下は、Spring Securityプロジェクトの(非機能的な)変更点です。

No Spring Securityプロジェクトの変更点
20 サンプルプロジェクトが再編された模様です。 (Issue#3752)
21 Issueトラッキングが、Spring JIRAからGitHubに移管されました。

動作検証用のプロジェクト準備

今回紹介する内容の動作検証は、Spring Boot 1.4(Spring Security 4.1)で行います。実際にアプリ上で動作を確認したい方は、「SPRING INITIALIZR」からプロジェクトを作りましょう。

sec41-create-project.png

ダウンロードしたZipファイルを適当なところに解凍し、pom.xmlに以下のアーティファクトを追加します。

pom.xml
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
</dependency>

Spring Boot起動を起動しましょう。以下のようなログが出力されればOKです。ちなみに・・・停止は「Ctl+C」です。

$ ./mvnw spring-boot:run
...
2016-06-05 14:04:57.149  INFO 59552 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2016-06-05 14:04:57.215  INFO 59552 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-06-05 14:04:57.220  INFO 59552 --- [           main] com.example.Sec41DemoApplication         : Started Sec41DemoApplication in 3.553 seconds (JVM running for 6.06)

UserDetailsService(+PasswordEncoder)の適用が超絶簡単になる :thumbsup:

Spring Security 4.1から、カスタマイズしたUserDetailsService及びPasswordEncoderをSpring Securityに適用する方法が超絶簡単になりました。結論から言うと・・・SpringのDIコンテナに登録すると自動検出してくれます :laughing:

Spring Bootが使うデフォルトのUserDetailsServicePasswordEncoderは?

Spring Bootの自動コンフィギュレーションでは、インメモリ実装のInMemoryUserDetailsManagerが利用され、デフォルトユーザーとして「ユーザ名:user、パスワード:起動時に生成したUUID」が作成されます。起動時に生成されるパスワード(UUID)は、以下のようにコンソールログに出力されます。なお、使用されるPasswordEncoderは、プレーンテキストのまま扱うPlaintextPasswordEncoderが利用されます。

コンソールログ
...
2016-06-05 14:10:05.479  INFO 59590 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration : 

Using default security password: c93c44f6-9194-4249-b360-6d8a1753b946

2016-06-05 14:10:05.555  INFO 59590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: Ant [pattern='/css/**'], []
...

独自のDB認証用のUserDetailsServiceをSpring Securityに適用する

ここでは、認証情報をデータベースから取得するUserDetailsServiceを作成し、Spring Securityに提供する方法を紹介します。

まず、ユーザー情報を保持するドメインオブジェクトと認証用のユーザー情報を作成します。

ユーザー情報を保持するドメインオブジェクト
public class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private String password;
    private String name;
    private String authorities;
    // ...
}
ユーザー情報を保持するUserDetails
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;

public class MyUserDetails extends User {
    private static final long serialVersionUID = 1L;
    private final Account account;
    public MyUserDetails(Account account) {
        super(account.getUsername(), account.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList(account.getAuthorities()));
        this.account = account;
    }
    public Account getAccount() {
        return account;
    }
}

ユーザー情報を保持するUserDetailsを返却するUserDetailsServiceを作成します。

@Service // コンポーネントスキャン機能でDIコンテナに登録すれば自動検出される
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    NamedParameterJdbcOperations namedParameterJdbcOperations;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            Account account = namedParameterJdbcOperations.queryForObject(
                    "SELECT username, password, name, authorities FROM account WHERE username = :username"
                    , new MapSqlParameterSource("username", username),
                    new BeanPropertyRowMapper<>(Account.class));
            return new MyUserDetails(account);
        } catch (IncorrectResultSizeDataAccessException e) {
            throw new UsernameNotFoundException("A specified user does not exist.", e);
        }
    }
}

実は、これだけで作成したMyUserDetailsServiceクラスが、Spring Securityに適用されちゃいます!!ポイントは・・・@Serviceアノテーションを付与してコンポーネントスキャン対象にしているところです。コンポーネントスキャン機能によってDIコンテナに登録されたMyUserDetailsServiceが、Spring Securityによって自動検出されます。作成したUserDetailsServiceをコンポーネントスキャン対象にしない場合は、普通にBean定義すればOKです。

コンポーネントスキャンしない場合のBean定義例
@Configuration
public class SecurityConfig {
    @Bean
    UserDetailsService userDetailsService(){
        return new MyUserDetailsService();
    }
}

ちなみに・・・DDLとDMLはこんな感じ。これらのSQLファイルは、Spring Bootが自動で実行してくれます。

src/resources/schema.sql
CREATE TABLE account (
    username NVARCHAR(128) PRIMARY KEY,
    password CHAR(60) NOT NULL,
    name NVARCHAR(128) NOT NULL,
    authorities TEXT
);
src/resources/data.sql
-- password = demo(Hashed by Bcrypt algorithm)

INSERT INTO account (username, password, name, authorities)
    VALUES ('demo', '$2a$10$oxSJl.keBwxmsMLkcT9lPeAIxfNTPNQxpeywMrF7A3kVszwUTqfTK', 'Kazuki Shimizu', 'ROLE_USER');
INSERT INTO account (username, password, name, authorities)
    VALUES ('admin', '$2a$10$oxSJl.keBwxmsMLkcT9lPeAIxfNTPNQxpeywMrF7A3kVszwUTqfTK', 'Administrator', 'ROLE_ADMIN,ROLE_USER');

PasswordEncoderの適用

Spring Bootのデフォルトのままだとパスワードはプレーンテキストとして扱ってしまうため、ここではデータベースで管理するパスワードのハッシュ化アルゴリズムにあうPasswordEncoderを適用しましょう。PasswordEncoderも、UserDetailsServiceと同様にDIコンテナに登録するだけで自動検出されます!!

@Configuration
public class SecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

AuthenticationProviderの適用が超絶簡単になる :thumbsup:

Spring Security 4.1から、カスタマイズしたAuthenticationProviderをSpring Securityに適用する方法が超絶簡単になりました。結論から言うと・・・SpringのDIコンテナに登録すると自動検出してくれます :laughing:

独自のAuthenticationProviderをSpring Securityに適用する

ここでは、Spring Securityから提供されているDaoAuthenticationProviderのチェック処理を拡張してみます。

@Component // コンポーネントスキャン機能でDIコンテナに登録すれば自動検出される
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // ... Please add a check
        // ... (省略)
        super.additionalAuthenticationChecks(userDetails, authentication);
        // ... Please add a check
        // ... (省略)
    }

    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        super.setUserDetailsService(userDetailsService);
    }
    @Autowired
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        super.setPasswordEncoder(passwordEncoder);
    }
}

作成したAuthenticationProviderをコンポーネントスキャン対象にしない場合は、普通にBean定義すればOKです。

コンポーネントスキャンしない場合のBean定義例
@Configuration
public class SecurityConfig {
    @Bean
    AuthenticationProvider authenticationProvider() {
        return new MyDaoAuthenticationProvider();
    }
}

LogoutSuccessHandlerの複数登録が可能になる :thumbsup:

LogoutSuccessHandlerをリクエストの種類毎(RequestMatcher毎)に適用するためのメソッドが追加されました。この変更により、クライアントの種類に応じてログアウト成功時のレスポンス(ステータスコードのみ、HTML、JSONなど)を切り替えられます。ここでは、Ajaxの場合はステータスコード(200 OK)のみを返却し、Ajaxでなければログイン画面(/login?logout)に遷移させる場合のサンプルです。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        // ...
        http.logout()
                // Ajaxリクエストの場合は「200 OK」のみを返却
                .defaultLogoutSuccessHandlerFor(new HttpStatusReturningLogoutSuccessHandler(),
                        request -> "XMLHttpRequest".equals(request.getHeader("X-Requested-With")))
                .permitAll();
    }
}

フォームとAjaxを使ってログアウトする画面も作成してみましょう。

pom.xml
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>2.2.4</version>
</dependency>
src/main/resources/templates/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <script th:src="@{/webjars/jquery/2.2.4/jquery.js}"/>
    <script type="text/javascript">
        $(function () {
            $("#logout").click(function () {
                $.ajax({
                    url: '/logout',
                    type: 'POST',
                    success: function () {
                        window.location = "/login?logout";
                    },
                    error: function (data) {
                        console.log(data);
                    }
                });

            });
            $(document).ajaxSend(function (event, xhr, options) {
                var token = $("meta[name='_csrf']").attr("content");
                var header = $("meta[name='_csrf_header']").attr("content");
                xhr.setRequestHeader(header, token);
            });
        });
    </script>
    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
<body>
<h3>メニュー</h3>

<!-- フォームを使ったログアウトボタン -->
<form th:action="@{/logout}" method="post">
    <button>ログアウト</button>
</form>

<br/>

<!-- Ajaxを使ったログアウトボタン -->
<button id="logout">ログアウト for Ajax</button>

</body>
</html>
src/main/java/com/example/component/WelcomeController.java
package com.example.component;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class WelcomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }

}

ちなみに・・・・後述する「Content Security Policy (CSP)」対策を有効にすると、<script>要素内のJava Scriptが実行できなくなる可能性があります。その場合は、<script>内のコードをjsファイルに移動するのがセキュリティ的には一番安全です。

InvalidSessionStrategyの適用が簡単になる :thumbsup:

Spring Security 4.1から、カスタマイズしたInvalidSessionStrategyをSpring Securityに適用するためのメソッドが追加されました。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.sessionManagement()
                .invalidSessionStrategy(new MyInvalidSessionStrategy());
    }
}

サーブレットフィルタの適用位置の指定が柔軟になる :thumbsup:

Spring Security 4.1から、サーブレットフィルタを指定した登録済みのフィルタと同じポジション(優先順)に追加するためのメソッドが追加されました。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.addFilterAt(new MyFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

UsernamePasswordAuthenticationFilter.classと同じポジション(優先順)で、指定したサーブレットフィルタがSpring Securityに追加されます。同じ優先順位で登録されますが、ほとんどのケースで第1引数に指定したサーブレットフィルタの方が先に実行されます。

Spring MVCのリクエストマッピングのルールと連動したRequestMatcherが追加される :thumbsup:

Spring Security 4.1.1から、Spring MVCのリクエストマッピングのルールと連動したRequestMatcher(MvcRequestMatcher)が追加されました。メンテナンスバージョンアップでこのような機能追加が行われたのは、どうやら「cve-2016-5007」の脆弱性対策に関係しているからみたいです :sweat_smile:

cve-2016-5007の脆弱性の詳細は、以下のページをご覧ください。

なお、Spring Securityの公式リファレンスにもURLの解釈の違いによってSpring Securityの設定をすり抜けるケースの説明や、MvcRequestMatcherを利用してすり抜けを防止する方法が紹介されています。

以下に、公式リファレンスで紹介しているサンプルコードを例に、URLの解釈の違いによって発生するすり抜け例とMvcRequestMatcherの利用方法を紹介します。

まず、管理者向けのエンドポイントを用意します。

管理者向けのエンドポイント
@RequestMapping("/admin")
public String admin() {
    // ...
}

管理者向けのエンドポイントに対してアクセスポリシーを設ける場合、多くの開発者は以下のような定義にすると思います。

protected configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/admin").hasRole("ADMIN");
}

ぱっとみ問題ない気がしますが・・・Spring MVCのデフォルトのリクエストマッピングルールでは、「/admin」に加えて以下のパスへのアクセスもadminメソッドにマッピングされます。(ちなみに・・・このあたりの動作は、Spring MVC側の設定を変更することで動作を変えることもできるみたいです。)

  • 「/admin/」
  • 「/admin.拡張子」(例: /admin.html, /admin.json, /admin.xml, etc ...)

つまり・・・Spring Security側の設定が「/admin」だけだと「/admin/」や「/admin.html」がすり抜けてしまうわけです :scream: :scream: :scream: antMatchersには複数のパスパターンを指定することができるので、「/admin/と/admin.*」も対象に加えればすり抜けない気もするけど、そんな単純な話じゃないのかな!?

で、この問題を解決するための仕組みとして、Spring MVCのリクエストマッピングルールと連携するRequestMatcher(MvcRequestMatcher)が導入されたわけです。MvcRequestMatcherを利用する場合は、以下のような定義に変えてください。

protected configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .mvcMatchers("/admin").hasRole("ADMIN"); // mvcMatchersメソッドを使う!!
}

Note: メソッド認可の併用について

公式リファレンスのノートには、アノテーションを利用したJavaメソッドへの認可(メソッドセキュリティ)との併用が推奨されていますね。仮にWebレイヤのセキュリティをすり抜けても、Javaメソッドに適切なアクセスポリシーが定義されていれば、不正なアクセスを防ぐことができます。機密度が高い情報を扱う処理については、メソッドセキュリティとの併用を検討した方がよいかもしれませんね。

さらに、Spring Security 4.1.2からは、HttpSecurityの適用範囲を明示的に指定する際にもMvcRequestMatcherが利用できるようになっています。

単一のパスパターンを適用対象する場合
protected void configure(HttpSecurity http) throws Exception {
    http.mvcMatcher("/admin/**") // 適用範囲の指定でも利用できる
            .authorizeRequests()
            .antMatchers("/admin/system").hasRole("SYS_ADMIN")
            .antMatchers("/admin/infra").hasRole("INFRA_ADMIN");
}

or

複数のパスパターンを適用対象にする場合
protected void configure(HttpSecurity http) throws Exception {
    http.requestMatchers()
            .mvcMatchers("/admin/system/**", "/admin/infra/**") // 適用範囲の指定でも利用できる
            .and()
            .authorizeRequests().anyRequest().hasRole("SUPER_ADMIN");

Spring SecurityをSpring MVCと一緒に使う場合は、MvcRequestMatcherを使いようにしましょう!!って話ですね :wink:

Content Security Policy (CSP)がサポートされる :thumbsup:

Spring Security 4.1から、XSS攻撃対策として、「Content Security Policy (CSP)」用のHTTPレスポンスヘッダ(Content-Security-PolicyやContent-Security-Policy-Report-Only)の出力がサポートされました。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.headers()
                .contentSecurityPolicy("default-src 'self'");
    }
}

HTTP Public Key Pinning (HPKP)がサポートされる :thumbsup:

Spring Security 4.1から、偽造された証明書による中間者攻撃対策として、「HTTP Public Key Pinning (HPKP)」用のHTTPレスポンスヘッダ(Public-Key-PinsやPublic-Key-Pins-Report-Only)の出力がサポートされました。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.headers()
                .httpPublicKeyPinning()
                .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=",
                               "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=");
    }
}

CSRFトークンの保存先としてCookieがサポートされる :thumbsup:

Spring Security 4.1から、CSRFトークンの保存先としてCookie(CookieCsrfTokenRepository)がサポートされました。これは、AngularJSのCSRF対策機能のデフォルト動作をサポートするために追加されたようです。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.csrf()
                .csrfTokenRepository(new CookieCsrfTokenRepository());
    }
}

なお、デフォルトで使用されるCookie名は「XSRF-TOKEN」で、CSRFトークン値を送信するためのリクエストパラメータ名は「_csrf」、CSRFトークン値を送信するためのリクエストヘッダ名は「X-XSRF-TOKEN」です。リクエストヘッダ名のデフォルト値がHttpSessionCsrfTokenRepository使用時とことなる点に注意が必要です。(HttpSessionCsrfTokenRepository使用時は「X-CSRF-TOKEN」がデフォルトです)

認証後の遷移方法としてForwardがサポートされる(≠Redirect) :thumbsup:

AuthenticationSuccessHandlerAuthenticationFailureHandlerにて、遷移先にフォワードする実装クラスが追加されました。この変更により、遷移先でリクエストスコープの情報にアクセスすることが可能になります。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.formLogin()
                .successForwardUrl("/authenticationSuccess") // ForwardAuthenticationSuccessHandlerが設定される
                .failureForwardUrl("/authenticationError"); // ForwardAuthenticationFailureHandlerが設定される
    }
}

@AuthenticationPrincipalを使ってSpring MVCのHandlerメソッドに渡せるオブジェクトが柔軟になる :thumbsup:

Spring Security 4.1から、@AuthenticationPrincipalexpression属性が追加されました。expression属性を利用すると、Spring MVCのHandlerメソッドの引数に、認証ユーザー情報(Authentication#getPrincipal()メソッドの返り値)が保持する任意のプロパティ値をインジェクションすることができます。

4.1〜
package com.example.component;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/profile")
@Controller
public class ProfileController {

    @GetMapping
    public String view(@AuthenticationPrincipal(expression = "account") Account account, Model model) {
        model.addAttribute(account);
        return "profile";
    }

}

Spring Security 4.0では、UserDetailsをメソッドの引数として受け取る必要がありました。

〜4.0
@GetMapping
public String view(@AuthenticationPrincipal MyUserDetails userDetails, Model model) {
    model.addAttribute(userDetails.getAccount()); // 明示的にメソッドを呼び出す
    return "profile";
}

Webリソースへの認可でパス変数値が参照可能になる :thumbsup:

Spring Security 4.1から、Webリソースへの認可に対してパス変数値を参照した認可制御が可能になりました !!! 個人的にはSpring Security 4.1の最大の目玉だと思っています :laughing: :laughing: :laughing:

ここでは、指定されたユーザー名のアカウント情報にアクセスするWebリソース(/accounts/{username})への認可を考えてみましょう。リソースへアクセスする処理(Handlerメソッド)は、おそらくこんな感じになります。

package com.example.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/accounts")
@Controller
public class AccountController {

    @Autowired
    NamedParameterJdbcOperations namedParameterJdbcOperations;

    @GetMapping("{username}")
    public String detail(@PathVariable String username, Model model) {
        try {
            Account account = namedParameterJdbcOperations.queryForObject(
                    "SELECT username, password, name, authorities FROM account WHERE username = :username"
                    , new MapSqlParameterSource("username", username),
                    new BeanPropertyRowMapper<>(Account.class));
            model.addAttribute(account);
        } catch (EmptyResultDataAccessException e) {
            model.addAttribute(new Account()); // この例外ハンドリングは適切ではありません。適切なハンドリングをしましょ!!
        }
        return "accountDetail";
    }

}

ここでは、管理者(ROLE_ADMIN)はすべてのアカウント情報にアクセスすることができ、一般ユーザー(ROLE_USER)は自分のアカウント情報のみアクセスできるように認可制御します。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
        http.authorizeRequests()
                .antMatchers("/accounts/{username}")
                        .access("isAuthenticated() and (hasRole('ADMIN') or (#username == principal.username))")
                .anyRequest()
                        .authenticated();
    }
}

パス変数「{パス変数名}」には、expression属性の中では「#パス変数名」でアクセスできます。ナイスですね :thumbsup: :thumbsup: :thumbsup:

メソッド認可用のアノテーションがメタアノテーションとして利用可能になる :thumbsup:

Spring Security 4.1から、Javaメソッドへの認可時に使用するアノテーション(@PreAuthorizeなど)がメタアノテーションとして使用できるようになりました。

カスタムアノテーション
package com.example.component;

import org.springframework.security.access.prepost.PreAuthorize;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@PreAuthorize("isAuthenticated() and (#username == principal.username"))
@Retention(RetentionPolicy.RUNTIME)
public @interface SelfPermission {
}
@SelfPermission // カスタムアノテーションを指定
@GetMapping("{username}")
public String detail(@PathVariable String username, Model model) {
    // ...
}

なお、メソッド認可用のアノテーションを使用する場合は、Javaメソッドへの認可機能を有効化する必要があります。

@EnableGlobalMethodSecurity(prePostEnabled = true) // Javaメソッドへの認可機能を有効化
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
}

SCryptアルゴリズムのPasswordEncoderがサポートされる :thumbsup:

Spring Security 4.1から、SCryptアルゴリズムをサポートしたPasswordEncoderの実装クラス(SCryptPasswordEncoder)が追加されました。SCryptPasswordEncoderを使用する場合は、Bouncy Castle Providerを依存アーティファクトに追加する必要があります。

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.54</version> <!-- 投稿時点の最新バージョン -->
</dependency>
@Configuration
public class SecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return new SCryptPasswordEncoder();
    }
}

SCryptPasswordEncoderを使う場合は、パスワードを保持するカラムサイズを140文字に変更する必要があります。

src/main/resources/schema.sql
CREATE TABLE account (
    username NVARCHAR(128) PRIMARY KEY,
    password CHAR(140) NOT NULL, -- 140文字へ変更
    name NVARCHAR(128) NOT NULL,
    authorities TEXT
);
src/main/resources/data.sql
-- password = demo(Hashed by SCrypt algorithm)

INSERT INTO account (username, password, name, authorities) 
    VALUES ('demo', '$e0801$xJb9axROvGvtu06vSPZBaDtD8Xpd/4NzTVBkP81E4ZrBSvnF276hkCj7R9YbxpVW3jY2e03Mqk8EFlgOH6FV3g==$2m3TbgXxYCuDK4PQt8eHDwoXOIahFEMCwznuT7DFSo0=', 'Kazuki Shimizu', 'ROLE_USER');
INSERT INTO account (username, password, name, authorities)
    VALUES ('admin', '$e0801$xJb9axROvGvtu06vSPZBaDtD8Xpd/4NzTVBkP81E4ZrBSvnF276hkCj7R9YbxpVW3jY2e03Mqk8EFlgOH6FV3g==$2m3TbgXxYCuDK4PQt8eHDwoXOIahFEMCwznuT7DFSo0=', 'Administrator', 'ROLE_ADMIN,ROLE_USER');

PBKDF2アルゴリズムのPasswordEncoderがサポートされる :thumbsup:

Spring Security 4.1から、PBKDF2アルゴリズムをサポートしたPasswordEncoderの実装クラス(Pbkdf2PasswordEncoder)が追加されました。

@Configuration
public class SecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return new Pbkdf2PasswordEncoder();
    }
}

SCryptPasswordEncoderを使う場合は、パスワードを保持するカラムサイズを96文字に変更する必要があります。

src/main/resources/schema.sql
CREATE TABLE account (
    username NVARCHAR(128) PRIMARY KEY,
    password CHAR(96) NOT NULL, -- 96文字へ変更
    name NVARCHAR(128) NOT NULL,
    authorities TEXT
);
src/main/resources/data.sql
-- password = demo(Hashed by PBKDF2 algorithm)

INSERT INTO account (username, password, name, authorities)
    VALUES ('demo', 'a35fd0d412a681a1a35fd0d412a681a154793f9cc057581cf317649f4ab1766d15aea67b9bef7f062a348907160e7752', 'Kazuki Shimizu', 'ROLE_USER');
INSERT INTO account (username, password, name, authorities)
    VALUES ('admin', 'a35fd0d412a681a1a35fd0d412a681a154793f9cc057581cf317649f4ab1766d15aea67b9bef7f062a348907160e7752', 'Administrator', 'ROLE_ADMIN,ROLE_USER');

テスト時に匿名ユーザー用の認証情報のセットアップができる :thumbsup:

Spring Security 4.1から、匿名ユーザーの認証情報をセットアップするためのアノテーション(@WithAnonymousUser)が追加されました。

package com.example;

import com.example.component.AccountController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.ui.ExtendedModelMap;


@RunWith(SpringRunner.class)
@SpringBootTest
public class Sec41DemoApplicationTests {

    @Autowired
    AccountController accountController;

    @WithAnonymousUser // 匿名ユーザー用の認証情報をセットアップ
    @Test(expected = AccessDeniedException.class)
    public void detailByAnonymous() throws Exception {

        accountController.detail("demo", new ExtendedModelMap());

    }

}

テスト時に利用するUserDetailsServiceを指定できる :thumbsup:

Spring Security 4.1から、@WithUserDetailsuserDetailsServiceBeanName属性が追加されました。userDetailsServiceBeanName属性を使用すると、テスト時に使用するUserDetailsServiceのBeanを明示的に指定することができます。

@WithUserDetails(value = "admin", userDetailsServiceBeanName = "myUserDetailsService")
@Test(expected = AccessDeniedException.class)
public void anonymous() throws Exception {

    accountController.detail("demo", new ExtendedModelMap());

}

テスト時に利用する認証情報などを指定するアノテーションがメタアノテーションとして利用可能になる :thumbsup:

Spring Security 4.1から、認証情報などを指定するアノテーション(@WithMockUserなど)をメタアノテーションとして使用できるようになりました。

カスタムアノテーション
@WithMockUser("demo")
@Retention(RetentionPolicy.RUNTIME)
public @interface DemoUser {}
@DemoUser // カスタムアノテーションを指定
@Test(expected = AccessDeniedException.class)
public void anonymous() throws Exception {

    accountController.detail("admin", new ExtendedModelMap());

}

SecurityMockMvcResultMatchers#withAuthoritiesメソッドの引数で扱える型が柔軟になる :thumbsup:

Spring Security 4.1から、SecurityMockMvcResultMatchers#withAuthoritiesメソッドで、GrantedAuthorityのサブクラスをキャストなしで扱えるようになりました。

package com.example;

import com.example.component.AccountController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.Collections;
import java.util.List;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
public class Sec41DemoApplicationTests {

    @Autowired
    WebApplicationContext was;

    MockMvc mockMvc;

    @Autowired
    AccountController accountController;

    @Before
    public void setupMockMvc() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(was)
                .apply(springSecurity())
                .build();
    }

    @Test
    public void login() throws Exception {

        // 要素の型を具象クラス(SimpleGrantedAuthority)にしても・・・
        List<SimpleGrantedAuthority> expectedAuthorities = new ArrayList<>(); 
        expectedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));

        mockMvc.perform(
                formLogin().user("demo").password("demo"))
                .andExpect(status().isFound()).andExpect(redirectedUrl("/"))
                .andExpect(authenticated().withAuthorities(expectedAuthorities)); // キャストなしでwithAuthoritiesに指定可能!!
    }
}

でも・・・・このケースだと、Spring Securityから提供されているAuthorityUtils#createAuthorityListメソッドを使う方がいいですけどね・・・(そうすればSpring Security 4.0でも問題なくコンパイル可能できます)

@Test
public void login() throws Exception {

    List<GrantedAuthority> expectedAuthorities = AuthorityUtils.createAuthorityList("ROLE_USER");

    mockMvc.perform(
            formLogin().user("demo").password("demo"))
            .andExpect(status().isFound()).andExpect(redirectedUrl("/"))
            .andExpect(authenticated().withAuthorities(expectedAuthorities));
}

まとめ

今回はSpring Security 4.1の主な変更点を紹介しました。コンフィギュレーションが簡単になったり、Webリソースへの認可でパス変数値の参照が可能になったり、新しいPasswordEncoderが追加されたり、新しいセキュリティヘッダがサポートされたりと、Spring Security 4.1は確実に進化しています。Spring Securityはサーブレットフィルタとして実装されているため、Spring MVC以外のフレームワークにも適用できます。Webアプリケーションのセキュリティ対策には是非Spring Securityを!! :smile:

参考サイト

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
Sign upLogin
62
Help us understand the problem. What are the problem?