On this page
9. Testing
本节描述了 Spring Security 提供的测试支持。
Tip
要使用 Spring Security 测试支持,您必须包括spring-security-test-5.1.2.RELEASE.jar
作为项目的依赖项。
9.1 测试方法的安全性
本节演示了如何使用 Spring Security 的 Test 支持来测试基于方法的安全性。首先,我们引入一个MessageService
,要求用户经过身份验证才能访问它。
public class HelloMessageService implements MessageService {
@PreAuthorize("authenticated")
public String getMessage() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
return "Hello " + authentication;
}
}
getMessage
的结果是一个字符串,向当前的 Spring Security Authentication
说“ Hello”。输出示例如下所示。
Hello org.springframew[emailprotected]ca25360: Principal: [emailprotected]: 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
9.1.1 安全测试设置
在使用 Spring Security Test 支持之前,我们必须执行一些设置。下面是一个示例:
@RunWith(SpringJUnit4ClassRunner.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
这是如何设置 Spring Security Test 的基本示例。重点是:
- (1)
@RunWith
指示 Spring 测试模块应创建ApplicationContext
。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅Spring Reference - (2)
@ContextConfiguration
指示对配置进行 Spring 测试以创建ApplicationContext
。由于未指定任何配置,因此将尝试使用默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅Spring Reference
Note
Spring Security 使用WithSecurityContextTestExecutionListener
连接到 Spring Test 支持,这将确保我们的测试以正确的用户运行。它是通过在运行测试之前填充SecurityContextHolder
来实现的。如果您使用反应式方法安全性,则还需要ReactorContextTestExecutionListener
填充ReactiveSecurityContextHolder
。测试完成后,它将清除SecurityContextHolder
。如果只需要与 Spring Security 相关的支持,则可以将@ContextConfiguration
替换为@SecurityTestExecutionListeners
。
请记住,我们在HelloMessageService
上添加了@PreAuthorize
注解,因此需要经过身份验证的用户才能调用它。如果我们运行以下测试,我们希望以下测试将通过:
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
messageService.getMessage();
}
9.1.2 @WithMockUser
问题是“我们如何最轻松地以特定用户身份运行测试?”答案是使用@WithMockUser
。以下测试将以用户名“ user”,密码“ password”和角色“ ROLE_USER”的用户身份运行。
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
具体来说,以下是正确的:
用户名“ user”的用户不必存在,因为我们在模拟用户
SecurityContext
中填充的Authentication
的类型为UsernamePasswordAuthenticationToken
Authentication
上的主体是 Spring Security 的User
对象User
的用户名为“ user”,密码为“ password”,并且使用一个单独的GrantedAuthority
,名称为“ ROLE_USER”。
我们的示例很好,因为我们能够利用很多默认值。如果我们想使用其他用户名运行测试该怎么办?以下测试将使用用户名“ customUser”运行。同样,用户不需要实际存在。
@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
String message = messageService.getMessage();
...
}
我们还可以轻松自定义角色。例如,将使用用户名“ admin”以及角色“ ROLE_USER”和“ ROLE_ADMIN”调用此测试。
@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
String message = messageService.getMessage();
...
}
如果我们不希望该值自动以 ROLE 作为前缀,则可以利用 Authority 属性。例如,将使用用户名“ admin”以及权限“ USER”和“ ADMIN”来调用此测试。
@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
String message = messageService.getMessage();
...
}
当然,将 Comments 放在每种测试方法上可能会有些乏味。相反,我们可以将 Comments 放置在类级别,并且每个测试都将使用指定的用户。例如,下面的代码将使用用户名“ admin”,密码“ password”以及角色“ ROLE_USER”和“ ROLE_ADMIN”的用户运行每个测试。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
默认情况下,在TestExecutionListener.beforeTestMethod
事件期间设置SecurityContext
。这等效于在 JUnit 的@Before
之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution
事件期间发生,该事件在 JUnit 的@Before
之后但在调用测试方法之前。
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
9.1.3 @WithAnonymousUser
使用@WithAnonymousUser
允许以匿名用户身份运行。当您希望与特定用户一起运行大多数测试,但又希望以匿名用户身份运行一些测试时,这特别方便。例如,以下将使用@WithMockUser并以匿名用户身份匿名运行 withMockUser1 和 withMockUser2.
@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
}
}
默认情况下,在TestExecutionListener.beforeTestMethod
事件期间设置SecurityContext
。这等效于在 JUnit 的@Before
之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution
事件期间发生,该事件在 JUnit 的@Before
之后但在调用测试方法之前。
@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
9.1.4 @WithUserDetails
@WithMockUser
是一种非常方便的入门方法,但不一定在所有情况下都起作用。例如,应用程序通常期望Authentication
主体是特定类型。这样做是为了使应用程序可以将委托人称为自定义类型,并减少 Spring Security 上的耦合。
自定义主体通常由自定义UserDetailsService
返回,自定义UserDetailsService
返回实现UserDetails
和自定义类型的对象。对于这种情况,使用自定义UserDetailsService
创建测试用户非常有用。正是@WithUserDetails
所做的。
假设我们有一个UserDetailsService
作为 bean 公开,下面的测试将使用类型UsernamePasswordAuthenticationToken
的Authentication
和从UserDetailsService
返回的用户名“ user”的主体来调用。
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
String message = messageService.getMessage();
...
}
我们还可以自定义用于从UserDetailsService
查找用户的用户名。例如,将使用从用户名为“ customUsername”的UserDetailsService
返回的委托人来执行此测试。
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
我们还可以提供一个明确的 bean 名称来查找UserDetailsService
。例如,此测试将使用 Bean 名称为“ myUserDetailsService”的UserDetailsService
查找“ customUsername”的用户名。
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
String message = messageService.getMessage();
...
}
像@WithMockUser
一样,我们也可以将 Comments 放在类级别,以便每个测试使用同一用户。但是,与@WithMockUser
不同,@WithUserDetails
要求用户存在。
默认情况下,在TestExecutionListener.beforeTestMethod
事件期间设置SecurityContext
。这等效于在 JUnit 的@Before
之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution
事件期间发生,该事件在 JUnit 的@Before
之后但在调用测试方法之前。
@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
9.1.5 @WithSecurityContext
我们已经看到,如果不使用自定义Authentication
主体,则@WithMockUser
是一个很好的选择。接下来,我们发现@WithUserDetails
允许我们使用自定义UserDetailsService
创建我们的Authentication
主体,但是要求用户存在。现在,我们将看到一个具有最大灵 Active 的选项。
我们可以创建自己的 Comments,该 Comments 使用@WithSecurityContext
创建所需的任何SecurityContext
。例如,我们可以创建一个名为@WithMockCustomUser
的 Comments,如下所示:
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username() default "rob";
String name() default "Rob Winch";
}
您可以看到@WithMockCustomUser
带有@WithSecurityContext
Comments。这就是向 Spring Security Test 支持发出 signal 的 signal,我们打算为该测试创建一个SecurityContext
。 @WithSecurityContext
注解要求我们指定SecurityContextFactory
,从而根据@WithMockCustomUser
注解创建一个新的SecurityContext
。您可以在下面找到我们的WithMockCustomUserSecurityContextFactory
实现:
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;
}
}
现在,我们可以使用新的 Comments 对测试类或测试方法进行 Comments,Spring Security 的WithSecurityContextTestExecutionListener
将确保正确填充SecurityContext
。
创建自己的WithSecurityContextFactory
实现时,很高兴知道可以使用标准的 SpringComments 对其进行 Comments。例如,WithUserDetailsSecurityContextFactory
使用@Autowired
Comments 获取UserDetailsService
:
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;
}
}
默认情况下,在TestExecutionListener.beforeTestMethod
事件期间设置SecurityContext
。这等效于在 JUnit 的@Before
之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution
事件期间发生,该事件在 JUnit 的@Before
之后但在调用测试方法之前。
@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
9.1.6 测试元 Comments
如果您经常在测试中重用同一用户,则不理想的是必须重复指定属性。例如,如果有许多与用户名为“ admin”且角色为ROLE_USER
和ROLE_ADMIN
的 Management 用户相关的测试,则您必须编写:
@WithMockUser(username="admin",roles={"USER","ADMIN"})
我们可以使用元 Comments,而不是在所有地方重复此操作。例如,我们可以创建一个名为WithMockAdmin
的元 Comments:
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }
现在,我们可以像更详细的@WithMockUser
一样使用@WithMockAdmin
。
元 Comments 可与上述任何测试 Comments 一起使用。例如,这意味着我们也可以为@WithUserDetails("admin")
创建一个元 Comments。
9.2 Spring MVC 测试集成
Spring Security 提供与SpringMVC 测试的全面集成
9.2.1 设置 MockMvc 和 Spring Security
为了将 Spring Security 与 Spring MVC Test 一起使用,有必要将 Spring Security FilterChainProxy
添加为Filter
。还必须添加 Spring Security 的TestSecurityContextHolderPostProcessor
以支持在带有 Comments 的 Spring MVC 测试中以用户身份运行。这可以使用 Spring Security 的SecurityMockMvcConfigurers.springSecurity()
完成。例如:
Note
Spring Security 的测试支持需要 spring-test-4.1.3.RELEASE 或更高版本。
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class CsrfShowcaseTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity()) (1)
.build();
}
...
- (1)
SecurityMockMvcConfigurers.springSecurity()
将执行将 Spring Security 与 Spring MVC Test 集成在一起所需的所有初始设置
9.2.2 SecurityMockMvcRequestPostProcessors
Spring MVC Test 提供了一个方便的接口,称为RequestPostProcessor
,可用于修改请求。 Spring Security 提供了许多RequestPostProcessor
实现,这些实现使测试更加容易。为了使用 Spring Security 的RequestPostProcessor
实现,请确保使用以下静态导入:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
使用 CSRF 保护进行测试
当测试任何非安全的 HTTP 方法并使用 Spring Security 的 CSRF 保护时,必须确保在请求中包括有效的 CSRF 令牌。使用以下命令将有效的 CSRF 令牌指定为请求参数:
mvc
.perform(post("/").with(csrf()))
如果愿意,可以在标题中包含 CSRF 令牌:
mvc
.perform(post("/").with(csrf().asHeader()))
您还可以使用以下方法测试提供的 CSRF 令牌无效:
mvc
.perform(post("/").with(csrf().useInvalidToken()))
在 Spring MVC 测试中以用户身份运行测试
通常需要以特定用户身份运行测试。填充用户有两种简单的方法:
使用 RequestPostProcessor 在 Spring MVC 测试中以用户身份运行
有许多选项可用于将用户与当前HttpServletRequest
关联。例如,以下将以用户名(用户),密码“ password”和角色“ ROLE_USER”的用户身份(不需要存在)运行:
Note
该支持通过将用户与HttpServletRequest
关联而起作用。要将请求与SecurityContextHolder
关联,您需要确保SecurityContextPersistenceFilter
与MockMvc
实例关联。实现此目的的几种方法是:
Invoking apply(springSecurity())
将 Spring Security 的
FilterChainProxy
添加到MockMvc
使用
MockMvcBuilders.standaloneSetup
时,手动将SecurityContextPersistenceFilter
添加到MockMvc
实例可能很有意义
mvc
.perform(get("/").with(user("user")))
您可以轻松进行自定义。例如,以下用户名(Management 员),用户名“ admin”,密码“ pass”以及角色“ ROLE_USER”和“ ROLE_ADMIN”将作为用户(不需要存在)运行。
mvc
.perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))
如果您有要使用的自定义UserDetails
,也可以轻松指定。例如,以下代码将使用指定的UserDetails
(不需要存在)与UsernamePasswordAuthenticationToken
一起运行,该UsernamePasswordAuthenticationToken
的主体为指定的UserDetails
:
mvc
.perform(get("/").with(user(userDetails)))
您可以使用以下身份以匿名用户身份运行:
mvc
.perform(get("/").with(anonymous()))
如果您使用默认用户运行,并希望以匿名用户身份执行一些请求,则此功能特别有用。
如果您想要一个自定义的Authentication
(不需要存在),可以使用以下方法:
mvc
.perform(get("/").with(authentication(authentication)))
您甚至可以使用以下方法自定义SecurityContext
:
mvc
.perform(get("/").with(securityContext(securityContext)))
通过使用MockMvcBuilders
的默认请求,我们还可以确保针对每个请求以特定用户身份运行。例如,以下用户名(Management 员),用户名“ admin”,密码“ password”和角色“ ROLE_ADMIN”将作为用户(不需要存在)运行:
mvc = MockMvcBuilders
.webAppContextSetup(context)
.defaultRequest(get("/").with(user("user").roles("ADMIN")))
.apply(springSecurity())
.build();
如果发现您在许多测试中使用的是同一用户,建议将用户移至某个方法。例如,您可以在自己的名为CustomSecurityMockMvcRequestPostProcessors
的类中指定以下内容:
public static RequestPostProcessor rob() {
return user("rob").roles("ADMIN");
}
现在,您可以对SecurityMockMvcRequestPostProcessors
执行静态导入,并在测试中使用它:
import static sample.CustomSecurityMockMvcRequestPostProcessors.*;
...
mvc
.perform(get("/").with(rob()))
在带有 Comments 的 Spring MVC 测试中以用户身份运行
作为使用RequestPostProcessor
创建用户的替代方法,您可以使用第 9.1 节“测试方法安全性”中描述的 Comments。例如,以下将对具有用户名“ user”,密码“ password”和角色“ ROLE_USER”的用户运行测试:
@Test
@WithMockUser
public void requestProtectedUrlWithUser() throws Exception {
mvc
.perform(get("/"))
...
}
或者,以下将使用用户名“ user”,密码“ password”和角色“ ROLE_ADMIN”的用户运行测试:
@Test
@WithMockUser(roles="ADMIN")
public void requestProtectedUrlWithUser() throws Exception {
mvc
.perform(get("/"))
...
}
测试 HTTP 基本身份验证
虽然始终可以使用 HTTP Basic 进行身份验证,但是记住 Headers 名称,格式和对值进行编码有点繁琐。现在,可以使用 Spring Security 的httpBasic
RequestPostProcessor
完成此操作。例如,以下代码段:
mvc
.perform(get("/").with(httpBasic("user","password")))
将通过确保在 HTTP 请求中填充以下 Headers,尝试使用 HTTP Basic 对用户名“ user”和密码“ password”进行身份验证:
Authorization: Basic dXNlcjpwYXNzd29yZA==
9.2.3 SecurityMockMvcRequestBuilders
Spring MVC Test 还提供了一个RequestBuilder
接口,可用于创建测试中使用的MockHttpServletRequest
。 Spring Security 提供了RequestBuilder
的一些实现,可用来简化测试。为了使用 Spring Security 的RequestBuilder
实现,请确保使用以下静态导入:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
测试基于表单的身份验证
您可以使用 Spring Security 的测试支持轻松地创建一个请求,以测试基于表单的身份验证。例如,以下代码将使用用户名“ user”,密码“ password”和有效的 CSRF 令牌向“/login”提交 POST:
mvc
.perform(formLogin())
定制请求很容易。例如,以下内容将使用用户名“ admin”,密码“ pass”和有效的 CSRF 令牌向“/auth”提交 POST:
mvc
.perform(formLogin("/auth").user("admin").password("pass"))
我们还可以自定义包含用户名和密码的参数名称。例如,这是上述请求,已修改为包括 HTTP 参数“ u”上的用户名和 HTTP 参数“ p”上的密码。
mvc
.perform(formLogin("/auth").user("u","admin").password("p","pass"))
Testing Logout
使用标准 Spring MVC Test 相当简单,但是您可以使用 Spring Security 的测试支持来简化测试注销。例如,以下代码将使用有效的 CSRF 令牌向“/logout”提交 POST:
mvc
.perform(logout())
您还可以自定义要发布到的 URL。例如,下面的代码片段将使用有效的 CSRF 令牌向“/signout”提交 POST:
mvc
.perform(logout("/signout"))
9.2.4 SecurityMockMvcResultMatchers
有时希望对请求做出各种与安全性有关的 assert。为了满足这一需求,Spring Security Test 支持实现了 Spring MVC Test 的ResultMatcher
接口。为了使用 Spring Security 的ResultMatcher
实现,请确保使用以下静态导入:
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
Unauthenticated Assertion
有时 assert 没有与MockMvc
调用的结果相关联的经过身份验证的用户可能很有价值。例如,您可能要测试提交的用户名和密码无效,并验证没有用户通过身份验证。您可以使用 Spring Security 的测试支持轻松地执行以下操作:
mvc
.perform(formLogin().password("invalid"))
.andExpect(unauthenticated());
Authenticated Assertion
通常,我们必须 assert 已通过身份验证的用户存在。例如,我们可能要验证我们已成功验证。我们可以使用以下代码片段来验证基于表单的登录是否成功:
mvc
.perform(formLogin())
.andExpect(authenticated());
如果我们想 assert 用户的角色,我们可以优化我们以前的代码,如下所示:
mvc
.perform(formLogin().user("admin"))
.andExpect(authenticated().withRoles("USER","ADMIN"));
或者,我们可以验证用户名:
mvc
.perform(formLogin().user("admin"))
.andExpect(authenticated().withUsername("admin"));
我们还可以结合以下 assert:
mvc
.perform(formLogin().user("admin").roles("USER","ADMIN"))
.andExpect(authenticated().withUsername("admin"));
我们还可以对身份验证进行任意 assert
mvc
.perform(formLogin())
.andExpect(authenticated().withAuthentication(auth ->
assertThat(auth).isInstanceOf(UsernamePasswordAuthenticationToken.class)));