我正在尝试做一些应该非常简单的事情,那就是在我的
CustomUserDetails
中进行SpingBoot测试,以便在测试期间使用@WithUserDetails
进行身份验证。
问题是被测试的方法总是用标准的
org.springframework.security.core.userdetails.User
而不是我的CustomUserDetails
来调用。
我已经实现了我自己的
UserDetailsService
并确定它正在被调用。它返回我的 CustomUserDetails
的实例,但只有普通 User
对象传递给我的方法。
我一生都无法弄清楚为什么。
当我将代码作为应用程序运行时,它运行良好。我只是无法让它与测试用户一起用于我的测试用例。
我的代码是:
自定义用户类
@Data
public class CustomUserDetails extends User {
private ZoneId zoneId;
public CustomUserDetails(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
我的测试 userDetailsService 返回 CustomUserDetails 的实例
@Bean
@Primary
public InMemoryUserDetailsManager myUserDetailsService() {
CustomUserDetails basicUser = new CustomUserDetails("[email protected]", "password", true, true, true, true, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
basicUser.setZoneId(ZoneId.of("UTC"));
return new InMemoryUserDetailsManager(basicUser);
}
我的实际测试
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SpringSecurityTestConfig.class
)
@AutoConfigureMockMvc(addFilters = false)
class UploadFileTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithUserDetails(value="[email protected]", userDetailsServiceBeanName="myUserDetailsService")
void doValidUploadFileNoParams() throws Exception {
mockMvc.perform(get("/web/uploadFile"))
.andExpect(status().isOk());
}
}
我要测试的服务方法
@GetMapping("/uploadFile")
public String listUploadedFiles(@AuthenticationPrincipal CustomUserDetails userDetails, Model model) {
List<ScanQueryFileUploadDetailsDTO> fileUploadList = storageService.findAllDetails();
//if users timeZone is set and is NOT UTC then change. If UTC then just leave unchanged.
if((userDetails.getZoneId() != null) && (!userDetails.getZoneId().equals(ZoneId.of("UTC")))) {
//convert times to match ZoneID of logged in user
fileUploadList.forEach(f -> f.setUploadDateTime(convertDateTime(f.getUploadDateTime(), userDetails.getZoneId())));
model.addAttribute("files", fileUploadList);
return "uploadForm";
}
通过上述参数
CustomUserDetails
userDetails
将是 null
。
如果我将其更改为
org.springframework.security.core.userdetails.User
的实例,则会填充它,但该属性不是我的 CustomUserDetails
的实例,并且无法转换为类。
我已确定
myUserDetailsService
正在按要求提供 bean。
我尝试了@annotations 的各种不同组合,但似乎没有什么可以改变它。
我发现很奇怪,我用
myUserDetailsService
对象填充我的 CustomUserDetails
,并且该对象被传递给我的方法,但仅作为父 User 而不是我的子类中的额外详细信息。
您使用
InMemoryUserDetailsManager
填充了 CustomUserDetails
的实例。但按照设计,InMemoryUserDetailsManager
从User
返回loadUserByUsername()
的实例。
要从
CustomUserDetails
检索 loadUserByUsername()
,您需要使用 UserDetailsService
或 UserDetailsManager
合约的 自定义实现(这将是现实应用程序中的情况,因为需要保留用户,而不是将用户数据保存在内存中)。
让我们更详细地了解一下配置测试
SecurityContext
时发生的情况。
@WithUserDetails()
在执行测试方法之前,新创建的
SecurityContext
将填充 Authentication
的实例。当使用您在 Authention
注释中指定的用户名调用 UserDetails
时,UserDetailsService
中的主体将与 loadUserByUsername()
返回的 @WithUserDetails
完全相同。如果你想验证我所说的是否正确,请查看WithUserDetailsSecurityContextFactory.createSecurityContext()
的实现。到目前为止,一切都很好。
InMemoryUserDetailsManager
UserDetails
实现进入
InMemoryUserDetailsManager
时,它会被 MutableUser
实例装饰,以便于通过 UserDetailsManager.changePassword()
执行密码更新。public InMemoryUserDetailsManager(UserDetails... users) {
for (UserDetails user : users) {
createUser(user);
}
}
@Override
public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()), "user should exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
当您使用
UserDetails
检索
loadUserByUsername()
时,它会构造一个不可变的 User
实例,忽略原始 UserDetails
实例可能具有的所有自定义数据。@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
催眠般地,如果它知道如何基于初始实例和新密码构建它,它可能会返回任何
UserDetails
实现(
这可以通过在实例化管理器时提供工厂来完成)。但由于
InMemoryUserDetailsManager
仅适用于编写简单的演示和探索 UserDetailsManager
合约之类的事情,因此可能假设没有人会超越标准属性。无论如何,InMemoryUserDetailsManager
只能给你
org.springframework.security.core.userdetails.User
。解决方案
UserDetailsManager
(或
UserDetailsService
)。最终,您希望保留用户,因此实施应该由存储库支持。
如果您只想继续尝试 Spring Security 并且暂时不想处理持久性,那么您可以创建自己的内存中实现。
下面是
UserDetailsService
实施方式的示例:
public class CustomInMemoryUserDetailsService implements UserDetailsService {
private final Map<String, UserDetails> userByName;
public CustomInMemoryUserDetailsService(UserDetails... users) {
this.userByName = Arrays.stream(users)
.collect(toMap(
UserDetails::getUsername,
Function.identity()
));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
var user = userByName.get(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return user;
}
}
在配置类中将
InMemoryUserDetailsManager
替换为
CustomInMemoryUserDetailsService
将解决该问题:@Bean
UserDetailsService myUserDetailsService() {
var user = new CustomUserDetails(
"[email protected]",
"password",
List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
return new CustomInMemoryUserDetailsService(user);
}
UserDetailsService
更容易实现,因为它只定义了一种方法,但它的能力不如
UserDetailsManager
。如果读者有兴趣使用UserDetailsManager
,那么我将实现此接口作为家庭作业。