Testing Method Security
@WithMockUser 注解允许指定一个用户(用户名、密码、角色)来运行测试。它设置了 SecurityContext,提供了简便的方法来模拟特定用户。自定义用户、角色和权限都很容易。可以使用 @WithAnonymousUser 作为匿名用户运行测试。
@WithUserDetails 根据指定的用户名查找 UserDetailsService 返回的主体来创建 SecurityContext。与 @WithMockUser 类似,它还提供了自定义选项。
@WithSecurityContext 允许创建自定义 SecurityContext,以实现最大的灵活性。它需要一个 SecurityContextFactory,它创建了 SecurityContext。
元注解用于避免重复指定用户属性,例如 @WithMockAdmin,它指定了一个具有特定用户名和角色的管理员用户。
此部分演示如何使用 Spring Security 的测试支持来测试基于方法的安全性。我们首先介绍 MessageService
,它要求用户经过身份验证才能访问它:
This section demonstrates how to use Spring Security’s Test support to test method-based security.
We first introduce a MessageService
that requires the user to be authenticated to be able to access it:
-
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
的结果是 String
,它向当前 Spring Security Authentication
中的 “Hello”。以下列表显示了示例输出:
The result of getMessage
is a String
that says “Hello” to the current Spring Security Authentication
.
The following listing shows example output:
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
Security Test Setup
在我们能够使用 Spring Security 测试支持之前,我们必须执行一些设置:
Before we can use the Spring Security test support, we must perform some setup:
-
Java
-
Kotlin
@ExtendWith(SpringExtension.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
// ...
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {
// ...
}
1 | @ExtendWith instructs the spring-test module that it should create an ApplicationContext . For additional information, refer to the {spring-framework-reference-url}testing.html#testcontext-junit-jupiter-extension[Spring reference]. |
2 | @ContextConfiguration instructs the spring-test the configuration to use to create the ApplicationContext . Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the {spring-framework-reference-url}testing.html#spring-testing-annotation-contextconfiguration[Spring Reference]. |
Spring Security 通过 Spring Security hooks into Spring Test support through the |
请记住,我们已向 HelloMessageService
添加了 @PreAuthorize
注解,因此它需要经过身份验证的用户才能调用它。如果我们运行测试,我们希望以下测试通过:
Remember, we added the @PreAuthorize
annotation to our HelloMessageService
, so it requires an authenticated user to invoke it.
If we run the tests, we expect the following test will pass:
-
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”的用户运行。
The question is "How could we most easily run the test as a specific user?"
The answer is to use @WithMockUser
.
The following test will be run as a user with the username "user", the password "password", and the roles "ROLE_USER".
-
Java
-
Kotlin
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
@Test
@WithMockUser
fun getMessageWithMockUser() {
val message: String = messageService.getMessage()
// ...
}
具体来说,以下内容是正确的:
Specifically the following is true:
-
The user with a username of
user
does not have to exist, since we mock the user object. -
The
Authentication
that is populated in theSecurityContext
is of typeUsernamePasswordAuthenticationToken
. -
The principal on the
Authentication
is Spring Security’sUser
object. -
The
User
has a username ofuser
. -
The
User
has a password ofpassword
. -
A single
GrantedAuthority
namedROLE_USER
is used.
前面的示例很方便,因为它让我们可以使用许多默认值。如果我们想使用不同的用户名运行测试怎么办?以下测试将使用 customUser
的用户名运行(同样,该用户实际无需存在):
The preceding example is handy, because it lets us use a lot of defaults.
What if we wanted to run the test with a different username?
The following test would run with a username of customUser
(again, the user does not need to actually exist):
-
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
的角色调用。
We can also easily customize the roles.
For example, the following test is invoked with a username of admin
and roles of ROLE_USER
and 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
权限调用。
If we do not want the value to automatically be prefixed with ROLE_
we can use the authorities
attribute.
For example, the following test is invoked with a username of admin
and the USER
and ADMIN
authorities.
-
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()
// ...
}
这种注解方法总是在每个测试方法中进行,可能有点儿复杂。而我们可以在类层级里设置这个注解。然后每个测试都可以使用指定的 user。这个例子展示了每个测试都有一个 user,用户名为 admin
,密码为 password
,并且有 ROLE_USER
和 ROLE_ADMIN
角色:
It can be a bit tedious to place the annotation on every test method.
Instead, we can place the annotation at the class level. Then every test uses the specified user.
The following example runs every test with a user whose username is admin
, whose password is password
, and who has the ROLE_USER
and ROLE_ADMIN
roles:
-
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
测试支持,你也可以在嵌套类里设置注解,用在所有嵌套类里。下面的例子展示了每个测试都有一个 user,用户名为 admin
,密码为 password
,并且在两个测试方法里具有 ROLE_USER
和 ROLE_ADMIN
角色。
If you use JUnit 5’s @Nested
test support, you can also place the annotation on the enclosing class to apply to all nested classes.
The following example runs every test with a user whose username is admin
, whose password is password
, and who has the ROLE_USER
and ROLE_ADMIN
roles for both test methods.
-
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
之后,但在测试方法执行之前:
By default, the SecurityContext
is set during the TestExecutionListener.beforeTestMethod
event.
This is the equivalent of happening before JUnit’s @Before
.
You can change this to happen during the TestExecutionListener.beforeTestExecution
event, which is after JUnit’s @Before
but before the test method is invoked:
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithAnonymousUser
使用 @WithAnonymousUser
允许以匿名用户运行。当你想使用特定用户运行大多数测试,但还想以匿名用户运行一些测试时,这会很方便。以下示例使用 <<@WithMockUser,test-method-withmockuser>> 以 withMockUser1
和 withMockUser2
运行,并以匿名用户身份运行 anonymous
:
Using @WithAnonymousUser
allows running as an anonymous user.
This is especially convenient when you wish to run most of your tests with a specific user but want to run a few tests as an anonymous user.
The following example runs withMockUser1
and withMockUser2
by using <<@WithMockUser,test-method-withmockuser>> and anonymous
as an anonymous user:
-
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
之后,但在测试方法执行之前:
By default, the SecurityContext
is set during the TestExecutionListener.beforeTestMethod
event.
This is the equivalent of happening before JUnit’s @Before
.
You can change this to happen during the TestExecutionListener.beforeTestExecution
event, which is after JUnit’s @Before
but before the test method is invoked:
@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithUserDetails
虽然 @WithMockUser
是一个简单的入门方法,但它可能并不适用于所有实例。例如,有些应用程序希望 Authentication
主体是特定类型。这样应用程序就可以将主体称为自定义类型,并减少与 Spring Security 的耦合。
While @WithMockUser
is a convenient way to get started, it may not work in all instances.
For example, some applications expect the Authentication
principal to be of a specific type.
This is done so that the application can refer to the principal as the custom type and reduce coupling on Spring Security.
自定义主体通常由自定义 UserDetailsService
返回,该 UserDetailsService
返回同时实现了 UserDetails
和自定义类型的一个对象。在这种情况里,使用自定义 UserDetailsService
创建测试用户会很有用。这正是 @WithUserDetails
正在做的事情。
The custom principal is often returned by a custom UserDetailsService
that returns an object that implements both UserDetails
and the custom type.
For situations like this, it is useful to create the test user by using a custom UserDetailsService
.
That is exactly what @WithUserDetails
does.
假设我们有一个公开为 bean 的 UserDetailsService
,那么以下测试将使用类型为 UsernamePasswordAuthenticationToken
的 Authentication
和从 UserDetailsService
返回的主体(用户名为 user
)调用:
Assuming we have a UserDetailsService
exposed as a bean, the following test is invoked with an Authentication
of type UsernamePasswordAuthenticationToken
and a principal that is returned from the UserDetailsService
with the username of user
:
-
Java
-
Kotlin
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
String message = messageService.getMessage();
...
}
@Test
@WithUserDetails
fun getMessageWithUserDetails() {
val message: String = messageService.getMessage()
// ...
}
我们还可以自定义从 UserDetailsService
里查找用户的用户名。例如,可以用从 UserDetailsService
返回的主体(用户名为 customUsername
)来运行这个测试:
We can also customize the username used to lookup the user from our UserDetailsService
.
For example, this test can be run with a principal that is returned from the UserDetailsService
with the username of customUsername
:
-
Java
-
Kotlin
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
@Test
@WithUserDetails("customUsername")
fun getMessageWithUserDetailsCustomUsername() {
val message: String = messageService.getMessage()
// ...
}
我们还可以提供明确的 bean 名称来查找 UserDetailsService
。以下测试使用 bean 名称 myUserDetailsService
通过 UserDetailsService
来查找 customUsername
的用户名:
We can also provide an explicit bean name to look up the UserDetailsService
.
The following test looks up the username of customUsername
by using the UserDetailsService
with a bean name of myUserDetailsService
:
-
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
需要用户存在。
As we did with @WithMockUser
, we can also place our annotation at the class level so that every test uses the same user.
However, unlike @WithMockUser
, @WithUserDetails
requires the user to exist.
默认情况下,SecurityContext
将在 TestExecutionListener.beforeTestMethod
事件中设置。这相当于发生在 JUnit 的 @Before
之前。你可以在 TestExecutionListener.beforeTestExecution
事件中改变这件事,也就是在 JUnit 的 @Before
之后,但在测试方法执行之前:
By default, the SecurityContext
is set during the TestExecutionListener.beforeTestMethod
event.
This is the equivalent of happening before JUnit’s @Before
.
You can change this to happen during the TestExecutionListener.beforeTestExecution
event, which is after JUnit’s @Before
but before the test method is invoked:
@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithSecurityContext
我们已经看到了,如果我们不使用自定义 Authentication
主体,@WithMockUser
是一个绝佳的选择。然后,我们发现 @WithUserDetails
让我们可以使用自定义 UserDetailsService
来创建 Authentication
主体,但需要用户存在。现在我们看到了一个允许最高灵活性的选项。
We have seen that @WithMockUser
is an excellent choice if we do not use a custom Authentication
principal.
Next, we discovered that @WithUserDetails
lets us use a custom UserDetailsService
to create our Authentication
principal but requires the user to exist.
We now see an option that allows the most flexibility.
我们可以创建自己的注解,它使用 @WithSecurityContext
来创建我们想要的任何 SecurityContext
。例如,我们可以创建一个名为 @WithMockCustomUser
的注解:
We can create our own annotation that uses the @WithSecurityContext
to create any SecurityContext
we want.
For example, we might create an annotation named @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
注解要求我们指定一个 SecurityContextFactory
,以根据我们的 @WithMockCustomUser
注解创建一个新的 SecurityContext
。以下列表显示了我们的 WithMockCustomUserSecurityContextFactory
实现:
You can see that @WithMockCustomUser
is annotated with the @WithSecurityContext
annotation.
This is what signals to Spring Security test support that we intend to create a SecurityContext
for the test.
The @WithSecurityContext
annotation requires that we specify a SecurityContextFactory
to create a new SecurityContext
, given our @WithMockCustomUser
annotation.
The following listing shows our WithMockCustomUserSecurityContextFactory
implementation:
-
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
得到适当填充。
We can now annotate a test class or a test method with our new annotation and Spring Security’s WithSecurityContextTestExecutionListener
to ensure that our SecurityContext
is populated appropriately.
创建自己的 WithSecurityContextFactory
实现时,最好知道它们可以用标准的 Spring 注解进行注解。例如,WithUserDetailsSecurityContextFactory
使用 @Autowired
注解来获取 UserDetailsService
:
When creating your own WithSecurityContextFactory
implementations, it is nice to know that they can be annotated with standard Spring annotations.
For example, the WithUserDetailsSecurityContextFactory
uses the @Autowired
annotation to acquire the 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
之后,但在测试方法执行之前:
By default, the SecurityContext
is set during the TestExecutionListener.beforeTestMethod
event.
This is the equivalent of happening before JUnit’s @Before
.
You can change this to happen during the TestExecutionListener.beforeTestExecution
event, which is after JUnit’s @Before
but before the test method is invoked:
@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
Test Meta Annotations
如果你常常在测试中重复使用同一个用户,那么反复指定属性并不是好的主意。例如,如果你有很多与一个具有 admin
用户名和 ROLE_USER
和 ROLE_ADMIN
角色的管理用户相关的测试,则必须编写:
If you reuse the same user within your tests often, it is not ideal to have to repeatedly specify the attributes.
For example, if you have many tests related to an administrative user with a username of admin
and roles of ROLE_USER
and ROLE_ADMIN
, you have to write:
-
Java
-
Kotlin
@WithMockUser(username="admin",roles={"USER","ADMIN"})
@WithMockUser(username="admin",roles=["USER","ADMIN"])
与其在所有地方重复这个过程,不如使用元注解。例如,我们可以创建一个名为 WithMockAdmin
的元注解:
Rather than repeating this everywhere, we can use a meta annotation.
For example, we could create a meta annotation named 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
。
Now we can use @WithMockAdmin
in the same way as the more verbose @WithMockUser
.
元注解可用于上面描述的任何测试注解。例如,这意味着我们也可以为 @WithUserDetails("admin")
创建一个元注解。
Meta annotations work with any of the testing annotations described above.
For example, this means we could create a meta annotation for @WithUserDetails("admin")
as well.