LoginSignup
54
58

More than 1 year has passed since last update.

Spring Security 使い方メモ テスト

Last updated at Posted at 2017-07-23

Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita を参照してください。

基礎・仕組み的な話
認証・認可の話
Remember-Me の話
CSRF の話
セッション管理の話
レスポンスヘッダーの話
メソッドセキュリティの話
CORS の話
Run-As の話
ACL の話
MVC, Boot との連携の話

番外編
Spring Security にできること・できないこと

Spring Security のテスト

Spring Security には、 JUnit によるテストをサポートする仕組みが用意されている。

例えば、テストのときに使用するユーザを固定したり、権限を指定したりすることができる。

Hello World

実装

build.gradle
dependencies {
    compile 'org.springframework.security:spring-security-web:4.2.1.RELEASE'
    compile 'org.springframework.security:spring-security-config:4.2.1.RELEASE'
    testCompile 'junit:junit:4.12' ★追加
    testCompile 'org.springframework.security:spring-security-test:4.2.1.RELEASE' ★追加
}
  • JUnit と Spring Test のモジュールを依存関係に追加
MyTestService.java
package sample.spring.security.test;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;

public class MyTestService {
    
    @PreAuthorize("authenticated")
    public String getMessage() {
        String name = SecurityContextHolder.getContext().getAuthentication().getName();
        return "Hello " + name;
    }
}
  • テスト対象のクラス
  • @PreAuthorize() で認証済みかどうかをチェック
  • 現在の Authentication の名前を取得し、 "Hello 名前" という形にして返す

namespace

src/test/resources/test-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd">

    <sec:global-method-security pre-post-annotations="enabled" />
    
    <bean class="sample.spring.security.test.MyTestService" />
    
</beans>
  • テスト用の Spring の設定ファイル
    • 実際は本番と同じ設定ファイルを使ってテストすべきだと思うが、ここは動作検証が目的なので別ファイルを用意している
  • メソッドセキュリティをオンにし、テスト対象のクラスを Bean として登録
MyTestServiceTest.java
package sample.spring.security.test;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
public class MyTestServiceTest {
    
    @Autowired
    private MyTestService service;

    @Test(expected = AuthenticationException.class)
    public void test_getMessage_no_authentication() throws Exception {
        // exercise
        this.service.getMessage();
    }

    @Test
    @WithMockUser(username = "hoge")
    public void test_getMessage() throws Exception {
        // exercise
        String message = this.service.getMessage();
        
        // verify
        Assert.assertEquals("Hello hoge", message);
    }
}
  • テストクラス
  • @ContextConfiguration で、テスト用の設定ファイルを読み込んでいる
  • @WithMockUser を指定せずに getMessage() を実行した場合は AuthenticationException がスローされることをテストしている
  • @WithMockUser を指定している場合は、戻り値のメッセージが @WithMockUser で指定した文字列で構築されていることをテストしている

Java Configuration

MyTestSpringSecurityConfig.java
package sample.spring.security.test;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MyTestSpringSecurityConfig {
    
    @Bean
    public MyTestService myTestService() {
        return new MyTestService();
    }
}
  • テスト用の Spring の設定クラス
  • 内容は xml のやつと同じ
MyTestServiceTest.java
package sample.spring.security.test;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MyTestSpringSecurityConfig.class)
public class MyTestServiceTest {
    
    @Autowired
    private MyTestService service;

    @Test(expected = AuthenticationException.class)
    public void test_getMessage_no_authentication() throws Exception {
        // exercise
        this.service.getMessage();
    }

    @Test
    @WithMockUser(username = "hoge")
    public void test_getMessage() throws Exception {
        // exercise
        String message = this.service.getMessage();

        // verify
        Assert.assertEquals("Hello hoge", message);
    }
}
  • こちらも、 @ContextConfiguration で設定クラスを読み込んでいること以外は xml の方と同じ

動作確認

それぞれ実行すると、テストが成功する。

説明

Spring Test の設定

MyTestServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
  • この2つのアノテーションは、 Spring Test のための設定で、とくに Spring Security 用の設定というわけではない
  • ここの設定で、テスト用の ApplicationContext を構築している
  • @ContextConfiguration で、テスト用に使用する Spring の設定を指定する
  • namespace の場合は、xml のクラスパス上の場所、 Java Configuration の場合は設定クラスの Class オブジェクトを渡す

モックユーザーの指定

