首页 > 解决方案 > MvC中spring security中每个路径/路由的不同AuthenticationManager

问题描述

前言

由于 StackOverflow 上已经有很多关于此的问题,我首先要确保这不是重复和区分。

这是关于

所以问题不在于:

案子

假设一个有 2 个自定义 AuthenticationProviders:CATApiTokenProviderDOGApiTokenProvider。我们不谈论 AOuth/JWT/Basic/Form 提供程序是设计使然,因为它们提供了快捷方式。

现在我们有 2 个 REST API 端点/dog/endpoint/cat/endpoint.

问题

今天如何使用 Spring Security 5.5 正确实现这一点:

因此,无法使用 cat 令牌进行身份验证,也无法使用/dog/endpointdog 令牌进行身份验证/cat/endpoint

我的想法/方法

a) 我知道,由于我有自定义 Cat/Dog 过​​滤器,因此可以AuthenticationManagerResolver在创建 bean 时使用并将一个实例传递给过滤器。这个解析器可能看起来像

  public AuthenticationManagerResolver<HttpServletRequest> resolver()
  {
    return request -> {
      if (request.getPathInfo().startsWith("/dog/")) {
        try {
          return ???;
        } catch (Exception exception) {
          log.error(exception);
        }
      }
      if (request.getPathInfo().startsWith("/cat/")) {
        try {
          return ???;
        } catch (Exception exception) {
          log.error(exception);
        }
      }
    };
  }

有两个问题是:

b) 以某种方式实例化 2 个不同的 AuthenticationManager,然后使用 SecurityConfiguration 将它们分配给不同的匹配器。

两个问题:

http.authorizeRequests()
      .antMatchers("/dog/**")
      .?

标签: restspring-mvcspring-security

解决方案


您可以发布多个过滤器链或将您自己AuthenticationFilter的过滤器链与AuthenticationManagerResolver

您可以使用AuthenticationManagerResolver返回不同AuthenticationManager的 s。从 Spring Security 5.4.0 开始,我们不再需要扩展WebSecurityConfigurerAdapter来配置我们SecurityFilterChain的,您可以改为定义一个SecurityFilterChain类型的 bean。

我将详细介绍您自己的接线AuthenticationFilter

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
        http.authorizeRequests((authz) -> authz
                .anyRequest().authenticated());
        http.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    private AuthenticationFilter apiAuthenticationFilter() {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter(new ApiAuthenticationManagerResolver(), new BasicAuthenticationConverter());
        authenticationFilter.setSuccessHandler((request, response, authentication) -> {});
        return authenticationFilter;
    }

    public static class ApiAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {

        private final Map<RequestMatcher, AuthenticationManager> managers = Map.of(
                new AntPathRequestMatcher("/dog/**"), new DogAuthenticationProvider()::authenticate,
                new AntPathRequestMatcher("/cat/**"), new CatAuthenticationProvider()::authenticate
        );

        @Override
        public AuthenticationManager resolve(HttpServletRequest request) {
            for (Map.Entry<RequestMatcher, AuthenticationManager> entry : managers.entrySet()) {
                if (entry.getKey().matches(request)) {
                    return entry.getValue();
                }
            }
            throw new IllegalArgumentException("Unable to resolve AuthenticationManager");
        }
    }

    public static class DogAuthenticationProvider implements AuthenticationProvider {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            if (authentication.getName().endsWith("_dog")) {
                return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
                        AuthorityUtils.createAuthorityList("ROLE_DOG"));
            }
            throw new BadCredentialsException("Username should end with _dog");
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return true;
        }

    }

    public static class CatAuthenticationProvider implements AuthenticationProvider {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            if (authentication.getName().endsWith("_cat")) {
                return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
                        AuthorityUtils.createAuthorityList("ROLE_CAT"));
            }
            throw new BadCredentialsException("Username should end with _cat");
        }

        @Override
        public boolean supports(Class<?> authentication) {
            return true;
        }

    }
}

在上面的示例中,我们有两个AuthenticationProviders,一个用于 cat,另一个用于 dog。它们在. AntPathRequestMatcher_ /dog/**_ 不需要为每只狗和猫定义一个,因为它们具有相同的接口。/cat/**ApiAuthenticationManagerResolverAuthenticationManagerAuthenticationProvider/Manager

然后将ApiAuthenticationManagerResolver其连接到AuthenticationFilter您的过滤器链中。

您还可以为每个端点定义两个不同的过滤器链,如下所示:

@Bean
public SecurityFilterChain dogApiSecurity(HttpSecurity http) throws Exception {
    http.requestMatchers((matchers) -> matchers
                .antMatchers("/dog/**"));
    http.authorizeRequests((authz) -> authz
            .anyRequest().authenticated());
    http.httpBasic();
    http.authenticationProvider(new DogAuthenticationProvider());
    return http.build();
}

@Bean
public SecurityFilterChain catApiSecurity(HttpSecurity http) throws Exception {
    http.requestMatchers((matchers) -> matchers
                .antMatchers("/cat/**"));
    http.authorizeRequests((authz) -> authz
            .anyRequest().authenticated());
    http.httpBasic();
    http.authenticationProvider(new CatAuthenticationProvider());
    return http.build();
}

请在定义多个过滤器链时,排序很重要,@Order在这些场景中使用注释。

当您这样做时,http.requestMatcher(new AntPathRequestMatcher("/endpoint/**"));您是在告诉 Spring Security 仅在请求与该路径匹配时才调用过滤器链。

Spring Security 的存储库中还有一张票,提供了一个AuthenticationManagerResolver接受的实现,Map<RequestMatcher, AuthenticationManager>如果您认为它有道理,那就太好了,在那里竖起大拇指。


推荐阅读