Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita を参照してください。
基礎・仕組み的な話
認証・認可の話
Remember-Me の話
CSRF の話
セッション管理の話
レスポンスヘッダーの話
メソッドセキュリティの話
CORS の話
Run-As の話
ACL の話
MVC, Boot との連携の話
番外編
Spring Security にできること・できないこと
Spring Security のテスト
Spring Security には、 JUnit によるテストをサポートする仕組みが用意されている。
例えば、テストのときに使用するユーザを固定したり、権限を指定したりすることができる。
Hello World
実装
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 のモジュールを依存関係に追加
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 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 として登録
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
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 のやつと同じ
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 の設定
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/test-applicationContext.xml")
- この2つのアノテーションは、 Spring Test のための設定で、とくに Spring Security 用の設定というわけではない
- ここの設定で、テスト用の
ApplicationContext
を構築している -
@ContextConfiguration
で、テスト用に使用する Spring の設定を指定する - namespace の場合は、xml のクラスパス上の場所、 Java Configuration の場合は設定クラスの
Class
オブジェクトを渡す
モックユーザーの指定
@Test
@WithMockUser(username = "hoge")
public void test_getMessage() throws Exception {
-
@WithMockUser
でアノテートすることで、そのテストが実行されている間に使用されるユーザーの情報(Authentication
)を指定できる -
username = "hoge"
としているので、ユーザ名がhoge
のAuthentication
が使用されることになる - Spring Security は Spring Test と統合することで、テストの前後で
SecurityContextHolder
の情報を設定・クリアしてくれるようになっている
アノテーション
アノテーションによってテスト中の SecurityContext
に任意の情報を設定することができる。
@WithMockUser
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
はモックユーザなので、その名前のユーザが実際に存在する必要はない - クラスをアノテートすることも可能(その場合は、全てのメソッドでモックユーザが使用される)
ユーザー名を設定する
@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
を利用したほうが分かりやすくなる
- 後述するロールなどと一緒に設定する場合は、
ロールを設定する
@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
と一緒に設定するとエラーになる
権限を設定する
@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
と一緒に設定するとエラーになる
パスワードを設定する
@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
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
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 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>
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
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();
}
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;
}
}
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
を任意の実装で自作することができる
@WithSecurityContext(factory=MyTestUserFactory.class)
public @interface MyTestUser {
- 任意のアノテーションを作成し、
@WithSecurityContext
でアノテートする -
factory
に、WithSecurityContextFactory
インターフェースを実装したクラスのClass
オブジェクトを指定する - ここで指定したファクトリクラスが、
SecurityContext
を作成する処理を実装する
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
がアノテーションからファクトリを生成してくれる
@MyTestUser(name="foo", pass="FOO", authority="TEST_FOO")
public void test() throws Exception {
- あとは、テストメソッドを自作のアノテーションで注釈すれば、指定したファクトリで作成された
SecurityContext
が認証情報として使用されるようになる
ファクトリクラスにコンテナ管理の Bean をインジェクションする
public class MyTestUserFactory implements WithSecurityContextFactory<MyTestUser> {
private UserDetailsService userDetailsService;
@Autowired
public MyTestUserFactory(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
- ファクトリクラスは通常の Bean と同じ要領で他の Bean をインジェクションできる
- 上記実装はコンストラクタインジェクションを利用している(
@Autowired
は省略可能)
メタアノテーション
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 {
}
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
実装
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
の依存関係を追加
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 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 宣言
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
の2つ - それぞれ
/mvc
に対してアクセスし、結果の HTTP ステータスとLocation
ヘッダーを出力している
実行結果
auth.name = foo
[authorized]
status : 200
Location : null
[unauthorized]
status : 302
Location : http://localhost/login
-
authorized
はコントローラが実行できて、ユーザー名が出力されている -
unauthorized
は302
のステータスが返されて、ログイン画面へリダイレクトを促されている
説明
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 の処理が動くようになっているっぽい
- この設定で、 Spring Security の
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 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 保護機能を有効にする
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 メソッドでは実行されないので
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()
はコントローラのメソッド実行に成功している
説明
@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()
とするらしい
不正なトークンをセットする
実装
@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 が提供するリクエストの拡張ポイントを利用した方法。
アノテーションによる指定と比べると、動的な指定が可能なのが特徴と思われる。
実装
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";
}
}
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
を直接指定できる
デフォルトのユーザー情報を設定する
実装
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
説明
@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()
でユーザーの情報を指定しておけば、デフォルトの設定として利用できる
よく使うユーザー定義をまとめる
実装
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"));
}
}
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
説明
public static RequestPostProcessor hoge() {
return user("hoge")
.password("test-pass")
.authorities(AuthorityUtils.createAuthorityList("FOO", "BAR"));
}
- よく使う設定の
RequestPostProcessor
の定義をstatic
メソッドで呼び出せるようにしておけば、使いまわしが楽になる
アノテーションを使った方法
最初の方で書いた @WithMockUser
などのアノテーションを使った方法。
RequestPostProcessor
を使った方法と比べると、動的な変更が不要な場合にメソッドやクラスへのアノテーションで済み、より宣言的になる特徴がある気がしなくもない。
あとは、 MVC 以外のテストとモックユーザの指定方法を共通化できる、とか。
実装
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 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>
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 : /
- ログインに成功してログイン後のデフォルトの遷移先であるルート
/
に飛ばされている
説明
<sec:user name="user" password="password1" authorities="FOO" />
- Form ログインでは通常のロジックが実行されるので、ユーザーは実際に存在しなければならない
@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 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 やユーザ名・パスワードのパラメータ名をデフォルトとは異なる値に設定
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 : /
説明
@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()
で指定できる
ログアウト
実装
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 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
のロールを設定
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"));
}
}
実行結果
全てのテストが成功する
説明
@Test
public void test_unauthenticated() throws Exception {
this.mvc.perform(formLogin().user("foo").password("invalid"))
.andExpect(unauthenticated());
}
-
perform()
に続けてandExpect()
を使って、認証結果の検証ができる -
SecurityMockMvcResultMatchers
にstatic
メソッドが用意されており、それらを利用する
メソッド名 | 検証内容 |
---|---|
unauthenticated() |
認証されていないことを検証する |
authenticated() |
認証されていることを検証する これに withUsername() などを続けることで、詳しい検証が可能 |
withUsername(String) |
ユーザ名を検証する |
withAuthorities(Collection<? extends GrantedAuthority>) |
付与された権限を検証する |
withRoles(String...) |
付与されたロールを検証する |
@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()
は複数組み合わせることが可能