メソッドのセキュリティテスト
このセクションでは、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 は、 |
@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")
のメタアノテーションも作成できることを意味します。