Edited at

Spring Security 使い方メモ テスト

More than 1 year has passed since last update.

基礎・仕組み的な話

認証・認可の話

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


参考