メソッドのセキュリティテスト

このセクションでは、Spring Security のテストサポートを使用してメソッドベースのセキュリティをテストするメソッドを示します。最初に、ユーザーがアクセスできるように認証される必要がある MessageService を紹介します。

  • Java

  • Kotlin

public class HelloMessageService implements MessageService {

	@PreAuthorize("authenticated")
	public String getMessage() {
		Authentication authentication = SecurityContextHolder.getContext()
			.getAuthentication();
		return "Hello " + authentication;
	}
}
class HelloMessageService : MessageService {
    @PreAuthorize("authenticated")
    fun getMessage(): String {
        val authentication: Authentication = SecurityContextHolder.getContext().authentication
        return "Hello $authentication"
    }
}

getMessage の結果は、現在の Spring Security Authentication に "Hello" と言う String です。次のリストは出力例を示しています。

Hello org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

セキュリティテストのセットアップ

Spring Security テストサポートを使用する前に、いくつかのセットアップを実行する必要があります。

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
	// ...
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {
    // ...
}
1@ExtendWith は、ApplicationContext を作成するように spring-test モジュールに指示します。詳細については、Spring リファレンスを参照してください。
2@ContextConfiguration は、ApplicationContext の作成に使用する構成を spring-test に指示します。構成が指定されていないため、デフォルトの構成場所が試されます。これは、既存の Spring Test サポートを使用するのと同じです。詳細については、Spring リファレンスを参照してください。

Spring Security は、WithSecurityContextTestExecutionListener を介して Spring Test サポートに接続します。これにより、テストが正しいユーザーで実行されることが保証されます。これは、テストを実行する前に SecurityContextHolder にデータを入力することによって行われます。リアクティブメソッドセキュリティを使用する場合は、ReactiveSecurityContextHolder にデータを入力する ReactorContextTestExecutionListener も必要です。テストが完了すると、SecurityContextHolder がクリアされます。Spring Security 関連のサポートのみが必要な場合は、@ContextConfiguration を @SecurityTestExecutionListeners に置き換えることができます。

@PreAuthorize アノテーションを HelloMessageService に追加したため、認証されたユーザーがそれを呼び出す必要があることを忘れないでください。テストを実行すると、次のテストに合格することが期待されます。

  • Java

  • Kotlin

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
	messageService.getMessage();
}
@Test(expected = AuthenticationCredentialsNotFoundException::class)
fun getMessageUnauthenticated() {
    messageService.getMessage()
}

@WithMockUser

問題は、「特定のユーザーとしてどのようにテストを最も簡単に実行できるか」です。答えは @WithMockUser を使用することです。次のテストは、ユーザー名 "user"、パスワード "password"、ロール "ROLE_USER" を持つユーザーとして実行されます。

  • Java

  • Kotlin

@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
@Test
@WithMockUser
fun getMessageWithMockUser() {
    val message: String = messageService.getMessage()
    // ...
}

具体的には、次のことが当てはまります。

  • ユーザーオブジェクトをモックするため、ユーザー名が user のユーザーは存在する必要はありません。

  • SecurityContext に入力される Authentication は、型 UsernamePasswordAuthenticationToken です。

  • Authentication のプリンシパルは、Spring Security の User オブジェクトです。

  • User のユーザー名は user です。

  • User のパスワードは password です。

  • ROLE_USER という名前の単一の GrantedAuthority が使用されます。

前の例は、多くのデフォルトを使用できるため便利です。別のユーザー名でテストを実行したい場合はどうなるでしょうか? 次のテストは、customUser のユーザー名で実行されます(ここでも、ユーザーは実際に存在する必要はありません)。

  • Java

  • Kotlin

@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
	String message = messageService.getMessage();
...
}
@Test
@WithMockUser("customUsername")
fun getMessageWithMockUserCustomUsername() {
    val message: String = messageService.getMessage()
    // ...
}

