预览 Spring Security 测试:方法安全

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

[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 以了解最新动态。

[/callout]

请记住,我们在 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 中填充的 AuthenticationUsernamePasswordAuthenticationToken 类型
  • Authentication 上的 principal 是一个 User
  • User 将具有用户名“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 {

@WithUserDetails

虽然 @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 要求用户必须存在。

@WithSecurityContext

我们已经看到,如果您不使用自定义 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;
    }
}

迈向 Web...

在本文中,我们演示了 Spring Security 测试如何使测试基于方法的方法安全性变得更加容易。然而,最好的还在后面。在我们的 下一篇文章 中,我们将演示 Spring Security 测试如何使我们的应用程序与 Spring MVC Test 的测试更加容易。

[callout title=请提供反馈!] 如果您对这个博客系列或 Spring Security 测试支持有任何反馈,我鼓励您通过 JIRA、评论区,或者在 Twitter 上联系我 @rob_winch。当然,最好的反馈形式是 贡献。 [/callout]

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有