在Spring Boot测试中无法使用@WithUserDetails来使用CustomUser。通过普通 User 类而不是 CustomUser

问题描述 投票:0回答:1

我正在尝试做一些应该非常简单的事情,那就是在我的

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 而不是我的子类中的额外详细信息。

java spring junit spring-boot-test
1个回答
0
投票

简短回答

您使用

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
,那么我
将实现此接口作为家庭作业

© www.soinside.com 2019 - 2024. All rights reserved.