WebSecurityConfiguration antMatcher() 不忽略集成测试中外部 API 的特定 url

WebSecurityConfiguration antMatcher() not ignoring specific url for external API in integration test

我正在 Spring 启动应用程序中实施 AWS Cognito 安全机制。启用安全性后,我遇到了外部 API 现有集成测试的问题。 作为测试结果,我收到一个错误:

2020-11-15 18:18:20.033 ERROR 12072 --- [ main] .c.s.f.AwsCognitoJwtAuthenticationFilter : Invalid Action, no token found MockHttpServletResponse: Status = 401 Error message = null Headers = [Access-Control-Allow-Origin:"*", Access-Control-Allow-Methods:"POST, GET, OPTIONS, PUT, DELETE", Access-Control-Max-Age:"3600", Access-Control-Allow-Credentials:"true", Access-Control-Allow-Headers:"content-type,Authorization", Content-Type:"application/json"] Content type = application/json Body = {"data":null,"exception":{"message":"JWT Handle exception","httpStatusCode":"INTERNAL_SERVER_ERROR","detail":null}}

我的 WebSecurityConfiguration 看起来像:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableTransactionManagement
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

  private CustomAuthenticationProvider authProvider;
  private AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter;
  private AccountControllerExceptionHandler exceptionHandler;
  private static final String LOGIN_URL = "/auth/login";
  private static final String LOGOUT_URL = "/auth/signOut";

  @Autowired
  public WebSecurityConfiguration(
      CustomAuthenticationProvider authProvider,
      AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter,
      AccountControllerExceptionHandler exceptionHandler) {
    this.authProvider = authProvider;
    this.awsCognitoJwtAuthenticationFilter = awsCognitoJwtAuthenticationFilter;
    this.exceptionHandler = exceptionHandler;
  }

  public WebSecurityConfiguration() {
    super(true);
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(authProvider).eraseCredentials(false);
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  @Override
  public void configure(WebSecurity web) {
    // TokenAuthenticationFilter will ignore the below paths
    web.ignoring().antMatchers("/auth");
    web.ignoring().antMatchers("/auth/**");
    web.ignoring().antMatchers("/v2/api-docs");
    web.ignoring().antMatchers(GET, "/nutrition/api/**");
    web.ignoring().antMatchers(GET, "/**");
    web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
  }

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
        .addFilterAfter(corsFilter(), ExceptionTranslationFilter.class)
        .exceptionHandling()
        .authenticationEntryPoint(new SecurityAuthenticationEntryPoint())
        .accessDeniedHandler(new RestAccessDeniedHandler())
        .and()
        .anonymous()
        .and()
        .sessionManagement()
        .sessionCreationPolicy(STATELESS)
        .and()
        .authorizeRequests()
        .antMatchers("/auth")
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .addFilterBefore(
            awsCognitoJwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        .formLogin(formLogin -> formLogin.loginProcessingUrl(LOGIN_URL).failureHandler(exceptionHandler))
        .logout(logout -> logout.permitAll().logoutUrl(LOGOUT_URL))
        .csrf(AbstractHttpConfigurer::disable);
  }

  private CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader(ORIGIN);
    config.addAllowedHeader(CONTENT_TYPE);
    config.addAllowedHeader(ACCEPT);
    config.addAllowedHeader(AUTHORIZATION);
    config.addAllowedMethod(GET);
    config.addAllowedMethod(PUT);
    config.addAllowedMethod(POST);
    config.addAllowedMethod(OPTIONS);
    config.addAllowedMethod(DELETE);
    config.addAllowedMethod(PATCH);
    config.setMaxAge(3600L);

    source.registerCorsConfiguration("/v2/api-docs", config);
    source.registerCorsConfiguration("/**", config);

    return new CorsFilter();
  }
}

AwsCognitoJwtAuthenticationFilter

@Slf4j
public class AwsCognitoJwtAuthenticationFilter extends OncePerRequestFilter {

  private static final String ERROR_OCCURRED_WHILE_PROCESSING_THE_TOKEN =
      "Error occured while processing the token";
  private static final String INVALID_TOKEN_MESSAGE = "Invalid Token";

  private final AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor;

  @Autowired private ApplicationContext appContext;