MyTestServiceTest.java
    @Test
    @WithMockUser(username = "hoge")
    public void test_getMessage() throws Exception {
  • @WithMockUser でアノテートすることで、そのテストが実行されている間に使用されるユーザーの情報(Authentication)を指定できる
  • username = "hoge" としているので、ユーザ名が hogeAuthentication が使用されることになる
  • Spring Security は Spring Test と統合することで、テストの前後で SecurityContextHolder の情報を設定・クリアしてくれるようになっている

アノテーション

アノテーションによってテスト中の SecurityContext に任意の情報を設定することができる。

@WithMockUser

MyTestServiceTest.java
package sample.spring.security.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
public class MyTestServiceTest {
    
    @Test
    @WithMockUser
    public void test() throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message = "class = " + auth.getClass() + "\n" +
                         "name = " + auth.getName() + "\n" +
                         "credentials = " + auth.getCredentials() + "\n" +
                         "authorities = " + auth.getAuthorities() + "\n" +
                         "principal = " + auth.getPrincipal() + "\n" +
                         "details = " + auth.getDetails();

        System.out.println(message);
    }
}

実行結果

class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = user
credentials = password
authorities = [ROLE_USER]
principal = org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER
details = null
  • @WithMockUser を使用すると、モックユーザの情報が SecurityContext に保存される
  • 何も指定しないと、デフォルトで次の情報を持つ
    • ユーザ名は user
    • パスワードは password
    • 権限は ROLE_USER
    • Authentication の実装には UsernamePasswordAuthenticationToken が使用される
    • プリンシパルには User オブジェクトが使用される
  • user はモックユーザなので、その名前のユーザが実際に存在する必要はない
  • クラスをアノテートすることも可能(その場合は、全てのメソッドでモックユーザが使用される)

ユーザー名を設定する

MyTestServiceTest.java
    @Test
    @WithMockUser("test-user")
    public void test() throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message = "class = " + auth.getClass() + "\n" +
                         "name = " + auth.getName() + "\n" +
                         "credentials = " + auth.getCredentials() + "\n" +
                         "authorities = " + auth.getAuthorities() + "\n" +
                         "principal = " + auth.getPrincipal() + "\n" +
                         "details = " + auth.getDetails();

        System.out.println(message);
    }

実行結果

class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = test-user
credentials = password
authorities = [ROLE_USER]
principal = org.springframework.security.core.userdetails.User@b6e8d426: Username: test-user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER
details = null
  • value に設定した文字列がユーザー名として利用される
  • value の代わりに username で指定することも可能
    • 後述するロールなどと一緒に設定する場合は、 value よりも username を利用したほうが分かりやすくなる

ロールを設定する

MyTestServiceTest.java
    @Test
    @WithMockUser(
        username="test-user",
        roles={"FOO", "BAR"}
    )
    public void test() throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message = "class = " + auth.getClass() + "\n" +
                         "name = " + auth.getName() + "\n" +
                         "credentials = " + auth.getCredentials() + "\n" +
                         "authorities = " + auth.getAuthorities() + "\n" +
                         "principal = " + auth.getPrincipal() + "\n" +
                         "details = " + auth.getDetails();

        System.out.println(message);
    }

実行結果

class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = test-user
credentials = test-pass
authorities = [ROLE_BAR, ROLE_FOO]
principal = org.springframework.security.core.userdetails.User@b6e8d426: Username: test-user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_BAR,ROLE_FOO
details = null
  • roles を指定することで、 ROLE_ プレフィックスが自動で付与された権限が設定される
  • authorities と一緒に設定するとエラーになる

権限を設定する

MyTestServiceTest.java
    @Test
    @WithMockUser(
        username="test-user",
        authorities={"FOO", "BAR"}
    )
    public void test() throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message = "class = " + auth.getClass() + "\n" +
                         "name = " + auth.getName() + "\n" +
                         "credentials = " + auth.getCredentials() + "\n" +
                         "authorities = " + auth.getAuthorities() + "\n" +
                         "principal = " + auth.getPrincipal() + "\n" +
                         "details = " + auth.getDetails();

        System.out.println(message);
    }

実行結果

class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = test-user
credentials = password
authorities = [BAR, FOO]
principal = org.springframework.security.core.userdetails.User@b6e8d426: Username: test-user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO
details = null
  • authorities を指定することで、権限が設定される
  • roles と一緒に設定するとエラーになる

パスワードを設定する

MyTestServiceTest.java
    @Test
    @WithMockUser(password="test-pass")
    public void test() throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message = "class = " + auth.getClass() + "\n" +
                         "name = " + auth.getName() + "\n" +
                         "credentials = " + auth.getCredentials() + "\n" +
                         "authorities = " + auth.getAuthorities() + "\n" +
                         "principal = " + auth.getPrincipal() + "\n" +
                         "details = " + auth.getDetails();

        System.out.println(message);
    }

実行結果

class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = user
credentials = test-pass
authorities = [ROLE_USER]
principal = org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER
details = null
  • password でパスワードを設定できる

@WithAnonymousUser

MyTestServiceTest.java
package sample.spring.security.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@WithMockUser
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
public class MyTestServiceTest {

    @Test
    @WithAnonymousUser
    public void testAnonymous() throws Exception {
        this.printAuthentication("testAnonymous");
    }
    
