【问题标题】:How can test a Spring Boot controller method annoted with @PreAuthorized(hasAnyAuthority(...))如何测试使用 @PreAuthorized(hasAnyAuthority(...)) 注释的 Spring Boot 控制器方法
【发布时间】:2020-04-27 05:33:33
【问题描述】:

我的控制器类如下:

 @PostMapping(path="/users/{id}")
    @PreAuthorize("hasAnyAuthority('CAN_READ')")
    public @ResponseBody ResponseEntity<User> getUser(@PathVariable int id) {
        ...
    }

我有以下资源服务器配置

@Configuration
public class ResourceServerCofig implements ResourceServerConfigurer {

    private static final String RESOURCE_ID = "test";

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.cors()
            .and()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(RESOURCE_ID);

    }
}

最后我的测试是这样的:

 @RunWith(SpringRunner.class)
  @WebMvcTest(ClientController.class)
  public class ClientControllerTest {

      @Autowired
      private MockMvc mockMvc;

      @WithMockUser(authorities={"CAN_READ"})
      @Test
      public void should_get_user_by_id() throws Exception {
          ...

          mockMvc.perform(MockMvcRequestBuilders.get("/user/1")).
              andExpect(MockMvcResultMatchers.status().isOk()).
              andExpect(MockMvcResultMatchers.header().string(HttpHeaders.CONTENT_TYPE, "application/json")).
              andExpect(MockMvcResultMatchers.jsonPath("$.name").value("johnson"));
      }
  }

问题是我总是收到带有消息 unauthorized","error_description":"Full authentication is required to access this resource 的 401 HTTP 状态。

我应该如何为 @PreAuthorized 带注释的控制器方法方法编写测试?

【问题讨论】:

  • 您的某个配置类中有@EnableGlobalMethodSecurity(prePostEnabled = true) 注释吗?
  • 是的,我的主课上有它
  • 那我没主意了,你的设置看起来和我的一样(虽然我使用 hasRole() 而不是 hasAuthority(),但这几乎是一样的)。您确实验证了实时请求是否有效?否则,当 Spring Security 设置出现问题时,您可能会在查看测试代码时旋转车轮......
  • 明天给你答复:)
  • 请注意,我解决了我遇到的问题。它最终与 OAUTH2 无关,因为整个资源服务器配置甚至不是测试上下文的一部分(您必须专门@Import 它);基本上默认的 spring 安全配置正在激活,并且触发了 CSRF 检查失败。修复该检查将单元测试恢复到工作状态。

标签: java spring-boot spring-security


【解决方案1】:

我花了一天的时间想办法解决这个问题。我最终得到了一个我认为还不错的解决方案,并且可以帮助很多人。

根据您在测试中尝试执行的操作,您可以发送mockMvc 来测试您的控制器。请注意,未调用 AuthorizationServer。您只需要在资源服务器中进行测试。

  • 创建将在OAuth2AuthenticationProcessingFilter 中使用的bean InMemoryTokenStore 来验证您的用户。很棒的是,您可以在执行测试之前将令牌添加到您的InMemoryTokenStoreOAuth2AuthenticationProcessingFilter 将根据用户使用的令牌对用户进行身份验证,并且不会调用任何远程服务器。
@Configuration
public class AuthenticationManagerProvider {

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }
}
  • 注解@WithMockUser 不适用于OAuth2。事实上,OAuth2AuthenticationProcessingFilter 总是检查你的令牌,而不考虑SecurityContext。我建议使用与@WithMockUser 相同的方法,但使用您在代码库中创建的注释。 (进行一些易于维护和清洁的测试):
    @WithMockOAuth2Scope 几乎包含自定义身份验证所需的所有参数。你可以删除那些你永远不会使用的,但我做了很多以确保你看到可能性。 (将这 2 个类放在您的测试文件夹中)
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockOAuth2ScopeSecurityContextFactory.class)
public @interface WithMockOAuth2Scope {

    String token() default "";

    String clientId() default "client-id";

    boolean approved() default true;

    String redirectUrl() default "";

    String[] responseTypes() default {};

    String[] scopes() default {};

    String[] resourceIds() default {};

    String[] authorities() default {};

    String username() default "username";

    String password() default "";

    String email() default "";
}