  public AwsCognitoJwtAuthenticationFilter(AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor) {
    this.awsCognitoIdTokenProcessor = awsCognitoIdTokenProcessor;
  }

  private void createExceptionResponse(
      ServletRequest request, ServletResponse response, CognitoException exception)
      throws IOException {
    HttpServletRequest req = (HttpServletRequest) request;
    ExceptionController exceptionController;
    ObjectMapper objMapper = new ObjectMapper();

    exceptionController = appContext.getBean(ExceptionController.class);
    ResponseData<Object> responseData = exceptionController.handleJwtException(req, exception);

    HttpServletResponse httpResponse = CorsHelper.addResponseHeaders(response);

    final HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(httpResponse);
    wrapper.setStatus(HttpStatus.UNAUTHORIZED.value());
    wrapper.setContentType(APPLICATION_JSON_VALUE);
    wrapper.getWriter().println(objMapper.writeValueAsString(responseData));
    wrapper.getWriter().flush();
  }

  @Override
  protected void doFilterInternal(
      HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    Authentication authentication;
    try {
      authentication = awsCognitoIdTokenProcessor.getAuthentication(request);

      SecurityContextHolder.getContext().setAuthentication(authentication);

    } catch (BadJOSEException e) {
      SecurityContextHolder.clearContext();
      log.error(e.getMessage());
      createExceptionResponse(
          request,
          response,
          new CognitoException(
              INVALID_TOKEN_MESSAGE,
              CognitoException.INVALID_TOKEN_EXCEPTION_CODE,
              e.getMessage()));
      return;
    } catch (CognitoException e) {
      SecurityContextHolder.clearContext();
      log.error(e.getMessage());
      createExceptionResponse(
          request,
          response,
          new CognitoException(
              e.getErrorMessage(),
              CognitoException.INVALID_TOKEN_EXCEPTION_CODE,
              e.getDetailErrorMessage()));
      return;
    } catch (Exception e) {
      SecurityContextHolder.clearContext();
      log.error(e.getMessage());
      createExceptionResponse(
          request,
          response,
          new CognitoException(
              ERROR_OCCURRED_WHILE_PROCESSING_THE_TOKEN,
              CognitoException.INVALID_TOKEN_EXCEPTION_CODE,
              e.getMessage()));
      return;
    }

    filterChain.doFilter(request, response);
  }
}

AwsCognitoIdTokenProcessor

@AllArgsConstructor
@NoArgsConstructor
public class AwsCognitoIdTokenProcessor {

  private static final String INVALID_TOKEN = "Invalid Token";
  private static final String NO_TOKEN_FOUND = "Invalid Action, no token found";

  private static final String ROLE_PREFIX = "ROLE_";
  private static final String EMPTY_STRING = "";

  private ConfigurableJWTProcessor<SecurityContext> configurableJWTProcessor;

  private AWSConfig jwtConfiguration;

  private String extractAndDecodeJwt(String token) {
    String tokenResult = token;

    if (token != null && token.startsWith("Bearer ")) {
      tokenResult = token.substring("Bearer ".length());
    }
    return tokenResult;
  }

  @SuppressWarnings("unchecked")
  public Authentication getAuthentication(HttpServletRequest request)
      throws ParseException, BadJOSEException, JOSEException {
    String idToken = request.getHeader(HTTP_HEADER);
    if (idToken == null) {
      throw new CognitoException(
          NO_TOKEN_FOUND,
          NO_TOKEN_PROVIDED_EXCEPTION,
          "No token found in Http Authorization Header");
    } else {

      idToken = extractAndDecodeJwt(idToken);
      JWTClaimsSet claimsSet;

      claimsSet = configurableJWTProcessor.process(idToken, null);

      if (!isIssuedCorrectly(claimsSet)) {
        throw new CognitoException(
            INVALID_TOKEN,
            INVALID_TOKEN_EXCEPTION_CODE,
            String.format(
                "Issuer %s in JWT token doesn't match cognito idp %s",
                claimsSet.getIssuer(), jwtConfiguration.getCognitoIdentityPoolUrl()));
      }

      if (!isIdToken(claimsSet)) {
        throw new CognitoException(
            INVALID_TOKEN, NOT_A_TOKEN_EXCEPTION, "JWT Token doesn't seem to be an ID Token");
      }

      String username = claimsSet.getClaims().get(USER_NAME_FIELD).toString();

      List<String> groups = (List<String>) claimsSet.getClaims().get(COGNITO_GROUPS);
      List<GrantedAuthority> grantedAuthorities =
          convertList(
              groups, group -> new SimpleGrantedAuthority(ROLE_PREFIX + group.toUpperCase()));
      User user = new User(username, EMPTY_STRING, grantedAuthorities);
      return new CognitoJwtAuthentication(user, claimsSet, grantedAuthorities);
    }
  }