    @Test
    public void testDefault() throws Exception {
        this.printAuthentication("testDefault");
    }

    private void printAuthentication(String testMethodName) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message =
                "[" + testMethodName + "]\n" +
                "class = " + auth.getClass() + "\n" +
                "name = " + auth.getName() + "\n" +
                "credentials = " + auth.getCredentials() + "\n" +
                "authorities = " + auth.getAuthorities() + "\n" +
                "principal = " + auth.getPrincipal() + "\n" +
                "details = " + auth.getDetails();

        System.out.println(message);
    }
}

実行結果

[testAnonymous]
class = class org.springframework.security.authentication.AnonymousAuthenticationToken
name = anonymous
credentials = 
authorities = [ROLE_ANONYMOUS]
principal = anonymous
details = null

[testDefault]
class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = user
credentials = password
authorities = [ROLE_USER]
principal = org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER
details = null
  • @WithAnonymousUser でアノテートすると、匿名ユーザが使用される
  • クラスを @WithMockUser でアノテートしていても、メソッドを @WithAnonymousUser でアノテートすることで設定を上書きすることが可能

@WithUserDetails

MyUserDetailsService.java
package sample.spring.security.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.Collection;

public class MyUserDetailsService implements UserDetailsService {
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new MyUser(username, "test-password", AuthorityUtils.createAuthorityList("FOO", "BAR"));
    }
    
    private static class MyUser extends User {
        private MyUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, authorities);
        }
    }
}
test-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean class="sample.spring.security.service.MyUserDetailsService" />
</beans>
MyTestServiceTest.java
package sample.spring.security.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
public class MyTestServiceTest {

    @Test
    @WithUserDetails
    public void test() throws Exception {
        this.printAuthentication("test");
    }

    private void printAuthentication(String testMethodName) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message =
                "[" + testMethodName + "]\n" +
                "class = " + auth.getClass() + "\n" +
                "name = " + auth.getName() + "\n" +
                "credentials = " + auth.getCredentials() + "\n" +
                "authorities = " + auth.getAuthorities() + "\n" +
                "principal = " + auth.getPrincipal() + "\n" +
                "details = " + auth.getDetails();

        System.out.println(message);
    }
}

実行結果

[test]
class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = user
credentials = test-password
authorities = [BAR, FOO]
principal = sample.spring.security.service.MyUserDetailsService$MyUser@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO
details = null
  • @WithUserDetails でアノテートすると、 Bean として登録した UserDetailsService から取得したユーザ情報で認証情報が構築される
  • 独自の UserDetails を使用している場合などに利用する
  • @WithUserDetailsvalue で、検索するユーザの名前を指定できる(デフォルトは "user"
  • userDetailsServiceBeanName で、使用する UserDetailsService Bean を指定できる

@WithSecurityContext

MyTestUser.java
package sample.spring.security.test;

import org.springframework.security.test.context.support.WithSecurityContext;

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

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@WithSecurityContext(factory=MyTestUserFactory.class)
public @interface MyTestUser {
    String name();
    String pass();
    String authority();
}
MyTestUserFactory.java
package sample.spring.security.test;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

public class MyTestUserFactory implements WithSecurityContextFactory<MyTestUser> {
    
    @Override
    public SecurityContext createSecurityContext(MyTestUser annotation) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        String name = annotation.name();
        String pass = annotation.pass();
        String authority = annotation.authority();

        UserDetails user = new User(name, pass, AuthorityUtils.createAuthorityList(authority));
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                                            user, user.getPassword(), user.getAuthorities());
        
        context.setAuthentication(authentication);

        return context;
    }
}
MyTestServiceTest.java
package sample.spring.security.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
public class MyTestServiceTest {

    @Test
    @MyTestUser(name="foo", pass="FOO", authority="TEST_FOO")
    public void test() throws Exception {
        this.printAuthentication("test");
    }

    private void printAuthentication(String testMethodName) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message =
                "[" + testMethodName + "]\n" +
                "class = " + auth.getClass() + "\n" +
                "name = " + auth.getName() + "\n" +
                "credentials = " + auth.getCredentials() + "\n" +
                "authorities = " + auth.getAuthorities() + "\n" +
                "principal = " + auth.getPrincipal() + "\n" +
                "details = " + auth.getDetails();

        System.out.println(message);
    }
}

実行結果

[test]
class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = foo
credentials = FOO
authorities = [TEST_FOO]
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: TEST_FOO
details = null
  • SecurityContext を任意の実装で自作することができる
MyTestUser.java
@WithSecurityContext(factory=MyTestUserFactory.class)
public @interface MyTestUser {
  • 任意のアノテーションを作成し、 @WithSecurityContext でアノテートする
  • factory に、 WithSecurityContextFactory インターフェースを実装したクラスの Class オブジェクトを指定する
  • ここで指定したファクトリクラスが、 SecurityContext を作成する処理を実装する
MyTestUserFactory.java
package sample.spring.security.test;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

public class MyTestUserFactory implements WithSecurityContextFactory<MyTestUser> {
    
