Spring Security 使い方メモ　テスト


:::note warn
Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの [Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita](https://qiita.com/suke_masa/items/908805dd45df08ba28d8) を参照してください。
:::



[基礎・仕組み的な話](http://qiita.com/opengl-8080/items/c105152c9ca48509bd0c)
[認証・認可の話](http://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc)
[Remember-Me の話](http://qiita.com/opengl-8080/items/7c34053c74448d39e8f5)
[CSRF の話](http://qiita.com/opengl-8080/items/3720ed415c2df26251a7)
[セッション管理の話](http://qiita.com/opengl-8080/items/ad9159910501d1989876)
[レスポンスヘッダーの話](http://qiita.com/opengl-8080/items/4e800eb6990653b0395c)
[メソッドセキュリティの話](http://qiita.com/opengl-8080/items/76715fd3a92b6ca680d0)
[CORS の話](http://qiita.com/opengl-8080/items/db0aa1ec0be36886953b)
[Run-As の話](http://qiita.com/opengl-8080/items/49aa6381c16c8b9a7f3c)
[ACL の話](http://qiita.com/opengl-8080/items/24a3118ef36bcf55ba71)
[MVC, Boot との連携の話](http://qiita.com/opengl-8080/items/4824d4a8c012f7ec53db)

番外編
[Spring Security にできること・できないこと](http://qiita.com/opengl-8080/items/6dc37f8b77abb5ae1642)


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

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

# Hello World
## 実装
```groovy: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 のモジュールを依存関係に追加

```java: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**

```xml: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 として登録

```java: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**

```java: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 のやつと同じ

```java: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 の設定
```java:MyTestServiceTest.java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
```

- この２つのアノテーションは、 Spring Test のための設定で、とくに Spring Security 用の設定というわけではない
- ここの設定で、テスト用の `ApplicationContext` を構築している
- `@ContextConfiguration` で、テスト用に使用する Spring の設定を指定する
- namespace の場合は、xml のクラスパス上の場所、 Java Configuration の場合は設定クラスの `Class` オブジェクトを渡す

### モックユーザーの指定
```java:MyTestServiceTest.java
    @Test
    @WithMockUser(username = "hoge")
    public void test_getMessage() throws Exception {
```

- `@WithMockUser` でアノテートすることで、そのテストが実行されている間に使用されるユーザーの情報（`Authentication`）を指定できる
- `username = "hoge"` としているので、ユーザ名が `hoge` の `Authentication` が使用されることになる
- Spring Security は Spring Test と統合することで、テストの前後で `SecurityContextHolder` の情報を設定・クリアしてくれるようになっている

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

## `@WithMockUser`
```java: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` はモックユーザなので、その名前のユーザが実際に存在する必要はない
- クラスをアノテートすることも可能（その場合は、全てのメソッドでモックユーザが使用される）

### ユーザー名を設定する
```java: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` を利用したほうが分かりやすくなる

### ロールを設定する
```java: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` と一緒に設定するとエラーになる

### 権限を設定する
```java: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` と一緒に設定するとエラーになる

### パスワードを設定する

```java: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`
```java: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`
```java: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);
        }
    }
}
```

```xml: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>
```

```java: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` を使用している場合などに利用する
- `@WithUserDetails` の `value` で、検索するユーザの名前を指定できる（デフォルトは `"user"`）
- `userDetailsServiceBeanName` で、使用する `UserDetailsService` Bean を指定できる

## `@WithSecurityContext`
```java: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();
}
```

```java: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;
    }
}
```

```java: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` を任意の実装で自作することができる

```java:MyTestUser.java
@WithSecurityContext(factory=MyTestUserFactory.class)
public @interface MyTestUser {
```

- 任意のアノテーションを作成し、 `@WithSecurityContext` でアノテートする
- `factory` に、 `WithSecurityContextFactory` インターフェースを実装したクラスの `Class` オブジェクトを指定する
- ここで指定したファクトリクラスが、 `SecurityContext` を作成する処理を実装する

```java: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` がアノテーションからファクトリを生成してくれる

```java:MyTestServiceTest.java
    @MyTestUser(name="foo", pass="FOO", authority="TEST_FOO")
    public void test() throws Exception {
```

- あとは、テストメソッドを自作のアノテーションで注釈すれば、指定したファクトリで作成された `SecurityContext` が認証情報として使用されるようになる

### ファクトリクラスにコンテナ管理の Bean をインジェクションする
```java:MyTestUserFactory.java
public class MyTestUserFactory implements WithSecurityContextFactory<MyTestUser> {
    private UserDetailsService userDetailsService;

    @Autowired
    public MyTestUserFactory(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
```

- ファクトリクラスは通常の Bean と同じ要領で他の Bean をインジェクションできる
- 上記実装はコンストラクタインジェクションを利用している（`@Autowired` は省略可能）


## メタアノテーション
```java: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 {
}
```

```java: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
**実装**

```groovy: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` の依存関係を追加


```java: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` の名前を出力している



```xml: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 宣言

```java: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);
    }
}
```

- テストメソッドは `unauthorized` と `authorized` の２つ
- それぞれ `/mvc` に対してアクセスし、結果の HTTP ステータスと `Location` ヘッダーを出力している

**実行結果**

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

[unauthorized]
status : 302
Location : http://localhost/login
```

- `authorized` はコントローラが実行できて、ユーザー名が出力されている
- `unauthorized` は `302` のステータスが返されて、ログイン画面へリダイレクトを促されている

**説明**

```java: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 の処理が動くようになっているっぽい

```java: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()` は `MockMvcRequestBuilders` の `static` メソッド
- `user()` は `SecurityMockMvcRequestPostProcessors` の `static` メソッド
    - 実行時のログインユーザの名前を指定している
    - Spring MVC Test には、リクエストを書き換えるための仕組みがあり、 `RequestPostProcessor` インターフェースを実装したクラスを利用する
    - Spring Security は、 Spring Security 用に設定しやすいように、この `RequestPostProcessor` を実装したクラスをたくさん用意している
    - それらのクラスを利用するための窓口として、 `static` なファクトリメソッドを集めた `SecurityMockMvcRequestPostProcessors` というクラスが用意されている

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

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

**実装**

```xml: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 保護機能を有効にする

```java: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 メソッドでは実行されないので

```java: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()` はコントローラのメソッド実行に成功している

**説明**

```java: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()` とするらしい

### 不正なトークンをセットする
**実装**

```java: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()` とすると、無効なトークンがセットされる

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

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

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

**実装**

```java: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";
    }
}
```

```java: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` を直接指定できる

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

```java: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
```

**説明**

```java: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()` でユーザーの情報を指定しておけば、デフォルトの設定として利用できる

#### よく使うユーザー定義をまとめる
**実装**

```java: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"));
    }
}
```

```java: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
```

**説明**

```java:MyMockUserPostProcessors.java
    public static RequestPostProcessor hoge() {
        return user("hoge")
                .password("test-pass")
                .authorities(AuthorityUtils.createAuthorityList("FOO", "BAR"));
    }
```

- よく使う設定の `RequestPostProcessor` の定義を `static` メソッドで呼び出せるようにしておけば、使いまわしが楽になる

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

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

**実装**

```java: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 ログイン
**実装**

```xml: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>
```

```java: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 : /
```

- ログインに成功してログイン後のデフォルトの遷移先であるルート `/` に飛ばされている

**説明**

```xml:test-applicationContext.xml
    <sec:user name="user" password="password1" authorities="FOO" />
```

- Form ログインでは通常のロジックが実行されるので、ユーザーは実際に存在しなければならない

```java: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`

### 各種設定を指定する
**実装**

```xml: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 やユーザ名・パスワードのパラメータ名をデフォルトとは異なる値に設定

```java: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 : /
```

**説明**

```java: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()` で指定できる

## ログアウト
**実装**

```java: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")` のように引数で指定できる

## ログイン後の認証情報の検証
**実装**

```xml: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` ユーザには `FOO` と `BAR` の権限を設定
- `fizz` ユーザには `ROLE_FIZZ` と `ROLE_BUZZ` のロールを設定

```java: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"));
    }
}
```

**実行結果**

全てのテストが成功する

**説明**

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

- `perform()` に続けて `andExpect()` を使って、認証結果の検証ができる
- `SecurityMockMvcResultMatchers` に `static` メソッドが用意されており、それらを利用する


|メソッド名|検証内容|
|---------|--------|
|`unauthenticated()`|認証されていないことを検証する|
|`authenticated()`|認証されていることを検証する<br>これに `withUsername()` などを続けることで、詳しい検証が可能|
|`withUsername(String)`|ユーザ名を検証する|
|`withAuthorities(Collection<? extends GrantedAuthority>)`|付与された権限を検証する|
|`withRoles(String...)`|付与されたロールを検証する|

```java: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()` は複数組み合わせることが可能



# 参考
- [Spring Security Reference](http://docs.spring.io/spring-security/site/docs/4.2.3.BUILD-SNAPSHOT/reference/htmlsingle/#test)