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 通过 WithSecurityContextTestExecutionListener 挂接到 Spring 测试支持,这可确保我们的测试使用正确用户运行。它通过在运行我们的测试之前填充 SecurityContextHolder 来执行此操作。如果您使用反应式方法安全性,还需要 ReactorContextTestExecutionListener,它将填充 ReactiveSecurityContextHolder。测试完成后,它会清除 SecurityContextHolder。如果您只需要 Spring Security 相关的支持,则可以使用 @SecurityTestExecutionListeners 替换 @ContextConfiguration

Spring Security hooks into Spring Test support through the WithSecurityContextTestExecutionListener, which ensures that our tests are run with the correct user. It does this by populating the SecurityContextHolder prior to running our tests. If you use reactive method security, you also need ReactorContextTestExecutionListener, which populates ReactiveSecurityContextHolder. After the test is done, it clears out the SecurityContextHolder. If you need only Spring Security related support, you can replace @ContextConfiguration with @SecurityTestExecutionListeners.

请记住,我们已向 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 the SecurityContext is of type UsernamePasswordAuthenticationToken.

  • The principal on the Authentication is Spring Security’s User object.

  • The User has a username of user.

  • The User has a password of password.

  • A single GrantedAuthority named ROLE_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_USERROLE_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 的用户名和 USERADMIN 权限调用。

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_USERROLE_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_USERROLE_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>> 以 withMockUser1withMockUser2 运行,并以匿名用户身份运行 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,那么以下测试将使用类型为 UsernamePasswordAuthenticationTokenAuthentication 和从 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_USERROLE_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.