领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多[侧边栏标题=2015年3月31日更新]此博客已过时,不再维护。请参阅参考文档的测试部分以获取更新的文档。[/侧边栏]
周一,我宣布发布 Spring Security 4.0.0.M1。这是介绍 Spring Security 测试支持的三部分博客系列的第一部分。系列概述如下所示
基于方法的安全测试一直以来都相当简单。但是,这并不意味着它不能更好。让我们探索一个非常简单的示例,看看如何使用 Spring Security 测试支持使基于方法的安全测试更容易。
我们首先引入一个`MessageService`,它要求用户经过身份验证才能访问它。
[侧边栏标题=源代码]您可以在github上找到此博客系列的完整源代码[/侧边栏]
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.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 测试的基本示例。重点可以很好地看到。
[侧边栏标题=注意]
可以理解的是,`@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();
...
}
具体来说,以下为真:
我们的示例很好,因为它提供了许多默认值。如果我们想以不同的用户名运行测试怎么办?以下测试将以用户名“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` 主体的类型特定。这样做是为了让应用程序可以将主体引用为自定义类型并减少对 Spring Security 的耦合。
自定义主体通常由自定义 `UserDetailsService` 返回,该服务返回同时实现 `UserDetails` 和自定义类型的对象。对于这种情况,使用自定义 `UserDetailsService` 创建测试用户非常有用。这正是 `@WithUserDetails` 所做的。
假设我们有一个作为 bean 公开的 `UserDetailsService`,以下测试将使用类型为 `UsernamePasswordAuthenticationToken` 的 `Authentication` 和从 `UserDetailsService` 返回的主体(用户名为“user”)调用。
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
String message = messageService.getMessage();
...
}
我们还可以自定义用于从我们的 `UserDetailsService` 中查找用户的用户名。例如,此测试将使用从 `UserDetailsService` 返回的主体(用户名为“customUsername”)执行。
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
像 `@WithMockUser` 一样,我们也可以将注释放在类级别,以便每个测试都使用相同的用户。但是,与 `@WithMockUser` 不同的是,`@WithUserDetails` 需要用户存在。
我们已经看到,如果我们不使用自定义 `Authentication` 主体,`@WithMockUser` 是一个极好的选择。接下来,我们发现 `@WithUserDetails` 将允许我们使用自定义 `UserDetailsService` 来创建我们的 `Authentication` 主体,但需要用户存在。我们现在将看到一个允许最大灵活性的选项。
我们可以创建自己的注释,使用 `@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 Test 如何使基于方法的安全测试更容易。然而,最好的还在后面。在我们的下一篇文章中,我们将演示 Spring Security Test 如何使使用 Spring MVC Test 测试我们的应用程序更容易。
[信息提示 标题=请反馈!] 如果你对这个博客系列或 Spring Security 测试支持有任何反馈,我鼓励你通过JIRA、评论区或在 Twitter 上联系我 @rob_winch。当然,最好的反馈是来自贡献。[/信息提示]