ロールを簡単にカスタマイズすることもできます。例: 次のテストは、admin のユーザー名と ROLE_USER および ROLE_ADMIN のロールで呼び出されます。

  • Java

  • Kotlin

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
	String message = messageService.getMessage();
	...
}
@Test
@WithMockUser(username="admin",roles=["USER","ADMIN"])
fun getMessageWithMockUserCustomUser() {
    val message: String = messageService.getMessage()
    // ...
}

値の前に ROLE_ を自動的に付けたくない場合は、authorities 属性を使用できます。例: 次のテストは、admin のユーザー名と USER および ADMIN 権限で呼び出されます。

  • Java

  • Kotlin

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
	String message = messageService.getMessage();
	...
}
@Test
@WithMockUser(username = "admin", authorities = ["ADMIN", "USER"])
fun getMessageWithMockUserCustomUsername() {
    val message: String = messageService.getMessage()
    // ...
}

すべてのテストメソッドにアノテーションを配置するのは少し面倒です。代わりに、クラスレベルでアノテーションを配置できます。次に、すべてのテストで指定されたユーザーが使用されます。次の例では、ユーザー名が admin、パスワードが password で、ROLE_USER および ROLE_ADMIN のロールを持つユーザーを使用してすべてのテストを実行します。

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
	// ...
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles=["USER","ADMIN"])
class WithMockUserTests {
    // ...
}

JUnit 5 の @Nested テストサポートを使用する場合は、囲んでいるクラスにアノテーションを配置して、ネストされたすべてのクラスに適用することもできます。次の例では、ユーザー名が admin、パスワードが password であり、両方のテストメソッドで ROLE_USER と ROLE_ADMIN のロールを持つユーザーを使用してすべてのテストを実行します。

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {

	@Nested
	public class TestSuite1 {
		// ... all test methods use admin user
	}

	@Nested
	public class TestSuite2 {
		// ... all test methods use admin user
	}
}
@ExtendWith(SpringExtension::class)
@ContextConfiguration
@WithMockUser(username = "admin", roles = ["USER", "ADMIN"])
class WithMockUserTests {
    @Nested
    inner class TestSuite1 { // ... all test methods use admin user
    }

    @Nested
    inner class TestSuite2 { // ... all test methods use admin user
    }
}

デフォルトでは、SecurityContext は TestExecutionListener.beforeTestMethod イベント中に設定されます。これは、JUnit の @Before の前に発生するのと同じです。これを変更して、TestExecutionListener.beforeTestExecution イベント中に発生するようにすることができます。これは、JUnit の @Before の後、テストメソッドが呼び出される前です。

@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithAnonymousUser

@WithAnonymousUser を使用すると、匿名ユーザーとして実行できます。これは、特定のユーザーでほとんどのテストを実行したいが、匿名ユーザーとしていくつかのテストを実行したい場合に特に便利です。次の例では、@WithMockUser および anonymous を匿名ユーザーとして使用して withMockUser1 および withMockUser2 を実行します。

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

	@Test
	public void withMockUser1() {
	}

	@Test
	public void withMockUser2() {
	}

	@Test
	@WithAnonymousUser
	public void anonymous() throws Exception {
		// override default to run as anonymous user
	}
}
@ExtendWith(SpringExtension.class)
@WithMockUser
class WithUserClassLevelAuthenticationTests {
    @Test
    fun withMockUser1() {
    }

    @Test
    fun withMockUser2() {
    }

    @Test
    @WithAnonymousUser
    fun anonymous() {
        // override default to run as anonymous user
    }
}

デフォルトでは、SecurityContext は TestExecutionListener.beforeTestMethod イベント中に設定されます。これは、JUnit の @Before の前に発生するのと同じです。これを変更して、TestExecutionListener.beforeTestExecution イベント中に発生するようにすることができます。これは、JUnit の @Before の後、テストメソッドが呼び出される前です。

@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithUserDetails

