# 测试方法安全性

本节演示如何使用 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 (opens new window)
2 @ContextConfiguration指示 Spring-test用于创建ApplicationContext的配置。由于未指定配置,因此将尝试默认的配置位置。这与使用现有的 Spring 测试支持没有什么不同。有关更多信息,请参阅Spring Reference (opens new window)
Spring 使用WithSecurityContextTestExecutionListener将安全挂钩到 Spring 测试支持中,这将确保我们的测试与正确的用户一起运行。
它通过在运行我们的测试之前填充SecurityContextHolder来实现这一点。
如果你使用的是反应式方法安全,你还需要填充ReactorContextTestExecutionListenerReactiveSecurityContextHolder
测试完成后,它将清除SecurityContextHolder
如果你只需要 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和匿名者作为匿名用户运行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_USERROLE_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")创建一个元注释。

TestingMockMVC支持