测试方法安全(Method Security)

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

本节演示了如何使用Spring Security的测试支持来测试基于方法的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 的结果是一个 String,对当前的 Spring Security Authentication 说 “Hello” 。下面的列表显示了输出示例。

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 测试设置

在我们使用 Spring Security 测试支持之前,我们必须进行一些设置。

  • Java

  • Kotlin

@ExtendWith(SpringExtension.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
	// ...
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {
    // ...
}
1 @ExtendWith 指示 spring-test 模块应该创建一个 ApplicationContext。有关其他信息,请参考 Spring Reference
2 @ContextConfiguration 指示 spring-test 使用何种配置来创建 ApplicationContext。由于没有指定配置,将尝试默认的配置位置。这与使用现有的Spring Test支持没有区别。有关其他信息,请参考 Spring Reference

Spring Security 通过 WithSecurityContextTestExecutionListener 与 Spring Test 支持挂钩,确保我们的测试是以正确的用户运行的。它通过在运行我们的测试之前填充 SecurityContextHolder 来做到这一点。如果你使用响应式 method security,你也需要 ReactorContextTestExecutionListener,它将填充 ReactiveSecurityContextHolder。在测试完成后,它将清除 SecurityContextHolder。如果你只需要 Spring Security 相关的支持,你可以用 @SecurityTestExecutionListeners 替换 @ContextConfiguration

记住,我们给 HelloMessageService 添加了 @PreAuthorize 注解,所以它需要一个认证的用户来调用它。如果我们运行测试,我们希望以下测试能够通过。

  • 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()
    // ...
}

具体来说,以下情况是true。

  • 用户名为 user 的用户不一定存在,因为我们模拟了用户对象。

  • SecurityContext 中被填充的 AuthenticationUsernamePasswordAuthenticationToken 类型的。

  • Authentication 上的 principal 是 Spring Security 的 User 对象。

  • User 有一个用户名是 user

  • User 有一个密码是 password

  • 一个名为 ROLE_USERGrantedAuthority 被使用。

前面的例子很方便,因为它让我们使用了很多的默认值。如果我们想用一个不同的用户名来运行测试呢?下面的测试将以 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_USERROLE_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 的权限来调用的。

  • 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_USERROLE_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 test支持,你也可以把注解放在包围(外面的)类上,以适用于所有嵌套类。下面的例子用一个用户名是 admin,密码是 password,并且在两个测试方法中都有 ROLE_USERROLE_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
    }
}

默认情况下,SecurityContextTestExecutionListener. beforeTestMethod 事件中被设置。这相当于发生在 JUnit 的 @Before 之前。你可以把它改为在 TestExecutionListener.beforeTestExecution 事件中发生,也就是在JUnit的 @Before 之后但在测试方法被调用之前。

@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithAnonymousUser

使用 @WithAnonymousUser 允许以匿名用户身份运行。当你希望用一个特定的用户来运行大部分的测试,但又想以匿名用户的身份运行一些测试时,这就特别方便。下面的例子通过使用 @WithMockUseranonymous 作为匿名用户来运行 withMockUser1withMockUser2

  • 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
    }
}

默认情况下,SecurityContextTestExecutionListener.beforeTestMethod 事件中被设置。这相当于发生在JUnit的 @Before 之前。你可以把它改为在 TestExecutionListener.beforeTestExecution 事件中发生,也就是在 JUnit 的 @Before 之后但在测试方法被调用之前。

@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithUserDetails

虽然 @WithMockUser 是一种方便的入门方式,但它不一定在所有情况下都适用。例如,有些应用程序希望 Authentication principal 是一个特定的类型。这样做的目的是为了让应用程序可以将 principal 称为自定义类型,减少对 Spring Security 的耦合。

自定义 principal 通常由一个自定义的 UserDetailsService 返回,该Service返回一个同时实现了 UserDetails 和自定义类型的对象。对于这样的情况,通过使用自定义 UserDetailsService 来创建测试用户是很有用的。这正是 @WithUserDetails 的作用。

假设我们有一个以Bean形式暴露的 UserDetailsService,下面的测试被调用,其中有一个 UsernamePasswordAuthenticationToken 类型的 Authentication,以及一个从 UserDetailsService 返回的用户名为 user 的 principal。

  • Java

  • Kotlin

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

我们还可以自定义用于从 UserDetailsService 中查找用户的用户名。例如,这个测试可以用一个从 UserDetailsService 返回的用户名 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。下面的测试通过使用 UserDetailsService 来查找 customUsername 的用户名,其bean名称为 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 要求用户存在。

默认情况下,SecurityContextTestExecutionListener.beforeTestMethod 事件中被设置。这相当于发生在JUnit的 @Before 之前。你可以把它改为在 TestExecutionListener.beforeTestExecution 事件中发生,也就是在JUnit的 @Before 之后但在测试方法被调用之前。

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithSecurityContext

我们已经看到,如果我们不使用自定义的 Authentication principal, @WithMockUser 是一个很好的选择。接下来,我们发现 @WithUserDetails 可以让我们使用一个自定义的 UserDetailsService 来创建我们的 Authentication principal,但需要用户存在。我们现在看到了一个允许最灵活的选项。

我们可以创建自己的注解,使用 @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 注解。这是向 Spring Security 测试支持发出的信号,我们打算为测试创建一个 SecurityContext@WithSecurityContext 注解要求我们指定一个 SecurityContextFactory 来创建一个新的 SecurityContext,鉴于我们的 @WithMockCustomUser 注解。下面的列表显示了我们 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
    }
}

默认情况下,SecurityContextTestExecutionListener.beforeTestMethod 事件中被设置。这相当于发生在JUnit的 @Before 之前。你可以把它改为在 TestExecutionListener.beforeTestExecution 事件中发生,也就是在JUnit的 @Before 之后但在测试方法被调用之前。

@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)

测试元注解

如果你经常在你的测试中重复使用同一个用户,那么重复指定属性是不理想的。例如,如果你有许多与管理用户有关的测试,其用户名为 admin,角色为 ROLE_USERROLE_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

现在我们可以用和更多的 @WithMockAdmin 一样的方式来使用 @WithMockUser

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