@WithMockUser は開始するのに便利な方法ですが、すべての場合に機能するとは限りません。例: 一部のアプリケーションは、Authentication プリンシパルが特定の型であることを期待しています。これは、アプリケーションがプリンシパルをカスタム型として参照し、Spring Security での結合を減らすことができるようにするために行われます。

カスタムプリンシパルは、多くの場合、UserDetails とカスタム型の両方を実装するオブジェクトを返すカスタム UserDetailsService によって返されます。このような状況では、カスタム UserDetailsService を使用してテストユーザーを作成すると便利です。それはまさに @WithUserDetails が行うことです。

UserDetailsService が Bean として公開されていると仮定すると、次のテストは、型 UsernamePasswordAuthenticationToken の Authentication と、ユーザー名 user で UserDetailsService から返されるプリンシパルを使用して呼び出されます。

  • Java

  • Kotlin

@Test
@WithUserDetails
public void getMessageWithUserDetails() {
	String message = messageService.getMessage();
	...
}
@Test
@WithUserDetails
fun getMessageWithUserDetails() {
    val message: String = messageService.getMessage()
    // ...
}

UserDetailsService からユーザーを検索するために使用するユーザー名をカスタマイズすることもできます。例: このテストは、customUsername のユーザー名で UserDetailsService から返されるプリンシパルを使用して実行できます。

  • Java

  • Kotlin

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
	String message = messageService.getMessage();
	...
}
@Test
@WithUserDetails("customUsername")
fun getMessageWithUserDetailsCustomUsername() {
    val message: String = messageService.getMessage()
    // ...
}

UserDetailsService を検索するための明示的な Bean 名を指定することもできます。次のテストでは、Bean 名が myUserDetailsService の UserDetailsService を使用して、customUsername のユーザー名を検索します。

  • Java

  • Kotlin

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
	String message = messageService.getMessage();
	...
}
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
fun getMessageWithUserDetailsServiceBeanName() {
    val message: String = messageService.getMessage()
    // ...
}

@WithMockUser で行ったように、すべてのテストが同じユーザーを使用するように、クラスレベルでアノテーションを配置することもできます。ただし、@WithMockUser とは異なり、@WithUserDetails ではユーザーが存在する必要があります。

デフォルトでは、SecurityContext は TestExecutionListener.beforeTestMethod イベント中に設定されます。これは、JUnit の @Before の前に発生するのと同じです。これを変更して、TestExecutionListener.beforeTestExecution イベント中に発生するようにすることができます。これは、JUnit の @Before の後、テストメソッドが呼び出される前です。

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithSecurityContext

カスタム Authentication プリンシパルを使用しない場合は、@WithMockUser が優れた選択肢であることがわかりました。次に、@WithUserDetails ではカスタム UserDetailsService を使用して Authentication プリンシパルを作成できますが、ユーザーが存在する必要があることを発見しました。これで、最も柔軟性のあるオプションが表示されます。

@WithSecurityContext を使用して独自のアノテーションを作成し、必要な SecurityContext を作成できます。例: @WithMockCustomUser という名前のアノテーションを作成する場合があります:

  • Java

  • Kotlin

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

	String username() default "rob";

	String name() default "Rob Winch";
}
@Retention(AnnotationRetention.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory::class)
annotation class WithMockCustomUser(val username: String = "rob", val name: String = "Rob Winch")

@WithMockCustomUser に @WithSecurityContext アノテーションが付けられていることがわかります。これは、テスト用に SecurityContext を作成する予定であることを Spring Security テストサポートに通知するものです。@WithSecurityContext アノテーションでは、@WithMockCustomUser アノテーションを指定して、新しい SecurityContext を作成するために SecurityContextFactory を指定する必要があります。次のリストは、WithMockCustomUserSecurityContextFactory の実装を示しています。

  • Java

  • Kotlin

