对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

本节演示如何使用 Spring Security 的 Test 支持来测试基于方法的安全性。 我们首先介绍一个要求用户经过身份验证才能访问它的方法。MessageServiceSpring中文文档

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

结果是一个字符串对当前的 Spring Security 说“Hello”。 下面显示了输出的示例。getMessageAuthenticationSpring中文文档

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

安全测试设置

在使用 Spring Security Test 支持之前,我们必须执行一些设置。下面可以看到一个示例:Spring中文文档

@ExtendWith(SpringExtension.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {

这是如何设置 Spring Security Test 的基本示例。亮点是:Spring中文文档

1 @ExtendWith指示 spring-test 模块应创建一个 .有关其他信息,请参阅 Spring 参考ApplicationContext
2 @ContextConfiguration指示 spring-test 用于创建 .由于未指定配置,因此将尝试默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅 Spring 参考ApplicationContext
Spring Security 使用 Spring Security挂接到Spring Test支持中,这将确保我们的测试与正确的用户一起运行。 它通过在运行测试之前填充 来做到这一点。 如果您使用的是反应式方法安全性,则还需要填充 . 测试完成后,它将清除 . 如果只需要 Spring Security 相关支持,可以替换为 .WithSecurityContextTestExecutionListenerSecurityContextHolderReactorContextTestExecutionListenerReactiveSecurityContextHolderSecurityContextHolder@ContextConfiguration@SecurityTestExecutionListeners

请记住,我们将注解添加到我们的注解中,因此它需要经过身份验证的用户才能调用它。 如果我们运行以下测试,我们希望以下测试将通过:@PreAuthorizeHelloMessageServiceSpring中文文档

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
	messageService.getMessage();
}
@Test(expected = AuthenticationCredentialsNotFoundException::class)
fun getMessageUnauthenticated() {
    messageService.getMessage()
}
1 @ExtendWith指示 spring-test 模块应创建一个 .有关其他信息,请参阅 Spring 参考ApplicationContext
2 @ContextConfiguration指示 spring-test 用于创建 .由于未指定配置,因此将尝试默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅 Spring 参考ApplicationContext
Spring Security 使用 Spring Security挂接到Spring Test支持中,这将确保我们的测试与正确的用户一起运行。 它通过在运行测试之前填充 来做到这一点。 如果您使用的是反应式方法安全性,则还需要填充 . 测试完成后,它将清除 . 如果只需要 Spring Security 相关支持,可以替换为 .WithSecurityContextTestExecutionListenerSecurityContextHolderReactorContextTestExecutionListenerReactiveSecurityContextHolderSecurityContextHolder@ContextConfiguration@SecurityTestExecutionListeners

@WithMockUser

问题是“我们如何才能以特定用户的身份最轻松地运行测试? 答案是使用 . 以下测试将以用户名“user”、密码“password”和角色“ROLE_USER”的用户身份运行。@WithMockUserSpring中文文档

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

具体来说,以下情况是正确的:Spring中文文档

  • 用户名为“user”的用户不必存在,因为我们是在嘲笑用户Spring中文文档

  • 填充在 是 类型AuthenticationSecurityContextUsernamePasswordAuthenticationTokenSpring中文文档

  • 上的主体是 Spring Security 的对象AuthenticationUserSpring中文文档

  • 将具有“user”的用户名,密码“password”,并使用一个名为“ROLE_USER”的单个。UserGrantedAuthoritySpring中文文档

我们的例子很好,因为我们能够利用很多默认值。 如果我们想使用不同的用户名运行测试怎么办? 以下测试将使用用户名“customUser”运行。同样,用户不需要实际存在。Spring中文文档

@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”调用此测试。Spring中文文档

@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”调用此测试。Spring中文文档

@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_USER”和“ROLE_ADMIN”的用户运行每个测试。Spring中文文档

@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 的测试支持,您还可以将注释放在封闭类上以应用于所有嵌套类。 例如,以下内容将使用用户名为“admin”、密码为“password”以及两种测试方法的角色“ROLE_USER”和“ROLE_ADMIN”的用户运行每个测试。@NestedSpring中文文档

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

默认情况下,在事件期间设置。 这相当于在 JUnit 之前发生。 您可以将其更改为在事件发生期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前。SecurityContextTestExecutionListener.beforeTestMethod@BeforeTestExecutionListener.beforeTestExecution@BeforeSpring中文文档

@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithAnonymousUser

使用允许以匿名用户身份运行。 当您希望使用特定用户运行大多数测试,但希望以匿名用户身份运行一些测试时,这尤其方便。 例如,以下内容将使用 @WithMockUser 和 MockUser2 作为匿名用户运行 MockUser1 和 withMockUser2。@WithAnonymousUserSpring中文文档

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

默认情况下,在事件期间设置。 这相当于在 JUnit 之前发生。 您可以将其更改为在事件发生期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前。SecurityContextTestExecutionListener.beforeTestMethod@BeforeTestExecutionListener.beforeTestExecution@BeforeSpring中文文档

@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithUserDetails

虽然这是一种非常方便的入门方式,但它可能并非在所有情况下都有效。 例如,应用程序通常期望主体为特定类型。 这样做是为了让应用程序可以将主体引用为自定义类型,并减少Spring Security上的耦合。@WithMockUserAuthenticationSpring中文文档

自定义主体通常由一个自定义返回,该自定义返回一个实现两者和自定义类型的对象。 对于此类情况,使用自定义 . 这正是所做的。UserDetailsServiceUserDetailsUserDetailsService@WithUserDetailsSpring中文文档

假设我们有一个公开的 bean,则将使用 of 类型和从用户名为“user”返回的主体调用以下测试。UserDetailsServiceAuthenticationUsernamePasswordAuthenticationTokenUserDetailsServiceSpring中文文档

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

我们还可以自定义用于从我们的 . 例如,此测试将使用从用户名为“customUsername”返回的主体运行。UserDetailsServiceUserDetailsServiceSpring中文文档

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

我们还可以提供一个显式的 Bean 名称来查找 . 例如,此测试将使用 Bean 名称“myUserDetailsService”查找“customUsername”的用户名。UserDetailsServiceUserDetailsServiceSpring中文文档

@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@WithUserDetailsSpring中文文档

默认情况下,在事件期间设置。 这相当于在 JUnit 之前发生。 您可以将其更改为在事件发生期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前。SecurityContextTestExecutionListener.beforeTestMethod@BeforeTestExecutionListener.beforeTestExecution@BeforeSpring中文文档

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@WithSecurityContext

我们已经看到,如果我们不使用自定义主体,这是一个很好的选择。 接下来,我们发现这将允许我们使用自定义来创建主体,但需要用户存在。 我们现在将看到一个允许最大灵活性的选项。@WithMockUserAuthentication@WithUserDetailsUserDetailsServiceAuthenticationSpring中文文档

我们可以创建自己的注释,使用 来创建我们想要的任何内容。 例如,我们可以创建一个如下所示的注释:@WithSecurityContextSecurityContext@WithMockCustomUserSpring中文文档

@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")

您可以看到它带有注释的注释。 这是向 Spring Security Test 支持发出的信号,表明我们打算为测试创建一个。 注解要求我们指定一个,它将在给定我们的注解的情况下创建一个新的。 您可以在下面找到我们的实现:@WithMockCustomUser@WithSecurityContextSecurityContext@WithSecurityContextSecurityContextFactorySecurityContext@WithMockCustomUserWithMockCustomUserSecurityContextFactorySpring中文文档

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 将确保我们的注解被正确填充。WithSecurityContextTestExecutionListenerSecurityContextSpring中文文档

在创建自己的实现时,很高兴知道它们可以用标准的 Spring 注解进行注解。 例如,使用注释来获取:WithSecurityContextFactoryWithUserDetailsSecurityContextFactory@AutowiredUserDetailsServiceSpring中文文档

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

默认情况下,在事件期间设置。 这相当于在 JUnit 之前发生。 您可以将其更改为在事件发生期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前。SecurityContextTestExecutionListener.beforeTestMethod@BeforeTestExecutionListener.beforeTestExecution@BeforeSpring中文文档

@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)

测试元注释

如果经常在测试中重用同一用户,则必须重复指定属性并不理想。 例如,如果有许多与用户名为“admin”和角色的管理用户相关的测试,则必须编写:ROLE_USERROLE_ADMINSpring中文文档

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

与其到处重复这一点,不如使用元注释。 例如,我们可以创建一个名为 :WithMockAdminSpring中文文档

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }
@Retention(AnnotationRetention.RUNTIME)
@WithMockUser(value = "rob", roles = ["ADMIN"])
annotation class WithMockAdmin

现在我们可以以与更冗长相同的方式使用 .@WithMockAdmin@WithMockUserSpring中文文档

元注释适用于上述任何测试注释。 例如,这意味着我们也可以为它创建一个元注释。@WithUserDetails("admin")Spring中文文档