    @Override
    public SecurityContext createSecurityContext(MyTestUser annotation) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        ...

        return context;
    }
}
  • WithSecurityContextFactory<T> インターフェースを実装して作成したファクトリクラス
  • 型変数 T には、そのファクトリクラスが扱うアノテーションの型を指定する
  • createSecurityContext() メソッドを実装し、自作の SecurityContext を作成して返却する
  • このクラスは特に Spring のコンテナに登録する必要はない
  • テストが実行されるときに WithSecurityContextTestExecutionListener がアノテーションからファクトリを生成してくれる
MyTestServiceTest.java
    @MyTestUser(name="foo", pass="FOO", authority="TEST_FOO")
    public void test() throws Exception {
  • あとは、テストメソッドを自作のアノテーションで注釈すれば、指定したファクトリで作成された SecurityContext が認証情報として使用されるようになる

ファクトリクラスにコンテナ管理の Bean をインジェクションする

MyTestUserFactory.java
public class MyTestUserFactory implements WithSecurityContextFactory<MyTestUser> {
    private UserDetailsService userDetailsService;

    @Autowired
    public MyTestUserFactory(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
  • ファクトリクラスは通常の Bean と同じ要領で他の Bean をインジェクションできる
  • 上記実装はコンストラクタインジェクションを利用している(@Autowired は省略可能)

メタアノテーション

HogeUser.java
package sample.spring.security.test;

import org.springframework.security.test.context.support.WithMockUser;

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

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "hoge", authorities = {"FOO", "BAR"})
public @interface HogeUser {
}
MyTestServiceTest.java
package sample.spring.security.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
public class MyTestServiceTest {

    @Test
    @HogeUser
    public void test() throws Exception {
        this.printAuthentication("test");
    }

    private void printAuthentication(String testMethodName) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String message =
                "[" + testMethodName + "]\n" +
                "class = " + auth.getClass() + "\n" +
                "name = " + auth.getName() + "\n" +
                "credentials = " + auth.getCredentials() + "\n" +
                "authorities = " + auth.getAuthorities() + "\n" +
                "principal = " + auth.getPrincipal() + "\n" +
                "details = " + auth.getDetails();

        System.out.println(message);
    }
}

実行結果

[test]
class = class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
name = hoge
credentials = password
authorities = [BAR, FOO]
principal = org.springframework.security.core.userdetails.User@30f425: Username: hoge; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO
details = null
  • 自作のアノテーションを @WithMockUser アノテーションで注釈することができる
  • この自作アノテーションをテストメソッドなどに使うと、自作アノテーションに設定された @WithMockUser の設定が引き継がれる
  • この仕組みをメタアノテーションと呼ぶ
  • 同じ設定のモックユーザを様々なテストで使うような場合は、それぞれのメソッドを @WithMockUser で注釈するより、メタアノテーションを使ったほうが設定が一か所になり使いまわしがしやすい
  • 当然 @WithMockUser 以外にも @WithUserDetails などでも利用できる

Spring MVC との統合

Hello World

実装

build.gradle
dependencies {
    compile 'org.springframework.security:spring-security-web:4.2.1.RELEASE'
    compile 'org.springframework.security:spring-security-config:4.2.1.RELEASE'
    compile 'org.springframework:spring-webmvc:4.2.1.RELEASE' ★追加
    testCompile 'junit:junit:4.12'
    testCompile 'org.springframework.security:spring-security-test:4.2.1.RELEASE'
}
  • Spring MVC の実装を追加するので、 spring-webmvc の依存関係を追加
MyMvcController.java
package sample.spring.security.control;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/mvc")
public class MyMvcController {
    
    @GetMapping
    public String hello() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("auth.name = " + auth.getName());
        
        return "test";
    }
}
  • /mvc への GET リクエストとマッピングしたコントローラクラス
  • Authentication の名前を出力している
test-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd">

    <bean class="sample.spring.security.control.MyMvcController" />

    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
    </sec:http>

    <sec:authentication-manager />
</beans>
  • Spring Security のための簡易な宣言と、 MyMvcController の Bean 宣言
MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(SecurityMockMvcConfigurers.springSecurity())
                    .build();
    }

    @Test
    public void unauthorized() throws Exception {
        MvcResult mvcResult = this.mvc.perform(get("/mvc")).andReturn();
        this.printResponse("unauthorized", mvcResult);
    }

    @Test
    public void authorized() throws Exception {
        MvcResult mvcResult = this.mvc.perform(get("/mvc").with(user("foo"))).andReturn();
        this.printResponse("authorized", mvcResult);
    }
    
    private void printResponse(String method, MvcResult result) throws Exception {
        MockHttpServletResponse response = result.getResponse();
        int status = response.getStatus();
        String locationHeader = response.getHeader("Location");

        System.out.println("[" + method + "]\n" +
                           "status : " + status + "\n" +
                           "Location : " + locationHeader);
    }
}
  • テストメソッドは unauthorizedauthorized の2つ
  • それぞれ /mvc に対してアクセスし、結果の HTTP ステータスと Location ヘッダーを出力している