  private boolean isIssuedCorrectly(JWTClaimsSet claimsSet) {
    return claimsSet.getIssuer().equals(jwtConfiguration.getCognitoIdentityPoolUrl());
  }

  private boolean isIdToken(JWTClaimsSet claimsSet) {
    return claimsSet.getClaim("token_use").equals("id");
  }

  private static <T, U> List<U> convertList(List<T> from, Function<T, U> func) {
    return from.stream().map(func).collect(Collectors.toList());
  }
}

CognitoJwtAutoConfiguration

@Configuration
@Import(AWSConfig.class)
@ConditionalOnClass({AwsCognitoJwtAuthenticationFilter.class, AwsCognitoIdTokenProcessor.class})
public class CognitoJwtAutoConfiguration {

  private final AWSConfig jwtConfiguration;

  public CognitoJwtAutoConfiguration(AWSConfig jwtConfiguration) {
    this.jwtConfiguration = jwtConfiguration;
  }

  @Bean
  @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
  public CognitoJwtIdTokenCredentialsHolder awsCognitoCredentialsHolder() {
    return new CognitoJwtIdTokenCredentialsHolder();
  }

  @Bean
  public AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor() {
    return new AwsCognitoIdTokenProcessor();
  }

  @Bean
  public CognitoJwtAuthenticationProvider jwtAuthenticationProvider() {
    return new CognitoJwtAuthenticationProvider();
  }

  @Bean
  public AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter() {
    return new AwsCognitoJwtAuthenticationFilter(awsCognitoIdTokenProcessor());
  }

  @SuppressWarnings({"rawtypes", "unchecked"})
  @Bean
  public ConfigurableJWTProcessor configurableJWTProcessor() throws MalformedURLException {
    ResourceRetriever resourceRetriever =
        new DefaultResourceRetriever(CONNECTION_TIMEOUT, READ_TIMEOUT);
    // https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json.
    URL jwkSetURL = new URL(jwtConfiguration.getJwkUrl());
    // Creates the JSON Web Key (JWK)
    JWKSource keySource = new RemoteJWKSet(jwkSetURL, resourceRetriever);
    ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
    JWSKeySelector keySelector = new JWSVerificationKeySelector(RS256, keySource);
    jwtProcessor.setJWSKeySelector(keySelector);
    return jwtProcessor;
  }

  @Bean
  public AWSCognitoIdentityProvider awsCognitoIdentityProvider() {
    return AWSCognitoIdentityProviderClientBuilder.standard()
        .withRegion(Regions.EU_CENTRAL_1)
        .withCredentials(getCredentialsProvider())
        .build();
  }

  @Bean
  public AWSCredentialsProvider getCredentialsProvider() {
    return new ClasspathPropertiesFileCredentialsProvider();
  }
}

我想排除我的控制器 URL 被视为需要授权的端点。

基于视觉测试的控制器看起来像:

@RestController
@RequestMapping("/nutrition/api/")
class NutritionixApiController {

  private ProductFacadeImpl productFacadeImpl;

  public NutritionixApiController(
      ProductFacadeImpl productFacadeImpl) {
    this.productFacadeImpl = productFacadeImpl;
  }

  @GetMapping("/productDetails")
  public ResponseEntity<Set<RecipeIngredient>> productsDetails(@RequestParam String query) {
  //logic here
  }
}

我尝试在方法 configure(WebSecurity web) 中将 URL "/nutrition/api/**" 列入白名单 通过添加:

web.ignoring().antMatchers(GET, "/nutrition/api/**");

web.ignoring().antMatchers(GET, "/**");

