【问题标题】:Why do I get null `Authentication` as @Controller method parameter in `@WebMvcTest`?为什么我在`@WebMvcTest`中得到空`Authentication`作为@Controller方法参数?
【发布时间】:2020-07-11 09:35:11
【问题描述】:

上下文:我创建了一个测试注释@WithMockAuthentication 来使用Authentication 实例填充测试安全上下文,就像@WithMockUser 所做的那样。 主要区别在于,在我的例子中,实例是一个 Mockito 模拟。

我的经验:一旦我用模拟替换实际实例,作为控制器方法参数提供的 Authentication 实例在带注释的测试中为空:在 WithSecurityContextFactory 中,如果我替换:

    public Authentication workingAuthentication(WithMockAuthentication annotation) {
        return new TestAuthentication(annotation.name(), Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
    }

    public Authentication bogousAuthentication(WithMockAuthentication annotation) {
        var auth = mock(Authentication.class);
        when(auth.getName()).thenReturn(annotation.name());
        when(auth.getAuthorities()).thenReturn((Collection) Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
        when(auth.isAuthenticated()).thenReturn(true);
        return auth;
    }

然后我在控制器测试中得到 NPE

    @RequestMapping("/method")
    @PreAuthorize("hasRole('ROLE_AUTHORIZED')")
    public ResponseEntity<String> securedMethod(Authentication auth) {
        // Here, auth is null if Authentication is a mock
        return ResponseEntity.ok(String.format("Hey %s, how are you?", auth.getName()));
    }

我创建了一个minimal sample to reproduce。运行测试看看失败。

我很确定我遇到了一个错误。够创建an issue in spring-security project了,不过Spring团队好像没时间调查了……

[编辑] 最后这句话是无用的冒犯性和完全错误的,因为答案是由 Spring-security 的主要成员 Rob Winch 提供的:/我的坏

【问题讨论】:

    标签: spring-mvc spring-security spring-test-mvc


    【解决方案1】:

    SampleController 的参数是 Authentication 类型,它是 Principal 的一个实例,因此 ServletRequestMethodArgumentResolver 将尝试解析来自 HttpServletRequest.getUserPrincipal() 的参数。

    mock that you are creating 没有存根 Authentication.getPrincipal() 方法。

    public Authentication bogousAuthentication(WithMockAuthentication annotation) {
        var auth = mock(Authentication.class);
        when(auth.getName()).thenReturn(annotation.name());
        when(auth.getAuthorities()).thenReturn((Collection) Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
        when(auth.isAuthenticated()).thenReturn(true);
        return auth;
    }
    

    因此,Authentication.getPrincipal()null,因此 SecurityContextHolderAwareRequestWrapper.getUserPrincipal() 返回 null。当主体为null 时,为什么它返回null?在成为团队成员之前,我无法确定 code was added 的初衷。但是,它在 Spring Security 的模型中是有意义的。 Authentication 可以代表经过身份验证的用户和用于身份验证的凭据。 Javadoc of Authentication.getPrincipal() 状态(强调我的):

    被认证的主体的身份。在一个 带有用户名和密码的身份验证请求,这将是 用户名。 调用者应填充主体 身份验证请求

    AuthenticationManager 实现通常会返回一个 身份验证包含更丰富的信息作为使用主体 由应用程序。许多身份验证提供程序将创建一个 UserDetails 对象作为主体。

    null 检查是为了确保Authentication 确实代表经过身份验证的用户。

    要修复它,您必须使用 when(auth.getPrincipal()).thenReturn("bogus"); 之类的东西将 getPrincipal() 方法存根。可以在下面和我的pull request 中看到更改。

    public Authentication bogousAuthentication(WithMockAuthentication annotation) {
        var auth = mock(Authentication.class);
        when(auth.getPrincipal()).thenReturn("bogus");
        when(auth.getName()).thenReturn(annotation.name());
        when(auth.getAuthorities()).thenReturn((Collection) Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
        when(auth.isAuthenticated()).thenReturn(true);
        return auth;
    }
    

    【讨论】:

    • 另外,我需要重现,但我几乎可以肯定 webflux 中提供了 Authentication 参数。双方的行为不应该是一样的吗?
    • 我更新了答案以说明为什么要执行 getPrincipal 检查。
    • 我对规范有不同的理解。 Authentication 是(扩展)Principal。不应该通过检查auth.getName() == null(而不是auth.getPrincipal() == null)来实现“调用者应该填充身份验证请求的主体”,因为这是Principal接口中指定的方法,并且主体不是@987654351 @ 但Object ?
    • 我会转过身来问为什么你会有一个 getPrincipal 为空的身份验证?我了解您来自哪里,但这就是 Spring Security 选择将 Authentication 对象映射到 Principal 的方式。老实说,我不确定我会选择那样做,但考虑到它会产生的影响,在这一点上改变它有点不可能。
    • 我不使用getPrincipal()返回值。大多数时候,授予权限就足够了,有时,用户名(Authentication.getName() 应该回答这个问题)来检索用户详细信息,如果我需要使用更多 Authentication 内部信息,那么我使用的实现会公开其他内容Object。因此,我使用 getPrincipal()、getDetails() 或 getCredentials() 不为 null 的身份验证的唯一原因是因为我被框架强制(在我的安全表达式或方法主体中没有用)
    猜你喜欢
    • 2022-01-02
    • 2017-04-06
    • 1970-01-01
    • 2014-01-22
    • 1970-01-01
    • 2013-02-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多