然后,我们需要一个类来解释这个注解,并用测试所需的数据填充我们的 `InMemoryTokenStore。

@Component
public class WithMockOAuth2ScopeSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2Scope> {

    @Autowired
    private TokenStore tokenStore;

    @Override
    public SecurityContext createSecurityContext(WithMockOAuth2Scope mockOAuth2Scope) {

        OAuth2AccessToken oAuth2AccessToken = createAccessToken(mockOAuth2Scope.token());
        OAuth2Authentication oAuth2Authentication = createAuthentication(mockOAuth2Scope);
        tokenStore.storeAccessToken(oAuth2AccessToken, oAuth2Authentication);

        return SecurityContextHolder.createEmptyContext();
    }


    private OAuth2AccessToken createAccessToken(String token) {
        return new DefaultOAuth2AccessToken(token);
    }

    private OAuth2Authentication createAuthentication(WithMockOAuth2Scope mockOAuth2Scope) {

        OAuth2Request oauth2Request = getOauth2Request(mockOAuth2Scope);
        return new OAuth2Authentication(oauth2Request,
                getAuthentication(mockOAuth2Scope));
    }

    private OAuth2Request getOauth2Request(WithMockOAuth2Scope mockOAuth2Scope) {
        String clientId = mockOAuth2Scope.clientId();
        boolean approved = mockOAuth2Scope.approved();
        String redirectUrl = mockOAuth2Scope.redirectUrl();
        Set<String> responseTypes = new HashSet<>(asList(mockOAuth2Scope.responseTypes()));
        Set<String> scopes = new HashSet<>(asList(mockOAuth2Scope.scopes()));
        Set<String> resourceIds = new HashSet<>(asList(mockOAuth2Scope.resourceIds()));

        Map<String, String> requestParameters = Collections.emptyMap();
        Map<String, Serializable> extensionProperties = Collections.emptyMap();
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(mockOAuth2Scope.authorities());

        return new OAuth2Request(requestParameters, clientId, authorities,
                approved, scopes, resourceIds, redirectUrl, responseTypes, extensionProperties);
    }

    private Authentication getAuthentication(WithMockOAuth2Scope mockOAuth2Scope) {
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(mockOAuth2Scope.authorities());

        String username = mockOAuth2Scope.username();
        User userPrincipal = new User(username,
                mockOAuth2Scope.password(),
                true, true, true, true, grantedAuthorities);

        HashMap<String, String> details = new HashMap<>();
        details.put("user_name", username);
        details.put("email", mockOAuth2Scope.email());

        TestingAuthenticationToken token = new TestingAuthenticationToken(userPrincipal, null, grantedAuthorities);
        token.setAuthenticated(true);
        token.setDetails(details);

        return token;
    }

}
  • 一切都设置好后,在src/test/java/your/package/ 下创建一个简单的测试类。此类将执行mockMvc 操作,并使用@ WithMockOAuth2Scope 创建测试所需的令牌。
@WebMvcTest(SimpleController.class)
@Import(AuthenticationManagerProvider.class)
class SimpleControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockOAuth2Scope(token = "123456789",
            authorities = "CAN_READ")
    public void test() throws Exception {
        mockMvc.perform(get("/whoami")
                .header("Authorization", "Bearer 123456789"))
                .andExpect(status().isOk())
                .andExpect(content().string("username"));
    }
}

我想出了这个解决方案,感谢:

对于好奇的人:
测试时,Spring 会加载一个InMemoryTokenStore,并让您有可能采用您提供的一个(作为@Bean)。在生产中运行时,就我而言,Spring 使用RemoteTokenStore,它调用远程授权服务器来检查令牌(http://authorization_server/oauth/check_token)。
当您决定使用 OAuth2 时,Spring 会触发 OAuth2AuthenticationProcessingFilter。这是我在所有调试会话期间的切入点。

我学到了很多,谢谢你。
您可以找到source code here

希望对您有所帮助!

【讨论】:

  • 感谢@ruaro 的解决方案。当我在我的项目中进行测试时,我收到一条org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext 错误消息。在链接的 Github 存储库中克隆并运行测试,我收到 java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test 错误消息。
  • 您必须执行SimpleControllerTest,而不是OauthtestApplicationTests。对不起,我没有把所有东西都清理干净。关于您项目中的错误,您能给我们您尝试过的代码吗?您将课程放在哪里,如何运行测试?自上次以来,您发生了什么变化?
  • 你在哪里对。解决方案有效。我执行了SimpleControllerTest,测试通过了。这导致我重新检查我的代码。现在一切正常。再次非常感谢。
  • “注释 @WithMockUser 不适用于 OAuth2” - 确实是 ResourceServerConfigurationAdapter,经过几个小时的研究和实验,我得出了相同的结论。这是一种回归,因为在 Spring Boot 1.5 中它运行良好。
  • 不幸的是,Spring-boot 在更改主要版本时并不意味着兼容(有时甚至是次要版本)。他们确实改变了在测试中使用安全性的方式。这是他们自 spring-boot 开始以来的行为方式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-23
  • 2019-09-05
  • 2020-12-04
  • 1970-01-01
  • 2021-02-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多