Spring Security 测试预览:方法安全

工程 | Rob Winch | 2014年5月7日 | ...

[侧边栏标题=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 测试的基本示例。重点可以很好地看到。

  • `@RunWith` 指示 spring-test 模块它应该创建一个 ApplicationContext。这与使用现有的 Spring Test 支持没有什么不同。有关更多信息,请参阅Spring 参考文档
  • `@ContextConfiguration` 指示 spring-test 要使用的配置来创建 `ApplicationContext`。由于没有指定配置,因此将尝试默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关更多信息,请参阅Spring 参考文档
  • `@TestExecutionListeners` 指示 spring-test 模块除了默认侦听器之外,还要使用 `WithSecurityContextTestExcecutionListener`,这将确保我们的测试以正确的用户身份运行。它通过在运行测试之前填充 `SecurityContextHolder` 来做到这一点。测试完成后,它将清除 `SecurityContextHolder`。

[侧边栏标题=注意]

可以理解的是,`@TestExecutionListeners` 非常冗长,并且有很多现有的 JIRA 应该在将来改进这一点。请查看SEC-2585以了解最新情况。

[/侧边栏]

记住我们在我们的 `HelloMessageService` 中添加了 `@PreAuthorize` 注解,因此它需要经过身份验证的用户才能调用它。如果我们运行以下测试,我们预期以下测试将通过。

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
    messageService.getMessage();
}

@WithMockUser

问题是如何最轻松地以特定用户身份运行测试。由于我们使用的是 `WithSecurityContextTestExcecutionListener`,因此以下测试将以用户名为“user”,密码为“password”且角色为“ROLE_USER”的用户身份运行。

@Test
@WithMockUser
public void getMessageWithMockUser() {
  String message = messageService.getMessage();
  ...
}

具体来说,以下为真:

  • 用户名为“user”的用户不必存在,因为我们正在模拟用户。
  • 在 `SecurityContext` 中填充的 `Authentication` 的类型为 `UsernamePasswordAuthenticationToken`。
  • `Authentication` 上的主体是 `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();
	...
}

当然,在每个测试方法上放置注释可能会有点繁琐。相反,我们可以将注释放在类级别,每个测试都将使用指定的用户。例如,以下操作将使用用户名为“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 {

@WithUserDetails

虽然 `@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` 需要用户存在。

@WithSecurityContext

我们已经看到,如果我们不使用自定义 `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。当然,最好的反馈是来自贡献。[/信息提示]

获取 Spring 新闻通讯

关注 Spring 新闻通讯

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获得支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部