実行結果

auth.name = foo
[authorized]
status : 200
Location : null

[unauthorized]
status : 302
Location : http://localhost/login
  • authorized はコントローラが実行できて、ユーザー名が出力されている
  • unauthorized302 のステータスが返されて、ログイン画面へリダイレクトを促されている

説明

MyMvcControllerTest.java
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(SecurityMockMvcConfigurers.springSecurity())
                    .build();
    }
  • このテストの準備の実装は、 Spring MVC の勉強はしていないのでドキュメントに書いてあったそのままで、あまり意味は理解できていない
  • Spring Security のための設定は、 setUp() メソッドの SecurityMockMvcConfigurers.springSecurity()
    • この設定で、 Spring Security の Filter が MVC のモックに組み込まれ、 Spring Security の処理が動くようになっているっぽい
MyMvcControllerTest.java

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;.

...

    @Test
    public void unauthorized() throws Exception {
        MvcResult mvcResult = this.mvc.perform(get("/mvc")).andReturn();
        this.printResponse("unauthorized", mvcResult);
    }

    @Test
    public void authorized() throws Exception {
        MvcResult mvcResult = this.mvc.perform(get("/mvc").with(user("foo"))).andReturn();
        this.printResponse("authorized", mvcResult);
    }
  • get()MockMvcRequestBuildersstatic メソッド
  • user()SecurityMockMvcRequestPostProcessorsstatic メソッド
    • 実行時のログインユーザの名前を指定している
    • Spring MVC Test には、リクエストを書き換えるための仕組みがあり、 RequestPostProcessor インターフェースを実装したクラスを利用する
    • Spring Security は、 Spring Security 用に設定しやすいように、この RequestPostProcessor を実装したクラスをたくさん用意している
    • それらのクラスを利用するための窓口として、 static なファクトリメソッドを集めた SecurityMockMvcRequestPostProcessors というクラスが用意されている

CSRF

CSRF の保護機能が有効になっている場合、トークンをリクエストに載せなければならない。
何もせずにそのままコントローラを呼び出すと、トークンがないのでエラーになる。

そのため、テストで CSRF のトークンチェックをパスするための API が用意されている。

実装

test-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    <bean class="sample.spring.security.control.MyMvcController" />

    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
        <sec:csrf /> ★追加
    </sec:http>

    <sec:authentication-manager />
</beans>
  • <csrf> を追加して CSRF 保護機能を有効にする
MyMvcController.java
package sample.spring.security.control;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/mvc")
public class MyMvcController {
    
    @PostMapping
    public String hello() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("auth.name = " + auth.getName());
        
        return "test";
    }
}
  • メソッドのアノテーションを @PostMapping に変更
  • CSRF 対策は GET メソッドでは実行されないので
MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(SecurityMockMvcConfigurers.springSecurity())
                    .build();
    }

    @Test
    public void noToken() throws Exception {
        MvcResult mvcResult = this.mvc.perform(
            post("/mvc")
            .with(user("foo"))
        ).andReturn();
        
        this.printResponse("noToken", mvcResult);
    }

    @Test
    public void useToken() throws Exception {
        MvcResult mvcResult = this.mvc.perform(
            post("/mvc")
            .with(user("bar"))
            .with(csrf())
        ).andReturn();
        
        this.printResponse("useToken", mvcResult);
    }
    
    private void printResponse(String method, MvcResult result) throws Exception {
        MockHttpServletResponse response = result.getResponse();
        int status = response.getStatus();
        String errorMessage = response.getErrorMessage();

        System.out.println("[" + method + "]\n" +
                           "status : " + status + "\n" +
                           "errorMessage : " + errorMessage);
    }
}

実行結果

[noToken]
status : 403
errorMessage : Could not verify the provided CSRF token because your session was not found.

auth.name = bar
[useToken]
status : 200
errorMessage : null
  • noToken() は、 CSRF のトークンチェックに引っ掛かりエラーとなっている
  • useToken() はコントローラのメソッド実行に成功している

説明

MyMvcControllerTest.java
    @Test
    public void noToken() throws Exception {
        MvcResult mvcResult = this.mvc.perform(
            post("/mvc")
            .with(user("foo"))
        ).andReturn();
        
        this.printResponse("noToken", mvcResult);
    }

    @Test
    public void useToken() throws Exception {
        MvcResult mvcResult = this.mvc.perform(
            post("/mvc")
            .with(user("bar"))
            .with(csrf())
        ).andReturn();
        
        this.printResponse("useToken", mvcResult);
    }
  • CSRF のトークンをテスト実行時のリクエストに載せるには、 csrf() メソッドを使用する
  • このメソッドも、 SecurityMockMvcRequestPostProcessors に定義されている
  • トークンをヘッダーに載せたい場合は csrf().asHeader() とするらしい

