# 测试方法安全性

本节演示如何使用 Spring Security的测试支持来测试基于方法的安全性。我们首先介绍一个`MessageService`，它需要对用户进行身份验证才能访问它。

Java

```
public class HelloMessageService implements MessageService {

	@PreAuthorize("authenticated")
	public String getMessage() {
		Authentication authentication = SecurityContextHolder.getContext()
			.getAuthentication();
		return "Hello " + authentication;
	}
}
```

Kotlin

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

`getMessage`的结果是对当前 Spring 安全性`Authentication`说“你好”的字符串。下面将显示一个输出示例。

```
Hello org.springframew[email protected]ca25360: Principal: [email protected]: 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 安全测试支持之前，我们必须执行一些设置。下面是一个例子：

Java

```
@RunWith(SpringJUnit4ClassRunner.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
```

Kotlin

```
@RunWith(SpringJUnit4ClassRunner::class)
@ContextConfiguration
class WithMockUserTests {
```

这是如何设置 Spring 安全性测试的一个基本示例。重点是：

|**1**|`@RunWith`指示 Spring-test模块应该创建`ApplicationContext`。这与使用现有的 Spring 测试支持没有什么不同。有关更多信息，请参阅[Spring Reference](https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-standard)|
|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|`@ContextConfiguration`指示 Spring-test用于创建`ApplicationContext`的配置。由于未指定配置，因此将尝试默认的配置位置。这与使用现有的 Spring 测试支持没有什么不同。有关更多信息，请参阅[Spring Reference](https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#testcontext-ctx-management)|

|   |Spring 使用`WithSecurityContextTestExecutionListener`将安全挂钩到 Spring 测试支持中，这将确保我们的测试与正确的用户一起运行。<br/>它通过在运行我们的测试之前填充`SecurityContextHolder`来实现这一点。<br/>如果你使用的是反应式方法安全，你还需要填充`ReactorContextTestExecutionListener`的`ReactiveSecurityContextHolder`。<br/>测试完成后，它将清除`SecurityContextHolder`。<br/>如果你只需要 Spring 安全相关的支持，则可以将`@ContextConfiguration`替换为`@SecurityTestExecutionListeners`。|
|---| |

请记住，我们在我们的`@PreAuthorize`中添加了`HelloMessageService`注释，因此它需要经过身份验证的用户来调用它。如果我们运行以下测试，我们预计以下测试将通过：

Java

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

Kotlin

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

## @WithMockuser

问题是“作为一个特定的用户，我们如何才能最容易地运行测试？”答案是使用`@WithMockUser`。下面的测试将以用户名“user”、密码“password”和角色“role\_user”的用户身份运行。

Java

```
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
```

Kotlin

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

具体来说，以下情况是正确的：

* 用户名为“user”的用户不一定存在，因为我们是在嘲笑用户

* 在`SecurityContext`中填充的`Authentication`类型为`UsernamePasswordAuthenticationToken`

* `Authentication`上的主体是 Spring 证券的`User`对象

* `User`将具有“user”的用户名，密码“password”，并使用一个名为“role\_user”的`GrantedAuthority`。

我们的例子很好，因为我们能够利用大量的违约。如果我们想用不同的用户名运行测试，该怎么办？下面的测试将使用用户名“CustomUser”运行。同样，用户不需要实际存在。

Java

```
@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
	String message = messageService.getMessage();
...
}
```

Kotlin

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

我们还可以轻松地定制角色。例如，将使用用户名“admin”以及角色“role\_user”和“role\_admin”来调用此测试。

Java

```
@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
	String message = messageService.getMessage();
	...
}
```

Kotlin

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

如果我们不希望该值自动以role\_为前缀，那么我们可以利用Authorities属性。例如，将使用用户名“admin”以及权威机构“user”和“admin”来调用此测试。

Java

```
@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
	String message = messageService.getMessage();
	...
}
```

Kotlin

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

当然，在每个测试方法上放置注释可能会有点繁琐。相反，我们可以将注释放在类级别，并且每个测试都将使用指定的用户。例如，下面将对用户名为“admin”、密码为“password”、角色为“role\_user”和“role\_admin”的用户进行每次测试。

Java

```
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
```

Kotlin

```
@RunWith(SpringJUnit4ClassRunner::class)
@ContextConfiguration
@WithMockUser(username="admin",roles=["USER","ADMIN"])
class WithMockUserTests {
```

如果你正在使用JUnit5的`@Nested`测试支持，那么你还可以将注释放置在封闭类上，以应用于所有嵌套的类。例如，下面将使用用户名为“admin”、密码为“password”、角色为“role\_user”和“role\_admin”的用户来运行每个测试。

Java

```
@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
	}
}
```

Kotlin

```
@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](#test-method-withmockuser)和匿名者作为匿名用户运行Mockuser1和Mockuser2。

Java

```
@RunWith(SpringJUnit4ClassRunner.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
	}
}
```

Kotlin

```
@RunWith(SpringJUnit4ClassRunner::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 安全性上的耦合。

自定义主体通常是由一个自定义`UserDetailsService`返回的，该自定义主体返回一个对象，该对象实现`UserDetails`和自定义类型。对于这样的情况，使用自定义`UserDetailsService`创建测试用户是有用的。这正是`@WithUserDetails`所做的。

假设我们将`UserDetailsService`作为 Bean 公开，那么下面的测试将使用`Authentication`类型的`UsernamePasswordAuthenticationToken`和从`UserDetailsService`返回的、用户名为“user”的主体来调用。

Java

```
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
	String message = messageService.getMessage();
	...
}
```

Kotlin

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

我们还可以自定义用于从`UserDetailsService`中查找用户的用户名。例如，该测试将使用从`UserDetailsService`返回的主体运行，该主体的用户名为“CustomUsername”。

Java

```
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
	String message = messageService.getMessage();
	...
}
```

Kotlin

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

我们还可以提供一个显式的 Bean 名称来查找`UserDetailsService`。例如，该测试将使用`UserDetailsService`和 Bean 名称“MyUserDetailsService”来查找“CustomUsername”的用户名。

Java

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

Kotlin

```
@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

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

	String username() default "rob";

	String name() default "Rob Winch";
}
```

Kotlin

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

你可以看到`@WithMockCustomUser`是用`@WithSecurityContext`注释的。这是向 Spring 安全性测试支持发出的信号，表示我们打算为测试创建`SecurityContext`。`@WithSecurityContext`注释要求我们指定一个`SecurityContextFactory`，给定我们的`@WithMockCustomUser`注释，它将创建一个新的`SecurityContext`注释。你可以在下面找到我们的`WithMockCustomUserSecurityContextFactory`实现：

Java

```
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 =
			new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities());
		context.setAuthentication(auth);
		return context;
	}
}
```

Kotlin

```
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

```
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 = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
		SecurityContext context = SecurityContextHolder.createEmptyContext();
		context.setAuthentication(authentication);
		return context;
	}
}
```

Kotlin

```
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

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

Kotlin

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

我们可以使用元注释，而不是到处重复这一点。例如，我们可以创建一个名为`WithMockAdmin`的元注释：

Java

```
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }
```

Kotlin

```
@Retention(AnnotationRetention.RUNTIME)
@WithMockUser(value = "rob", roles = ["ADMIN"])
annotation class WithMockAdmin
```

现在我们可以使用`@WithMockAdmin`，就像使用更详细的`@WithMockUser`一样。

元注释可以与上面描述的任何测试注释一起工作。例如，这意味着我们也可以为`@WithUserDetails("admin")`创建一个元注释。

[Testing](index.html)[MockMVC支持](mockmvc/index.html)