本节演示如何使用 Spring Security 的 Test 支持来测试基于方法的安全性。
我们首先介绍一个要求用户经过身份验证才能访问它的方法: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"
}
}
的结果是一个对当前 Spring Security 说“Hello”的 。
以下列表显示了示例输出:getMessage
String
Authentication
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 测试支持之前,我们必须执行一些设置:
-
Java
-
Kotlin
@ExtendWith(SpringExtension.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
// ...
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {
// ...
}
1 | @ExtendWith 指示 spring-test 模块应创建一个 .有关其他信息,请参阅 Spring 参考。ApplicationContext |
2 | @ContextConfiguration 指示 spring-test 用于创建 .由于未指定配置,因此将尝试默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅 Spring 参考。ApplicationContext |
Spring Security 通过 挂接到 Spring Test 支持,这确保了我们的测试与正确的用户一起运行。
它通过在运行测试之前填充 来做到这一点。
如果使用反应式方法安全性,则还需要 ,它填充 。
测试完成后,它会清除 .
如果只需要 Spring Security 相关支持,可以替换为 . |
请记住,我们将注解添加到了 ,因此它需要经过身份验证的用户来调用它。
如果我们运行测试,我们预计以下测试将通过:@PreAuthorize
HelloMessageService
-
Java
-
Kotlin
@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 Test 支持,这确保了我们的测试与正确的用户一起运行。
它通过在运行测试之前填充 来做到这一点。
如果使用反应式方法安全性,则还需要 ,它填充 。
测试完成后,它会清除 .
如果只需要 Spring Security 相关支持,可以替换为 . |
@WithMockUser
问题是“我们如何才能以特定用户的身份最轻松地运行测试?
答案是使用 .
以下测试将以用户名“user”、密码“password”和角色“ROLE_USER”的用户身份运行。@WithMockUser
-
Java
-
Kotlin
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
@Test
@WithMockUser
fun getMessageWithMockUser() {
val message: String = messageService.getMessage()
// ...
}
具体来说,以下情况是正确的:
-
用户名为 的用户不必存在,因为我们模拟用户对象。
user
-
填充在 的 是 类型 。
Authentication
SecurityContext
UsernamePasswordAuthenticationToken
-
上的主体是 Spring Security 的对象。
Authentication
User
-
的用户名为 。
User
user
-
的密码为 。
User
password
-
使用单个 name。
GrantedAuthority
ROLE_USER
前面的例子很方便,因为它允许我们使用很多默认值。
如果我们想使用不同的用户名运行测试怎么办?
以下测试将使用用户名运行(同样,用户不需要实际存在):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_USER
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()
// ...
}
如果我们不希望该值自动添加前缀,则可以使用该属性。
例如,使用用户名 和 和 authorities 调用以下测试。ROLE_
authorities
admin
USER
ADMIN
-
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_USER
ROLE_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 的测试支持,还可以将注释放在封闭类上以应用于所有嵌套类。
以下示例使用用户名为 、密码为 、具有两种测试方法的 and 角色的用户运行每个测试。@Nested
admin
password
ROLE_USER
ROLE_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
}
}
默认情况下,在事件期间设置。
这相当于在 JUnit 之前发生。
您可以将其更改为在事件期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前:SecurityContext
TestExecutionListener.beforeTestMethod
@Before
TestExecutionListener.beforeTestExecution
@Before
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithAnonymousUser
使用允许以匿名用户身份运行。
当您希望使用特定用户运行大多数测试,但希望以匿名用户身份运行一些测试时,这尤其方便。
以下示例通过使用 @WithMockUser 和 匿名用户:@WithAnonymousUser
withMockUser1
withMockUser2
anonymous
-
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
}
}
默认情况下,在事件期间设置。
这相当于在 JUnit 之前发生。
您可以将其更改为在事件期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前:SecurityContext
TestExecutionListener.beforeTestMethod
@Before
TestExecutionListener.beforeTestExecution
@Before
@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithUserDetails
虽然这是一种方便的入门方式,但它可能并非在所有情况下都有效。
例如,某些应用程序要求主体具有特定类型。
这样做是为了让应用程序可以将主体引用为自定义类型,并减少Spring Security上的耦合。@WithMockUser
Authentication
自定义主体通常由一个自定义返回,该自定义返回一个实现两者和自定义类型的对象。
对于此类情况,使用自定义 .
这正是所做的。UserDetailsService
UserDetails
UserDetailsService
@WithUserDetails
假设我们有一个公开的 bean,则使用 of 类型和从用户名为 :UserDetailsService
Authentication
UsernamePasswordAuthenticationToken
UserDetailsService
user
-
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 名称来查找 .
以下测试使用 Bean 名称为 :UserDetailsService
customUsername
UserDetailsService
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
默认情况下,在事件期间设置。
这相当于在 JUnit 之前发生。
您可以将其更改为在事件期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前:SecurityContext
TestExecutionListener.beforeTestMethod
@Before
TestExecutionListener.beforeTestExecution
@Before
@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithSecurityContext
我们已经看到,如果我们不使用自定义主体,这是一个很好的选择。
接下来,我们发现它允许我们使用自定义来创建主体,但需要用户存在。
我们现在看到了一个允许最大灵活性的选项。@WithMockUser
Authentication
@WithUserDetails
UserDetailsService
Authentication
我们可以创建自己的注释,使用 来创建我们想要的任何内容。
例如,我们可以创建一个名为 :@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")
您可以看到它带有注释的注释。
这是向 Spring Security 测试支持发出的信号,表明我们打算为测试创建一个。
注解要求我们指定 a 来创建一个新的 ,给定我们的注解。
以下列表显示了我们的实现:@WithMockCustomUser
@WithSecurityContext
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
在创建自己的实现时,很高兴知道它们可以用标准的 Spring 注解进行注解。
例如,使用注释来获取:WithSecurityContextFactory
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
}
}
默认情况下,在事件期间设置。
这相当于在 JUnit 之前发生。
您可以将其更改为在事件期间发生,该事件发生在 JUnit 之后,但在调用测试方法之前:SecurityContext
TestExecutionListener.beforeTestMethod
@Before
TestExecutionListener.beforeTestExecution
@Before
@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
测试元注释
如果经常在测试中重用同一用户,则必须重复指定属性并不理想。
例如,如果有许多与用户名为 和 且角色为 and 的管理用户相关的测试,则必须编写:admin
ROLE_USER
ROLE_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")