不正なトークンをセットする

実装

MyMvcControllerTest.java
    @Test
    public void useInvalidToken() throws Exception {
        MvcResult mvcResult = this.mvc.perform(
            post("/mvc")
            .with(user("bar"))
            .with(csrf().useInvalidToken())
        ).andReturn();
        
        this.printResponse("useInvalidToken", mvcResult);
    }

実行結果

[useInvalidToken]
status : 403
errorMessage : Invalid CSRF Token 'invalidd19aed27-65e2-4cf6-9456-157e1f29c984' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.

説明

  • csrf().useInvalidToken() とすると、無効なトークンがセットされる

ユーザーの指定

ユーザーの指定には、次の2つの方法がある

  • RequestPostProcessor を使った方法
  • アノテーションを使った方法

RequestPostProcessor を使った方法

Spring MVC Test が提供するリクエストの拡張ポイントを利用した方法。
アノテーションによる指定と比べると、動的な指定が可能なのが特徴と思われる。

実装

MyMvcController.java
package sample.spring.security.control;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.stream.Collectors;

@Controller
@RequestMapping("/mvc")
public class MyMvcController {
    
    @GetMapping
    public String hello() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        String name = auth.getName();
        Object credentials = auth.getCredentials();
        Object principal = auth.getPrincipal();
        String authorities = auth.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(", "));

        System.out.println(
            "name = " + name + "\n" + 
            "credentials = " + credentials + "\n" + 
            "authorities = " + authorities + "\n" +
            "principal = " + principal
        );
        
        return "test";
    }
}
MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.List;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    private static final String USERNAME = "foo";
    private static final String PASSWORD = "test-pass";
    private static final List<GrantedAuthority> AUTHORITIES = AuthorityUtils.createAuthorityList("FOO", "BAR");
    
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(SecurityMockMvcConfigurers.springSecurity())
                    .build();
    }

    @Test
    public void withUser() throws Exception {
        System.out.println("[withUser]");
        this.mvc.perform(
            get("/mvc").with(user(USERNAME))
        );
    }

    @Test
    public void customized() throws Exception {
        System.out.println("[customized]");
        this.mvc.perform(
            get("/mvc").with(
                user(USERNAME)
                .password(PASSWORD)
                .authorities(AUTHORITIES)
            )
        );
    }

    @Test
    public void userDetails() throws Exception {
        System.out.println("[userDetails]");
        UserDetails user = this.createUserDetails();

        this.mvc.perform(
            get("/mvc").with(user(user))
        );
    }

    @Test
    public void withAnonymous() throws Exception {
        System.out.println("[withAnonymous]");

        this.mvc.perform(
            get("/mvc").with(anonymous())
        );
    }

    @Test
    public void withAuthentication() throws Exception {
        System.out.println("[withAuthentication]");
        Authentication auth = this.createAuthentication();

        this.mvc.perform(
            get("/mvc").with(authentication(auth))
        );
    }

    @Test
    public void withSecurityContext() throws Exception {
        System.out.println("[withAuthentication]");
        Authentication auth = this.createAuthentication();
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(auth);

        this.mvc.perform(
            get("/mvc").with(securityContext(context))
        );
    }

    private UserDetails createUserDetails() {
        return new User(USERNAME, PASSWORD, AUTHORITIES);
    }
    
    private Authentication createAuthentication() {
        UserDetails user = this.createUserDetails();
        return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }
}

実行結果

[withUser]
name = foo
credentials = password
authorities = ROLE_USER
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER

[customized]
name = foo
credentials = test-pass
authorities = BAR, FOO
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO

[userDetails]
name = foo
credentials = test-pass
authorities = BAR, FOO
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO

[withAnonymous]
name = anonymous
credentials = 
authorities = ROLE_ANONYMOUS
principal = anonymous

[withAuthentication]
name = foo
credentials = test-pass
authorities = BAR, FOO
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO

[withSecurityContext]
name = foo
credentials = test-pass
authorities = BAR, FOO
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO

説明

  • user(String) だけを指定した場合は、ユーザー名だけが指定された値になる
    • パスワードは password
    • 権限は ROLE_USER だけになる
  • user(String) に続けて password(String)authorities(Collection<? extends GrantedAuthority>) で細かい設定が可能
    • roles(String...) でロール指定も可能
  • user(UserDetails) なら、 UserDetails を直接指定できる
  • anonymous() で、匿名ユーザーを指定できる
  • authentication(Authentication) で、 Authentication を直接指定できる
  • securityContext(SecurityContext) で、 SecurityContext を直接指定できる