但效果不理想。我对为什么 ignoring.antMatchers() 不工作有点困惑,所以我将不胜感激有关如何解决上述问题的建议。

编辑

我回到了主题,但结果相同。在 WebSecurityConfiguration 中,我注释掉了 @EnableGlobalMethodSecurity(prePostEnabled = true) 以尝试在没有 prePostEnabled = true 的情况下进行配置,但没有达到预期的效果。我对配置中忽略的端点 /auth 有同样的问题。 我在教程之后制作了图案,该教程正在运行并可在此处获得 click 但是我稍微重构了我的代码以摆脱使用 @Autowired 的字段注入,但没有在幕后进行彻底的更改和逻辑。

此外 class CustomAuthenticationProvider 看起来像:

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

  private final CognitoAuthenticationService cognitoService;

  @SuppressWarnings("unchecked")
  @Override
  public Authentication authenticate(Authentication authentication) {
    AuthenticationRequest authenticationRequest;

    if (authentication != null) {
      authenticationRequest = new AuthenticationRequest();
      Map<String, String> credentials = (Map<String, String>) authentication.getCredentials();
      authenticationRequest.setNewPassword(credentials.get(NEW_PASS_WORD_KEY));
      authenticationRequest.setPassword(credentials.get(PASS_WORD_KEY));
      authenticationRequest.setUsername(authentication.getName());

      SpringSecurityUser userAuthenticated = cognitoService.authenticate(authenticationRequest);
      if (userAuthenticated != null) {

        Map<String, String> authenticatedCredentials = new HashMap<>();
        authenticatedCredentials.put(ACCESS_TOKEN_KEY, userAuthenticated.getAccessToken());
        authenticatedCredentials.put(EXPIRES_IN_KEY, userAuthenticated.getExpiresIn().toString());
        authenticatedCredentials.put(ID_TOKEN_KEY, userAuthenticated.getIdToken());
        authenticatedCredentials.put(PASS_WORD_KEY, userAuthenticated.getPassword());
        authenticatedCredentials.put(REFRESH_TOKEN_KEY, userAuthenticated.getRefreshToken());
        authenticatedCredentials.put(TOKEN_TYPE_KEY, userAuthenticated.getTokenType());
        return new UsernamePasswordAuthenticationToken(
            userAuthenticated.getUsername(),
            authenticatedCredentials,
            userAuthenticated.getAuthorities());
      } else {
        return null;
      }
    } else {
      throw new UsernameNotFoundException("No application user for given username");
    }
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return authentication.equals(UsernamePasswordAuthenticationToken.class);
  }
}

老实说,我不知道还能做些什么来解决这个过滤器不工作的问题。将不胜感激。

虽然您指出了正确的忽略模式并且 Spring 安全实际上忽略了过滤器,但我认为它仍在执行,因为可能 Spring 正在安全链之外再次注册过滤器,因为您在 CognitoJwtAutoConfiguration.

中使用 @Bean 公开了过滤器

