此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.3! |
测试方法安全性
本节演示如何使用 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"
}
}
的结果getMessage
是一个 String ,表示当前 Spring Security 的 “Hello”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 Test 支持之前,我们必须执行一些设置。示例如下所示:
-
Java
-
Kotlin
@ExtendWith(SpringExtension.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {
这是如何设置 Spring Security Test 的基本示例。亮点是:
1 | @ExtendWith 指示 spring-test 模块应创建一个ApplicationContext .有关更多信息,请参阅 Spring 参考。 |
2 | @ContextConfiguration 指示 spring-test 用于创建ApplicationContext .由于未指定配置,因此将尝试默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关更多信息,请参阅 Spring 参考 |
Spring Security 使用WithSecurityContextTestExecutionListener 这将确保我们的测试由正确的用户运行。
它通过填充SecurityContextHolder 在运行我们的测试之前。
如果你正在使用响应式方法安全性,你还需要ReactorContextTestExecutionListener 填充ReactiveSecurityContextHolder .
测试完成后,它将清除SecurityContextHolder .
如果只需要 Spring Security 相关支持,可以将@ContextConfiguration 跟@SecurityTestExecutionListeners . |
请记住,我们添加了@PreAuthorize
注解添加到我们的HelloMessageService
因此,它需要经过身份验证的用户来调用它。
如果我们运行以下测试,我们预期以下测试将通过:
-
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()
// ...
}
具体来说,以下内容是正确的:
-
用户名为 “user” 的用户不必存在,因为我们正在模拟用户
-
这
Authentication
填充在SecurityContext
属于 类型UsernamePasswordAuthenticationToken
-
的
Authentication
是 Spring Security 的User
对象 -
这
User
将具有 “user” 的用户名、密码 “password” 和一个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()
// ...
}
如果我们不希望该值自动以 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()
// ...
}
当然,将 annotation 放在每个测试方法上可能有点乏味。 相反,我们可以将 Comments 放在 class 级别,并且每个测试都将使用指定的 user。 例如,以下命令将使用用户名为 “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 的@Nested
test 支持,您还可以将 annotation 放在 Encovering 类上以应用于所有嵌套类。
例如,对于这两种测试方法,以下代码将使用用户名为 “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
}
}
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件。
这相当于发生在 JUnit 的@Before
.
您可以将此更改为在TestExecutionListener.beforeTestExecution
event 的 JUnit 的@Before
但在调用测试方法之前。
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithAnonymousUser
用@WithAnonymousUser
允许以匿名用户身份运行。
当您希望使用特定用户运行大多数测试,但希望以匿名用户身份运行一些测试时,这尤其方便。
例如,以下代码将使用 @WithMockUser 和 anonymous 作为匿名用户运行 withMockUser1 和 withMockUser2。
-
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
}
}
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件。
这相当于发生在 JUnit 的@Before
.
您可以将此更改为在TestExecutionListener.beforeTestExecution
event 的 JUnit 的@Before
但在调用测试方法之前。
@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithUserDetails
而@WithMockUser
是一种非常方便的入门方式,但并非在所有情况下都有效。
例如,应用程序通常期望Authentication
principal 为特定类型。
这样做是为了让应用程序可以将主体引用为自定义类型并减少对 Spring Security 的耦合。
自定义主体通常由自定义返回UserDetailsService
返回一个对象,该对象同时实现UserDetails
和自定义类型。
对于此类情况,使用自定义UserDetailsService
.
这正是@WithUserDetails
确实。
假设我们有一个UserDetailsService
作为 bean 公开,以下测试将使用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 名称来查找UserDetailsService
.
例如,此测试将使用UserDetailsService
替换为 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
我们还可以将 Comments 放在 Class 级别,以便每个测试都使用相同的 User。
然而,与@WithMockUser
,@WithUserDetails
需要用户存在。
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件。
这相当于发生在 JUnit 的@Before
.
您可以将此更改为在TestExecutionListener.beforeTestExecution
event 的 JUnit 的@Before
但在调用测试方法之前。
@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
@WithSecurityContext
我们已经看到了这一点@WithMockUser
如果我们不使用自定义Authentication
主要。
接下来我们发现@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 Test 支持发出的信号,表明我们打算创建一个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
}
}
我们现在可以使用我们的新 Comments 和 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
}
}
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件。
这相当于发生在 JUnit 的@Before
.
您可以将此更改为在TestExecutionListener.beforeTestExecution
event 的 JUnit 的@Before
但在调用测试方法之前。
@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
测试 Meta 注释
如果您经常在测试中重用同一用户,则必须重复指定属性并不理想。
例如,如果有许多测试与用户名 “admin” 和 角色ROLE_USER
和ROLE_ADMIN
你得写:
-
Java
-
Kotlin
@WithMockUser(username="admin",roles={"USER","ADMIN"})
@WithMockUser(username="admin",roles=["USER","ADMIN"])
与其到处重复此作,不如使用 meta 注解。
例如,我们可以创建一个名为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
.
Meta 注解适用于上述任何测试注解。
例如,这意味着我们可以为@WithUserDetails("admin")
也。