public class WithMockCustomUserSecurityContextFactory
	implements WithSecurityContextFactory<WithMockCustomUser> {
	@Override
	public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
		SecurityContext context = SecurityContextHolder.createEmptyContext();

		CustomUserDetails principal =
			new CustomUserDetails(customUser.name(), customUser.username());
		Authentication auth =
			UsernamePasswordAuthenticationToken.authenticated(principal, "password", principal.getAuthorities());
		context.setAuthentication(auth);
		return context;
	}
}
class WithMockCustomUserSecurityContextFactory : WithSecurityContextFactory<WithMockCustomUser> {
    override fun createSecurityContext(customUser: WithMockCustomUser): SecurityContext {
        val context = SecurityContextHolder.createEmptyContext()
        val principal = CustomUserDetails(customUser.name, customUser.username)
        val auth: Authentication =
            UsernamePasswordAuthenticationToken(principal, "password", principal.authorities)
        context.authentication = auth
        return context
    }
}

これで、新しいアノテーションと Spring Security の WithSecurityContextTestExecutionListener を使用してテストクラスまたはテストメソッドにアノテーションを付け、SecurityContext が適切に設定されていることを確認できます。

独自の WithSecurityContextFactory 実装を作成する場合、標準の Spring アノテーションでアノテーションを付けることができることを知っておくと便利です。例: WithUserDetailsSecurityContextFactory は @Autowired アノテーションを使用して UserDetailsService を取得します。

  • Java

  • Kotlin

final class WithUserDetailsSecurityContextFactory
	implements WithSecurityContextFactory<WithUserDetails> {

	private UserDetailsService userDetailsService;

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

	public SecurityContext createSecurityContext(WithUserDetails withUser) {
		String username = withUser.value();
		Assert.hasLength(username, "value() must be non-empty String");
		UserDetails principal = userDetailsService.loadUserByUsername(username);
		Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(principal, principal.getPassword(), principal.getAuthorities());
		SecurityContext context = SecurityContextHolder.createEmptyContext();
		context.setAuthentication(authentication);
		return context;
	}
}
class WithUserDetailsSecurityContextFactory @Autowired constructor(private val userDetailsService: UserDetailsService) :
    WithSecurityContextFactory<WithUserDetails> {
    override fun createSecurityContext(withUser: WithUserDetails): SecurityContext {
        val username: String = withUser.value
        Assert.hasLength(username, "value() must be non-empty String")
        val principal = userDetailsService.loadUserByUsername(username)
        val authentication: Authentication =
            UsernamePasswordAuthenticationToken(principal, principal.password, principal.authorities)
        val context = SecurityContextHolder.createEmptyContext()
        context.authentication = authentication
        return context
    }
}

デフォルトでは、SecurityContext は TestExecutionListener.beforeTestMethod イベント中に設定されます。これは、JUnit の @Before の前に発生するのと同じです。これを変更して、TestExecutionListener.beforeTestExecution イベント中に発生するようにすることができます。これは、JUnit の @Before の後、テストメソッドが呼び出される前です。

@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)

メタアノテーションのテスト

テスト内で同じユーザーを頻繁に再利用する場合、属性を繰り返し指定する必要があるのは理想的ではありません。例: ユーザー名が admin で、ロールが ROLE_USER と ROLE_ADMIN の管理ユーザーに関連するテストが多数ある場合は、次のように記述する必要があります。

  • Java

  • Kotlin

@WithMockUser(username="admin",roles={"USER","ADMIN"})
@WithMockUser(username="admin",roles=["USER","ADMIN"])

これをどこでも繰り返すのではなく、メタアノテーションを使用できます。例: WithMockAdmin という名前のメタアノテーションを作成できます。

  • Java

  • Kotlin

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }
@Retention(AnnotationRetention.RUNTIME)
@WithMockUser(value = "rob", roles = ["ADMIN"])
annotation class WithMockAdmin

これで、より詳細な @WithMockUser と同じ方法で @WithMockAdmin を使用できます。

メタアノテーションは、上記のテストアノテーションのいずれかと連携します。例: これは、@WithUserDetails("admin") のメタアノテーションも作成できることを意味します。