デフォルトのユーザー情報を設定する

実装

MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .defaultRequest(get("/")
                        .with(
                            user("foo")
                            .password("test-pass")
                            .authorities(AuthorityUtils.createAuthorityList("FOO", "BAR"))
                        )
                    )
                    .build();
    }

    @Test
    public void test() throws Exception {
        this.mvc.perform(get("/mvc"));
    }
}

実行結果

name = foo
credentials = test-pass
authorities = BAR, FOO
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO

説明

MyMvcControllerTest.java
    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .defaultRequest(get("/")
                        .with(
                            user("foo")
                            .password("test-pass")
                            .authorities(AuthorityUtils.createAuthorityList("FOO", "BAR"))
                        )
                    )
                    .build();
    }
  • MockMvc を作るときに defaultRequest() でユーザーの情報を指定しておけば、デフォルトの設定として利用できる

よく使うユーザー定義をまとめる

実装

MyMockUserPostProcessors.java
package sample.spring.security.test;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.test.web.servlet.request.RequestPostProcessor;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;

public class MyMockUserPostProcessors {
    
    public static RequestPostProcessor hoge() {
        return user("hoge")
                .password("test-pass")
                .authorities(AuthorityUtils.createAuthorityList("FOO", "BAR"));
    }
}
MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static sample.spring.security.test.MyMockUserPostProcessors.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .build();
    }
    
    @Test
    public void test() throws Exception {
        this.mvc.perform(get("/mvc").with(hoge()));
    }
}

実行結果

name = hoge
credentials = test-pass
authorities = BAR, FOO
principal = org.springframework.security.core.userdetails.User@30f425: Username: hoge; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO

説明

MyMockUserPostProcessors.java
    public static RequestPostProcessor hoge() {
        return user("hoge")
                .password("test-pass")
                .authorities(AuthorityUtils.createAuthorityList("FOO", "BAR"));
    }
  • よく使う設定の RequestPostProcessor の定義を static メソッドで呼び出せるようにしておけば、使いまわしが楽になる

アノテーションを使った方法

最初の方で書いた @WithMockUser などのアノテーションを使った方法。

RequestPostProcessor を使った方法と比べると、動的な変更が不要な場合にメソッドやクラスへのアノテーションで済み、より宣言的になる特徴がある気がしなくもない。
あとは、 MVC 以外のテストとモックユーザの指定方法を共通化できる、とか。

実装

MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .build();
    }

    @Test
    @WithMockUser(username="foo", password="test-pass", authorities={"FOO", "BAR"})
    public void test() throws Exception {
        this.mvc.perform(get("/mvc"));
    }
}

実行結果

name = foo
credentials = test-pass
authorities = BAR, FOO
principal = org.springframework.security.core.userdetails.User@18cc6: Username: foo; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: BAR,FOO

説明

  • MVC でないときの使用方法と一緒なので、特記することはなさげ

Form ログイン

実装

test-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    <bean class="sample.spring.security.control.MyMvcController" />

    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
    </sec:http>

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="user" password="password" authorities="FOO" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .build();
    }

    @Test
    public void test() throws Exception {
        MvcResult result = this.mvc.perform(formLogin()).andReturn();
        this.printResponse("test", result);
    }

    private void printResponse(String method, MvcResult result) throws Exception {
        MockHttpServletResponse response = result.getResponse();
        int status = response.getStatus();
        String location = response.getHeader("Location");

        System.out.println("[" + method + "]\n" +
                "status : " + status + "\n" +
                "location : " + location;
    }
}

実行結果

[test]
status : 302
location : /
  • ログインに成功してログイン後のデフォルトの遷移先であるルート / に飛ばされている

説明

test-applicationContext.xml
    <sec:user name="user" password="password1" authorities="FOO" />
  • Form ログインでは通常のロジックが実行されるので、ユーザーは実際に存在しなければならない
MyMvcControllerTest.java
    @Test
    public void test() throws Exception {
        MvcResult result = this.mvc.perform(formLogin()).andReturn();
        this.printResponse("test", result);
    }
  • SecurityMockMvcRequestBuilders.formLogin()perform() にセットすることで、 Form ログインが実行される
  • デフォルトでは、次のリクエストが実行される
    • リクエストパス: /login
    • メソッド: POST
    • ユーザ名: user
    • パスワード: password
    • ユーザー名のパラメータ名: username
    • パスワードのパラメータ名: password

各種設定を指定する

実装

test-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    <bean class="sample.spring.security.control.MyMvcController" />

    <sec:http>
        <sec:intercept-url pattern="/do-login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login login-processing-url="/do-login"
                        username-parameter="login-id"
                        password-parameter="pass" />
        <sec:logout />
    </sec:http>

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="user" password="password" authorities="FOO" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
  • ログインURL やユーザ名・パスワードのパラメータ名をデフォルトとは異なる値に設定
MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .build();
    }

    @Test
    public void test() throws Exception {
        MvcResult result = this.mvc.perform(
            formLogin("/do-login")
            .userParameter("login-id")
            .passwordParam("pass")
            .user("user")
            .password("password")
        ).andReturn();

        this.printResponse("test", result);
    }

    private void printResponse(String method, MvcResult result) throws Exception {
        ...
    }
}

実行結果

[test]
status : 302
location : /

説明

MyMvcControllerTest.java
    @Test
    public void test() throws Exception {
        MvcResult result = this.mvc.perform(
            formLogin("/do-login")
            .userParameter("login-id")
            .passwordParam("pass")
            .user("user")
            .password("password")
        ).andReturn();

        this.printResponse("test", result);
    }
  • ログイン処理のパスがデフォルト(/login)と異なる場合は formLogin() の引数でパスを指定できる
    • loginProcessingUrl() でも指定可能
  • ユーザー名のパラメータ名がデフォルト(username)と異なる場合は、 userParameter() で指定できる
  • パスワードのパラメータ名がデフォルト(password)と異なる場合は、 passwordParam() で指定できる

ログアウト

実装

MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .build();
    }

    @Test
    public void test() throws Exception {
        MvcResult result = this.mvc.perform(logout()).andReturn();
        this.printResponse("test", result);
    }

    private void printResponse(String method, MvcResult result) throws Exception {
        ...
    }
}

実行結果

[test]
status : 302
location : /login?logout

説明

  • SecurityMockMvcRequestBuilders.logout()perform() に渡すことで、ログアウトのリクエストが実行される
  • ログアウトの URL がデフォルト(/logout)と異なる場合は、 logout("/other-logout-url") のように引数で指定できる

ログイン後の認証情報の検証

実装

test-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    <bean class="sample.spring.security.control.MyMvcController" />

    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
    </sec:http>

    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="foo" password="foo" authorities="FOO, BAR" />
                <sec:user name="fizz" password="fizz" authorities="ROLE_FIZZ, ROLE_BUZZ" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
  • foo ユーザには FOOBAR の権限を設定
  • fizz ユーザには ROLE_FIZZROLE_BUZZ のロールを設定
MyMvcControllerTest.java
package sample.spring.security.test;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.List;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
@WebAppConfiguration
public class MyMvcControllerTest {
    @Autowired
    private WebApplicationContext context;
    
    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        this.mvc = MockMvcBuilders
                    .webAppContextSetup(this.context)
                    .apply(springSecurity())
                    .build();
    }

    @Test
    public void test_unauthenticated() throws Exception {
        this.mvc.perform(formLogin().user("foo").password("invalid"))
                .andExpect(unauthenticated());
    }

    @Test
    public void test_authenticated() throws Exception {
        this.mvc.perform(formLogin().user("foo").password("foo"))
                .andExpect(authenticated());
    }

    @Test
    public void test_withUsername() throws Exception {
        this.mvc.perform(formLogin().user("foo").password("foo"))
                .andExpect(authenticated().withUsername("foo"));
    }

    @Test
    public void test_withAuthorities() throws Exception {
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("FOO", "BAR");
        
        this.mvc.perform(formLogin().user("foo").password("foo"))
                .andExpect(authenticated().withAuthorities(authorities));
    }

    @Test
    public void test_combine() throws Exception {
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("FOO", "BAR");
        
        this.mvc.perform(formLogin().user("foo").password("foo"))
                .andExpect(authenticated().withUsername("foo").withAuthorities(authorities));
    }

    @Test
    public void test_withRoles() throws Exception {
        this.mvc.perform(formLogin().user("fizz").password("fizz"))
                .andExpect(authenticated().withRoles("FIZZ", "BUZZ"));
    }
}

実行結果

全てのテストが成功する

説明

MyMvcControllerTest.java
    @Test
    public void test_unauthenticated() throws Exception {
        this.mvc.perform(formLogin().user("foo").password("invalid"))
                .andExpect(unauthenticated());
    }
  • perform() に続けて andExpect() を使って、認証結果の検証ができる
  • SecurityMockMvcResultMatchersstatic メソッドが用意されており、それらを利用する
メソッド名 検証内容
unauthenticated() 認証されていないことを検証する
authenticated() 認証されていることを検証する
これに withUsername() などを続けることで、詳しい検証が可能
withUsername(String) ユーザ名を検証する
withAuthorities(Collection<? extends GrantedAuthority>) 付与された権限を検証する
withRoles(String...) 付与されたロールを検証する
MyMvcControllerTest.java
    @Test
    public void test_combine() throws Exception {
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("FOO", "BAR");
        
        this.mvc.perform(formLogin().user("foo").password("foo"))
                .andExpect(authenticated().withUsername("foo").withAuthorities(authorities));
    }
  • withUsername()withAuthorities() は複数組み合わせることが可能

参考

54
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
58