11. 测试方法安全

本节演示了如何使用 Spring Security 的 Test 支持来测试基于方法的安全性。首先,我们引入一个MessageService,要求用户经过身份验证才能访问它。

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.springframew[emailprotected]ca25360: Principal: [emailprotected]: 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

11.1 安全测试设置

在使用 Spring Security Test 支持之前,我们必须执行一些设置。下面是一个示例:

@RunWith(SpringJUnit4ClassRunner.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {

这是如何设置 Spring Security Test 的基本示例。重点是:

  • (1) @RunWith指示 Spring 测试模块应创建ApplicationContext。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅Spring Reference
  • (2) @ContextConfiguration指示对配置进行 Spring 测试以创建ApplicationContext。由于未指定任何配置,因此将尝试使用默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅Spring Reference

Note

Spring Security 使用WithSecurityContextTestExecutionListener连接到 Spring Test 支持,这将确保我们的测试以正确的用户运行。它是通过在运行测试之前填充SecurityContextHolder来实现的。测试完成后,它将清除SecurityContextHolder。如果只需要 Spring Security 相关的支持,则可以将@ContextConfiguration替换为@SecurityTestExecutionListeners

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

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

11.2 @WithMockUser

问题是“我们如何最轻松地以特定用户身份运行测试?”答案是使用@WithMockUser。以下测试将以用户名“ user”,密码“ password”和角色“ ROLE_USER”的用户身份运行。

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

具体来说,以下是正确的:

  • 用户名“ user”的用户不必存在,因为我们在模拟用户

  • SecurityContext中填充的Authentication的类型为UsernamePasswordAuthenticationToken

  • Authentication上的主体是 Spring Security 的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();
	...
}

如果我们不希望该值自动以 ROLE 作为前缀,则可以利用 Authority 属性。例如,将使用用户名“ admin”以及权限“ USER”和“ ADMIN”来调用此测试。

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
	String message = messageService.getMessage();
	...
}

当然,将 Comments 放在每种测试方法上可能会有些乏味。相反,我们可以将 Comments 放置在类级别,并且每个测试都将使用指定的用户。例如,下面的代码将使用用户名“ admin”,密码“ password”以及角色“ ROLE_USER”和“ ROLE_ADMIN”的用户运行每个测试。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {

11.3 @WithAnonymousUser

使用@WithAnonymousUser允许以匿名用户身份运行。当您希望与特定用户一起运行大多数测试,但又希望以匿名用户身份运行一些测试时,这特别方便。例如,以下将使用@WithMockUser并以匿名用户身份匿名运行 withMockUser1 和 withMockUser2.

@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

	@Test
	public void withMockUser1() {
	}

	@Test
	public void withMockUser2() {
	}

	@Test
	@WithAnonymousUser
	public void anonymous() throws Exception {
		// override default to run as anonymous user
	}
}

11.4 @WithUserDetails

@WithMockUser是一种非常方便的入门方法,但不一定在所有情况下都起作用。例如,应用程序通常期望Authentication主体是特定类型。这样做是为了使应用程序可以将委托人称为自定义类型,并减少 Spring Security 上的耦合。

自定义主体通常由自定义UserDetailsService返回,自定义UserDetailsService返回实现UserDetails和自定义类型的对象。对于这种情况,使用自定义UserDetailsService创建测试用户非常有用。正是@WithUserDetails所做的。

假设我们有一个UserDetailsService作为 bean 公开,下面的测试将使用类型UsernamePasswordAuthenticationTokenAuthentication和从UserDetailsService返回的用户名“ user”的主体来调用。

@Test
@WithUserDetails
public void getMessageWithUserDetails() {
	String message = messageService.getMessage();
	...
}

我们还可以自定义用于从UserDetailsService查找用户的用户名。例如,将使用从用户名为“ customUsername”的UserDetailsService返回的委托人来执行此测试。

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
	String message = messageService.getMessage();
	...
}

我们还可以提供一个明确的 bean 名称来查找UserDetailsService。例如,此测试将使用 Bean 名称为“ myUserDetailsService”的UserDetailsService查找“ customUsername”的用户名。

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
	String message = messageService.getMessage();
	...
}

@WithMockUser一样,我们也可以将 Comments 放在类级别,以便每个测试使用同一用户。但是,与@WithMockUser不同,@WithUserDetails要求用户存在。

11.5 @WithSecurityContext

我们已经看到,如果不使用自定义Authentication主体,则@WithMockUser是一个很好的选择。接下来,我们发现@WithUserDetails允许我们使用自定义UserDetailsService创建我们的Authentication主体,但是要求用户存在。现在,我们将看到一个具有最大灵 Active 的选项。

我们可以创建自己的 Comments,该 Comments 使用@WithSecurityContext创建所需的任何SecurityContext。例如,我们可以创建一个名为@WithMockCustomUser的 Comments,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

	String username() default "rob";

	String name() default "Rob Winch";
}

您可以看到@WithMockCustomUser带有@WithSecurityContextComments。这就是向 Spring Security Test 支持发出 signal 的 signal,我们打算为该测试创建一个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;
	}
}

现在,我们可以使用新的 Comments 对测试类或测试方法进行 Comments,Spring Security 的WithSecurityContextTestExecutionListener将确保正确填充SecurityContext

创建自己的WithSecurityContextFactory实现时,很高兴知道可以使用标准的 SpringComments 对其进行 Comments。例如,WithUserDetailsSecurityContextFactory使用@AutowiredComments 获取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;
	}
}

11.6 测试元 Comments

如果您经常在测试中重用同一用户,则不理想的是必须重复指定属性。例如,如果有许多与用户名为“ admin”且角色为ROLE_USERROLE_ADMIN的 Management 用户相关的测试,则您必须编写:

@WithMockUser(username="admin",roles={"USER","ADMIN"})

我们可以使用元 Comments,而不是在所有地方重复此操作。例如,我们可以创建一个名为WithMockAdmin的元 Comments:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }

现在,我们可以像更详细的@WithMockUser一样使用@WithMockAdmin

元 Comments 可与上述任何测试 Comments 一起使用。例如,这意味着我们也可以为@WithUserDetails("admin")创建一个元 Comments。