最新の安定バージョンについては、Spring Security 6.5.0 を使用してください! |
メソッドのセキュリティテスト
このセクションでは、Spring Security のテストサポートを使用して、メソッドベースのセキュリティをテストするメソッドを示します。まず、MessageService
を導入します。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" を示す文字列です。出力の例を以下に示します。
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 {
これは、Spring Security テストのセットアップ方法の基本的な例です。ハイライトは次のとおりです。
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"、パスワードは "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
の前に発生することと同等です。これは、JUnit の @Before
の後、テストメソッドが呼び出される前の TestExecutionListener.beforeTestExecution
イベント中に発生するように変更できます。
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithAnonymousUser
@WithAnonymousUser
を使用すると、匿名ユーザーとして実行できます。これは、特定のユーザーでほとんどのテストを実行したいが、匿名ユーザーとしていくつかのテストを実行したい場合に特に便利です。例: 以下は、@WithMockUser と anonymous ユーザーとして 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
の前に発生することと同等です。これは、JUnit の @Before
の後、テストメソッドが呼び出される前の TestExecutionListener.beforeTestExecution
イベント中に発生するように変更できます。
@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
の前に発生することと同等です。これは、JUnit の @Before
の後、テストメソッドが呼び出される前の TestExecutionListener.beforeTestExecution
イベント中に発生するように変更できます。
@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
アノテーションが付けられていることがわかります。これは、Spring Security テストサポートに、テスト用に SecurityContext
を作成する予定であることを示すものです。@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
の前に発生することと同等です。これは、JUnit の @Before
の後、テストメソッドが呼び出される前の TestExecutionListener.beforeTestExecution
イベント中に発生するように変更できます。
@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")
のメタアノテーションも作成できることを意味します。