为避免该问题,请在您的代码中执行以下修改(基本上,请确保只有一个过滤器实例就位)。首先,在 WebSecurityConfiguration:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableTransactionManagement
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

  private CustomAuthenticationProvider authProvider;
  private AccountControllerExceptionHandler exceptionHandler;
  private static final String LOGIN_URL = "/auth/login";
  private static final String LOGOUT_URL = "/auth/signOut";

  @Autowired
  public WebSecurityConfiguration(
      CustomAuthenticationProvider authProvider,
      AccountControllerExceptionHandler exceptionHandler) {
    // Do not provide AwsCognitoJwtAuthenticationFilter() as instance filed any more
    this.authProvider = authProvider;
    this.exceptionHandler = exceptionHandler;
  }

  public WebSecurityConfiguration() {
    super(true);
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(authProvider).eraseCredentials(false);
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  @Override
  public void configure(WebSecurity web) {
    // TokenAuthenticationFilter will ignore the below paths
    web.ignoring().antMatchers("/auth");
    web.ignoring().antMatchers("/auth/**");
    web.ignoring().antMatchers("/v2/api-docs");
    web.ignoring().antMatchers(GET, "/nutrition/api/**");
    web.ignoring().antMatchers(GET, "/**");
    web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
  }

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
        .addFilterAfter(corsFilter(), ExceptionTranslationFilter.class)
        .exceptionHandling()
        .authenticationEntryPoint(new SecurityAuthenticationEntryPoint())
        .accessDeniedHandler(new RestAccessDeniedHandler())
        .and()
        .anonymous()
        .and()
        .sessionManagement()
        .sessionCreationPolicy(STATELESS)
        .and()
        .authorizeRequests()
        .antMatchers("/auth")
        .permitAll()
        .anyRequest()
        .authenticated()
        .and()
        // Instantiate a new instance of the filter
        .addFilterBefore(
            awsCognitoJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
        .formLogin(formLogin -> formLogin.loginProcessingUrl(LOGIN_URL).failureHandler(exceptionHandler))
        .logout(logout -> logout.permitAll().logoutUrl(LOGOUT_URL))
        .csrf(AbstractHttpConfigurer::disable);
  }

  private CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader(ORIGIN);
    config.addAllowedHeader(CONTENT_TYPE);
    config.addAllowedHeader(ACCEPT);
    config.addAllowedHeader(AUTHORIZATION);
    config.addAllowedMethod(GET);
    config.addAllowedMethod(PUT);
    config.addAllowedMethod(POST);
    config.addAllowedMethod(OPTIONS);
    config.addAllowedMethod(DELETE);
    config.addAllowedMethod(PATCH);
    config.setMaxAge(3600L);

    source.registerCorsConfiguration("/v2/api-docs", config);
    source.registerCorsConfiguration("/**", config);

    return new CorsFilter();
  }

  // It will also be possible to inject AwsCognitoIdTokenProcessor
  private AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter() {
    return new AwsCognitoJwtAuthenticationFilter(new AwsCognitoIdTokenProcessor());
  }
}

您还需要从 CognitoJwtAutoConfiguration 中删除不必要的内容:

@Configuration
@Import(AWSConfig.class)
@ConditionalOnClass({AwsCognitoJwtAuthenticationFilter.class, AwsCognitoIdTokenProcessor.class})
public class CognitoJwtAutoConfiguration {

  private final AWSConfig jwtConfiguration;

  public CognitoJwtAutoConfiguration(AWSConfig jwtConfiguration) {
    this.jwtConfiguration = jwtConfiguration;
  }

  @Bean
  @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
  public CognitoJwtIdTokenCredentialsHolder awsCognitoCredentialsHolder() {
    return new CognitoJwtIdTokenCredentialsHolder();
  }

  /* No longer needed
  @Bean
  public AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor() {
    return new AwsCognitoIdTokenProcessor();
  }*/

  @Bean
  public CognitoJwtAuthenticationProvider jwtAuthenticationProvider() {
    return new CognitoJwtAuthenticationProvider();
  }

  /* No longer needed
  @Bean
  public AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter() {
    return new AwsCognitoJwtAuthenticationFilter(awsCognitoIdTokenProcessor());
  }*/

  @SuppressWarnings({"rawtypes", "unchecked"})
  @Bean
  public ConfigurableJWTProcessor configurableJWTProcessor() throws MalformedURLException {
    ResourceRetriever resourceRetriever =
        new DefaultResourceRetriever(CONNECTION_TIMEOUT, READ_TIMEOUT);
    // https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json.
    URL jwkSetURL = new URL(jwtConfiguration.getJwkUrl());
    // Creates the JSON Web Key (JWK)
    JWKSource keySource = new RemoteJWKSet(jwkSetURL, resourceRetriever);
    ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
    JWSKeySelector keySelector = new JWSVerificationKeySelector(RS256, keySource);
    jwtProcessor.setJWSKeySelector(keySelector);
    return jwtProcessor;
  }

  @Bean
  public AWSCognitoIdentityProvider awsCognitoIdentityProvider() {
    return AWSCognitoIdentityProviderClientBuilder.standard()
        .withRegion(Regions.EU_CENTRAL_1)
        .withCredentials(getCredentialsProvider())
        .build();
  }

  @Bean
  public AWSCredentialsProvider getCredentialsProvider() {
    return new ClasspathPropertiesFileCredentialsProvider();
  }
}

我认为这个 SO question 也可能有所帮助。