On this page
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的类型为UsernamePasswordAuthenticationTokenAuthentication上的主体是 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 公开,下面的测试将使用类型UsernamePasswordAuthenticationToken的Authentication和从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_USER和ROLE_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。