抢占先机
VMware 提供培训和认证,助您加速发展。
了解更多[标注 title=更新于 2015 年 3 月 31 日]此博客已过时,不再维护。请参阅参考文档的测试部分以获取更新的文档。[/标注]
周一,我宣布了 Spring Security 4.0.0.M1 的发布。这是介绍 Spring Security 测试支持的三个部分博客系列中的第一篇。系列大纲如下所示
测试基于方法的安全性一直相当简单。然而,这并不意味着它不能做得更好。让我们探索一个非常简单的示例,看看如何使用 Spring Security Test 支持来简化基于方法的安全测试。
我们首先介绍一个 MessageService
,它要求用户经过身份验证才能访问它。
[标注 title=源代码]你可以在 github 上找到此博客系列的完整源代码。[/标注]
public class HelloMessageService implements MessageService {
@PreAuthorize("authenticated")
public String getMessage() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
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
在使用 Spring Security Test 支持之前,我们必须进行一些设置。一个示例如下所示
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TestExecutionListeners(listeners={ServletTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
WithSecurityContextTestExcecutionListener.class})
public class WithMockUserTests {
这是设置 Spring Security Test 的一个基本示例。重点如下
@RunWith
指示 spring-test 模块应创建一个 ApplicationContext。这与使用现有的 Spring Test 支持没有区别。更多信息请参阅 Spring 参考文档@ContextConfiguration
指示 spring-test 使用哪种配置来创建 ApplicationContext
。由于未指定配置,将尝试默认配置位置。这与使用现有的 Spring Test 支持没有区别。更多信息请参阅 Spring 参考文档@TestExecutionListeners
指示 spring-test 模块,除了默认的监听器外,还要使用 WithSecurityContextTestExcecutionListener
,它将确保我们的测试以正确的用户身份运行。它通过在运行测试之前填充 SecurityContextHolder
来实现这一点。测试完成后,它将清除 SecurityContextHolder
。[标注 title=注意]
我们理解 @TestExecutionListeners
相当冗长,并且目前有一些现有的 JIRA 问题有望在将来对此进行改进。请参阅 SEC-2585 以获取最新信息。
请记住,我们在 HelloMessageService
中添加了 @PreAuthorize
注解,因此它要求经过身份验证的用户才能调用。如果我们运行以下测试,我们期望以下测试会通过
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
messageService.getMessage();
}
问题是如何最方便地以特定用户身份运行测试。由于我们使用了 WithSecurityContextTestExcecutionListener
,以下测试将以用户名为 "user"、密码为 "password"、角色为 "ROLE_USER" 的用户身份运行。
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
具体来说,以下几点是成立的
SecurityContext
中的 Authentication
类型是 UsernamePasswordAuthenticationToken
Authentication
中的 principal 是一个 User
对象User
将具有用户名为 "user"、密码为 "password",以及 GrantedAuthority
s 只有一个名为 "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();
...
}
当然,在每个测试方法上放置注解可能有点麻烦。我们可以将注解放在类级别,这样每个测试都将使用指定的用户。例如,以下代码将以用户名为 "admin"、密码为 "password"、角色为 "ROLE_USER" 和 "ROLE_ADMIN" 的用户身份运行所有测试。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TestExecutionListeners(listeners={ServletTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
WithSecurityContextTestExcecutionListener.class})
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
虽然 @WithMockUser
是一个非常方便的入门方式,但它可能并非适用于所有情况。例如,应用程序通常期望 Authentication
principal 是特定类型的对象。这样做是为了让应用程序可以将 principal 引用为自定义类型,并减少对 Spring Security 的耦合。
自定义 principal 通常由自定义的 UserDetailsService
返回,它返回一个同时实现了 UserDetails
和自定义类型的对象。对于这种情况,使用自定义的 UserDetailsService
来创建测试用户非常有用。这正是 @WithUserDetails
的作用。
假设我们将一个 UserDetailsService
暴露为一个 bean,那么以下测试将使用类型为 UsernamePasswordAuthenticationToken
的 Authentication
以及从 UserDetailsService
返回的、用户名为 "user" 的 principal 来调用。
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
String message = messageService.getMessage();
...
}
我们还可以自定义用于从 UserDetailsService
中查找用户的用户名。例如,此测试将使用从 UserDetailsService
返回的、用户名为 "customUsername" 的 principal 执行。
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
与 @WithMockUser
一样,我们也可以将注解放在类级别,这样每个测试都将使用相同的用户。但与 @WithMockUser
不同的是,@WithUserDetails
要求用户实际存在。
我们已经看到,如果我们没有使用自定义的 Authentication
principal,那么 @WithMockUser
是一个很好的选择。接下来我们发现 @WithUserDetails
允许我们使用自定义的 UserDetailsService
来创建我们的 Authentication
principal,但它要求用户实际存在。现在我们将看到一个提供最大灵活性的选项。
我们可以创建自己的注解,该注解使用 @WithSecurityContext
来创建我们想要的任何 SecurityContext
。例如,我们可能会创建一个名为 @WithMockCustomUser
的注解,如下所示
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username() default "rob";
String name() default "Rob Winch";
}
您可以看到 @WithMockCustomUser
注解使用了 @WithSecurityContext
注解进行标注。这向 Spring Security Test 支持表明我们打算为测试创建一个 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;
}
}
现在,我们可以使用新注解来标注测试类或测试方法,而 Spring Security 的 WithSecurityContextTestExcecutionListener
将确保我们的 SecurityContext
被适当地填充。
在创建自己的 WithSecurityContextFactory
实现时,很高兴知道它们可以使用标准的 Spring 注解进行标注。例如,WithUserDetailsSecurityContextFactory
使用 @Autowired
注解来获取 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;
}
}
在这篇文章中,我们演示了 Spring Security Test 如何让测试基于方法的安全性变得更加容易。然而,最好的部分还在后面。在我们的下一篇文章中,我们将演示 Spring Security Test 如何让使用 Spring MVC Test 测试我们的应用程序变得更容易。
[标注 title=请提供反馈!] 如果您对此博客系列或 Spring Security Test 支持有任何反馈,我鼓励您通过 JIRA、评论区或在 twitter 上 ping 我 @rob_winch。当然,最好的反馈是贡献的形式。[/标注]