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 公开,下面的测试将使用类型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
带有@WithSecurityContext
Comments。这就是向 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
使用@Autowired
Comments 获取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。