当前位置: 首页 > 知识库问答 >
问题:

如何测试用@PreAuthorized(hasAnyAuthority(…))注释的Spring Boot控制器方法

蒋正平
2023-03-14

我的控制器类如下:

 @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"));
      }
  }

问题是我总是获得401 HTTP状态,并显示消息“unauthorized”,“error\u description”:“访问此资源需要完全身份验证”。

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

共有1个答案

黎玺
2023-03-14

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

根据您在测试中尝试做的事情,您可以执行mockMvc来测试您的控制器。请注意,没有调用AuthorizationServer。您只留在资源服务器中进行测试。

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

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }
}
  • 注释WithMockUser不适用于OAuth2。实际上,OAuth2AuthenticationProcessingFilter总是检查您的令牌,而不考虑SecurityContext。我建议使用与MockUser相同的方法,但使用您在代码库中创建的注释。(要进行一些易于维护和清理的测试):
    @WithMockOAuth2Scope包含自定义身份验证所需的几乎所有参数。你可以删除那些你永远不会使用的,但我放了很多,以确保你看到的可能性。(将这两个类放在测试文件夹中)
@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/code>下创建一个简单的测试类。此类将执行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-security-oAuth2资源服务器的安全性?
  • Spring假OAuth2单点登录,两种方式
  • 注释解
  • Fluent API解决方案
  • 很多调试,我在Spring中迷失了自己,但我终于找到了我要找的东西。

出于好奇:
测试时,Spring会在MemoryTokenStore中加载一个,并让您可以使用您提供的一个(作为@Bean)。在生产环境中运行时,对于我的情况,Spring使用一个RemoteTokenStore,它调用远程授权服务器来检查令牌(http://authorization_server/oauth/check_token
当您决定使用OAuth2时,Spring将启动OAuth2AuthenticationProcessingFilter。这是我在所有me调试会话期间的入口点。

我学到了很多,谢谢你
您可以在这里找到源代码。

希望这会有帮助!

 类似资料:
  • 因此,我正在进行我的第一个Spring Boot项目,我一直在进行测试。我查了很多例子,但似乎都不管用。 这是我的控制器的当前测试: 这是可行的,但在sonarqube上,我发现我的代码覆盖率为0%,而我似乎找不到一个测试,它的覆盖率甚至超过了零。有谁能给我一个关于如何为控制器编写一个好的单元测试的例子,然后我就可以根据您的例子自己解决这个问题。 这是我的控制器: 这是我的服务(以防您需要): 还

  • 我对使用Spring控制器进行单元测试的概念是新的。我正在遵循我在网上找到的一些示例,并尝试实现他们的测试策略。这是我的基本控制器: 这是我的单元测试: 看起来很简单,但我得到了以下错误: 它完成了这项工作,但它没有像我之前尝试的那样使用任何Spring注释…这种方法是不好的,所以试图弄清楚为什么每当我在测试文件中包含注释时,总是会出现错误。 我的POM:

  • 我在我的SpringBoot应用程序“demo”中使用了注释样式的Resilience4j。当通过RestTemplate调用外部后端时,我希望使用TimeLimiter并重试以实现以下目标: 将REST调用持续时间限制在5秒-->如果需要更长时间,则使用TimeoutException失败 在TimeoutException上重试-->最多尝试2次 为了查看弹性设置的配置是否如设计的那样工作,我

  • 问题内容: 我正在关注Spring 2.5教程,并尝试同时将代码/设置更新为Spring 3.0。 在 Spring 2.5中, 我有了 HelloController (供参考): 还有一个用于 HelloController 的JUnit测试(供参考): 但是现在我将控制器更新为 Spring 3.0 ,并且现在使用注释(我还添加了一条 消息 ): 知道我正在使用JUnit 4.9,有人可以解

  • 问题内容: 我正在使用spring 3.2.0和junit 4 这是我需要测试的控制器方法 spring-servlet config is: This is my test class : 如何使用MockMvc测试此方法? 问题答案: 你可以使用以下注释来使用应用程序调度程序servlet xml。以下示例使用路径/ mysessiontest设置了一些会话属性并期望返回某个视图来命中控制器:

  • 我的SpringBoot应用程序中有一个控制器: 我想在mocks的帮助下,将其与服务分开进行测试。如何实施?