领先一步
VMware 提供培训和认证,助您加速进步。
了解更多[callout title=更新于 2015 年 3 月 31 日]此博客已过时且不再维护。请参阅参考文档的 测试部分 获取最新文档。 [/callout]
周一,我 宣布 了 Spring Security 4.0.0.M1 的发布。这是三部分博客系列的第一篇,将介绍 Spring Security 测试支持。系列大纲如下所示
测试基于方法的方法安全性一直以来都相当简单。但是,这并不意味着它不能做得更好。让我们通过一个非常简单的示例来探索如何使用 Spring Security 测试支持来简化基于方法的方法安全测试。
我们首先介绍一个 MessageService,它要求用户已登录才能访问它。
[callout title=源代码]您可以在 github 上找到此博客系列的完整源代码[/callout]
public class HelloMessageService implements MessageService {
@PreAuthorize("authenticated")
public String getMessage() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
return "Hello " + authentication;
}
}
getMessage 的结果是一个说“Hello”给当前 Spring Security 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 测试支持之前,我们必须进行一些设置。下面是一个示例
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TestExecutionListeners(listeners={ServletTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
WithSecurityContextTestExcecutionListener.class})
public class WithMockUserTests {
这是设置 Spring Security 测试的基本示例。重点内容可以很好地展示出来
@RunWith 指示 spring-test 模块它应该创建一个 ApplicationContext。这与使用现有的 Spring Test 支持没有区别。有关更多信息,请参阅 Spring 参考@ContextConfiguration 指示 spring-test 使用什么配置来创建 ApplicationContext。由于没有指定配置,它将尝试默认配置位置。这与使用现有的 Spring Test 支持没有区别。有关更多信息,请参阅 Spring 参考@TestExecutionListeners 指示 spring-test 模块,除了默认监听器之外,还使用 WithSecurityContextTestExcecutionListener,它将确保我们的测试以正确的用户运行。它通过在运行我们的测试之前填充 SecurityContextHolder 来实现这一点。测试完成后,它将清除 SecurityContextHolder。[callout 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 是一个 UserUser 将具有用户名“user”,密码“password”,并且 GrantedAuthoritys 是一个名为“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 所做的。
假设我们有一个作为 bean 公开的 UserDetailsService,那么以下测试将使用一个 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 测试支持,我们打算为测试创建一个 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 测试如何使测试基于方法的方法安全性变得更加容易。然而,最好的还在后面。在我们的 下一篇文章 中,我们将演示 Spring Security 测试如何使我们的应用程序与 Spring MVC Test 的测试更加容易。
[callout title=请提供反馈!] 如果您对这个博客系列或 Spring Security 测试支持有任何反馈,我鼓励您通过 JIRA、评论区,或者在 Twitter 上联系我 @rob_winch。当然,最好的反馈形式是 贡献。 [/callout]