使用多个 spring 安全配置并根据调用应用它们 url

Use several spring security configuration and apply them according to the calling url

我目前有一个后端应用程序,它实现了一个非常简单的 Spring 基于登录名/密码的安全性,必须在 http headers.

中添加

我还有一个使用 OKTA 作为提供者并使用 JWT 令牌的前端。

我现在想让专用于前端应用程序的端点使用 JWT 令牌系统,而所有其他端点使用当前的 login/password 系统。

我可以使我的应用程序与 OKTA 配置或登录/密码配置一起工作,但我不能让两者一起工作。

查看有关堆栈溢出的不同消息,我已经实现了双重配置,但它始终是应用的第一个。第二个被简单地忽略并且允许边界的端点没有任何令牌或登录名/密码

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Configuration
@Order(1)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) throws Exception {

        http.cors();
        http.csrf().disable();

        http
                .authorizeRequests().antMatchers("/api/v1/end-point/**").authenticated()
                .and().oauth2ResourceServer().jwt();

        Okta.configureResourceServer401ResponseBody(http);
    }
}

@Configuration
@Order(2)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {

    @Value("${http.auth-app-id-header-name}")
    private String appIdRequestHeaderName;
    @Value("${http.auth-api-key-header-name}")
    private String apiKeyRequestHeaderName;
    private final AuthenticationManager authenticationManager;

    @Autowired
    public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
        super();
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors();
        http.csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().addFilter(initAuthenticationFilter())
                .antMatcher("/api/v1/tools/**")
                .authorizeRequests().anyRequest().authenticated();
    }

    private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
        RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
                apiKeyRequestHeaderName);
        requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
        requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
        return requestHeaderAuthenticationFilter;
    }
}

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

在此代码中,即使我调用 /api/v1/tools 也永远不会使用配置 2 如果我删除配置 1,则会应用配置 2。

你能帮我理解我做错了什么吗?


编辑 1:

在 Eleftheria Stein-Kousathana 的帮助和建议下,我更改了配置(并添加了 Swagger 白名单配置)

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

private static final String[] AUTH_WHITELIST = {
        "/v2/api-docs",
        "/swagger-resources/configuration/ui",
        "/swagger-resources",
        "/swagger-resources/configuration/security",
        "/swagger-ui.html",
        "/webjars/**"
};

@Configuration
@Order(1)
public static class SwaggerConfigurationAdapter extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {

        System.out.println("Loading configuration 1");

        http.cors();
        http.csrf().disable();

        http
                .requestMatchers(matchers -> matchers.antMatchers(AUTH_WHITELIST))
                .authorizeRequests(authz -> {
                    authz.anyRequest().permitAll();
                });
    }
}

@Configuration
@Order(2)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {

        System.out.println("Loading configuration 2");

        http.cors();
        http.csrf().disable();

        http
                .requestMatchers(matchers -> matchers.antMatchers("/api/v1/end-point/**"))
                    .authorizeRequests(authz -> {
                        try {
                            authz.anyRequest().authenticated().and().oauth2ResourceServer().jwt();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    });

        Okta.configureResourceServer401ResponseBody(http);
    }
}

@Configuration
@Order(3)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {

    @Value("${algo.http.auth-app-id-header-name}")
    private String appIdRequestHeaderName;
    @Value("${algo.http.auth-api-key-header-name}")
    private String apiKeyRequestHeaderName;
    private final AuthenticationManager authenticationManager;

    @Autowired
    public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
        super();
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        System.out.println("Loading configuration 3");

        http.cors();
        http.csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().addFilter(initAuthenticationFilter())
                .requestMatchers(matchers -> matchers.antMatchers("/api/**"))
                .authorizeRequests(authz -> {
                    try {
                        authz.anyRequest().authenticated();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
    }

    private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
        RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
                apiKeyRequestHeaderName);
        requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
        requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
        return requestHeaderAuthenticationFilter;
    }
}

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

我觉得我离成功很近了

但现在我有以下错误:

每次我在 swagger 下请求页面或调用我的 api 时,我的网络浏览器都会要求我输入登录名/密码。

如果我取消,我仍然可以在 Swagger UI 上导航并调用“/api/v1/end-point/**”。 每个登录名/密码都被拒绝,即使它们在配置 3 中有效。

如果我不填写登录名/密码并调用“/api/**”的任何路由,我会收到以下错误:

2021-07-23 14:49:16.642 [http-nio-8081-exec-9] INFO  c.c.a.a.c.CorrelationIdLoggingAspect - Calling api.controller.endpoint.getActivities executed in 197ms.
2021-07-23 14:49:22.247 [http-nio-8081-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/secret] threw exception [Filter execution threw an exception] with root cause
java.lang.WhosebugError: null
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
    at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
    at jdk.internal.reflect.GeneratedMethodAccessor220.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
    at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)

如果我正确理解了您的程序草图和描述,请让我尝试进行总结。您的应用程序寻求支持以下内容:

  1. 向 public 提供 swagger UI 并允许浏览 API 定义。
  2. 将经过身份验证的 API 端点(以 /api/v1/end-point 为前缀)与来自另一个客户端(不是 swagger)的 Okta-provided JWT 一起使用。
  3. 使用经过身份验证的 API 端点(以 /api 为前缀,但不是 /api/v1/end-point)通过 swagger 将 username/password 作为 headers.

注意:我不打算在这里介绍如何将 Okta 配置为提供者,也不打算介绍如何配置 swagger。如果这些步骤没有正确完成,您可能仍然会遇到问题。

就 Spring 安全性而言,我认为您的主要问题是您似乎没有为 header-based 配置配置身份验证提供程序。这通常通过 UserDetailsService 完成(参见 UserDetailsService 部分):

@Bean
public UserDetailsService userDetailsService() {
    // @formatter:off
    UserDetails userDetails = User.builder()
        .username("api-client")
        .password("{noop}my-api-key")
        .roles("USER")
        .build();
    // @formatter:on

    return new InMemoryUserDetailsManager(userDetails);
}

这显然不是用于生产的示例。但重要的一点是,您必须为 Spring 安全提供一种方法来确定凭据是否有效。无论是用户的 username/password,还是 API 客户的 appId/apiKey,principal(参见 Authentication)都是通过 UserDetailsService 查找的,并且然后凭据由 AuthenticationProvider.

验证

不幸的是,built-in RequestHeaderAuthenticationFilter 建立在假定您是 pre-authenticated 的不同类型的提供商之上,因此与 username/password 身份验证不兼容。虽然您可以通过使一种类型的提供程序适应另一种类型来解决此问题,但更直接(至少出于示例目的)使 UsernamePasswordAuthenticationFilter 适应您的用例。例如:

private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
    UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
        @Override
        protected String obtainUsername(HttpServletRequest request) {
            return request.getHeader(getUsernameParameter());
        }

        @Override
        protected String obtainPassword(HttpServletRequest request) {
            return request.getHeader(getPasswordParameter());
        }

        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            super.successfulAuthentication(request, response, chain, authResult);
            chain.doFilter(request, response);
        }
    };
    usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
    usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
    usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
    usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
    usernamePasswordAuthenticationFilter.setPostOnly(false);
    usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
        // Do nothing
    });
    return usernamePasswordAuthenticationFilter;
}

如果您有兴趣让这种感觉更加 built-in,请查看 custom DSLs 上的文档部分。

我还建议您覆盖 WebSecurityConfigurerAdapter 中的 configure(WebSecurity web) 方法来执行您的 permitAll 并将配置压缩为两个,同时消除 /api/**模式,因此默认情况下您的整个应用程序都是安全的。这是一个完整的示例(省略任何 Okta-specific 代码),它还演示了 Spring 安全 lambda DSL 的正确用法:

@Configuration
public class SecurityConfiguration {

    private static final String[] AUTH_WHITELIST = {
        "/v2/api-docs",
        "/swagger-resources/configuration/ui",
        "/swagger-resources",
        "/swagger-resources/configuration/security",
        "/swagger-ui.html",
        "/webjars/**"
    };

    @Order(1)
    @EnableWebSecurity
    public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                .antMatcher("/api/v1/end-point/**")
                .authorizeRequests((authorizeRequests) ->
                    authorizeRequests
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .sessionManagement((sessionManagement) ->
                    sessionManagement
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .cors(withDefaults())
                .csrf(CsrfConfigurer::disable);
            // @formatter:on
        }

    }

    @Order(2)
    @EnableWebSecurity
    public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {

        @Value("${algo.http.auth-app-id-header-name}")
        private String appIdRequestHeaderName;

        @Value("${algo.http.auth-api-key-header-name}")
        private String apiKeyRequestHeaderName;

        @Override
        public void configure(WebSecurity web) {
            web.ignoring().antMatchers(AUTH_WHITELIST);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                .addFilterAt(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests((authorizeRequests) ->
                    authorizeRequests
                        .anyRequest().authenticated()
                )
                .sessionManagement((sessionManagement) ->
                    sessionManagement
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .cors(withDefaults())
                .csrf(CsrfConfigurer::disable);
            // @formatter:on
        }

        private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
            UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
                @Override
                protected String obtainUsername(HttpServletRequest request) {
                    return request.getHeader(getUsernameParameter());
                }

                @Override
                protected String obtainPassword(HttpServletRequest request) {
                    return request.getHeader(getPasswordParameter());
                }

                @Override
                protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
                    super.successfulAuthentication(request, response, chain, authResult);
                    chain.doFilter(request, response);
                }
            };
            usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
            usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
            usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
            usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
            usernamePasswordAuthenticationFilter.setPostOnly(false);
            usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                // Do nothing
            });
            return usernamePasswordAuthenticationFilter;
        }

        @Bean
        public UserDetailsService userDetailsService() {
            // @formatter:off
            UserDetails userDetails = User.builder()
                .username("api-client")
                .password("{noop}my-api-key")
                .roles("USER")
                .build();
            // @formatter:on

            return new InMemoryUserDetailsManager(userDetails);
        }

    }

}

最后说明: 需要注意的是,我包括了禁用 CSRF,您已经做到了。如果您不打算在带有会话的 Web 浏览器中使用此应用程序,则这只是一个 reasonable thing to do。由于我将两种配置都标记为无状态(您的 Okta+JWT 示例不是),这似乎是合理的。然而,大多数时候,您真的不想禁用 CSRF 保护,尤其是当原因是“我不知道如何让我的 UI 应用程序在启用 CSRF 的情况下工作。”

首先非常感谢您的帮助。 我花时间回复是因为我想了解你的回答。

你对我要实现的草图的描述是正确的。 通过您的配置,我现在可以在没有任何 login/password.

的情况下访问 Swagger

第一个配置 (OKTA) 工作正常,我认为最后一个配置(登录名/密码)也可以。

我现在在尝试访问路线时遇到最后一个错误 受登录名和密码保护。 我面临 Spring 抛出“org.springframework.security.authentication.ProviderNotFoundException:找不到 org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken 的 AuthenticationProvider”异常的问题。

我正在寻求解决此问题,我认为此后一切都会正常。

让我谦虚地指出 setter 方法 :

requestHeaderAuthenticationFilter.setPrincipalRequestHeader(appIdRequestHeaderName);
requestHeaderAuthenticationFilter.setCredentialsRequestHeader(apiKeyRequestHeaderName);
        

无法访问,我一直通过构造函数设置它们。

RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName, apiKeyRequestHeaderName)

非常感谢您的所有回答。 感谢您的帮助,我们找到了解决方案。

这是帮助所有需要和我们做同样事情的人的最终代码。

安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String[] AUTH_WHITELIST = {
            "/v2/api-docs",
            "/swagger-resources/configuration/ui",
            "/swagger-resources",
            "/swagger-resources/configuration/security",
            "/swagger-ui.html",
            "/webjars/**"
    };

    @Order(1)
    @Configuration
    public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {

        protected void configure(HttpSecurity http) throws Exception {


            http
                    .antMatcher("/api/v1/end-point/**")
                    .authorizeRequests((authz) -> authz.anyRequest().authenticated())
                    .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                    .sessionManagement((sessionManagement) ->
                            sessionManagement
                                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    )
                    .cors(withDefaults())
                    .csrf(CsrfConfigurer::disable);

            Okta.configureResourceServer401ResponseBody(http);
        }
    }

    @Order(2)
    @Configuration
    public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {

        @Value("${http.app-id-header-name}")
        private String appIdRequestHeaderName;
        @Value("${http.api-key-header-name}")
        private String apiKeyRequestHeaderName;

        private final AuthenticationManager authenticationManager;

        @Autowired
        public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
            super();
            this.authenticationManager = authenticationManager;
        }

        @Override
        public void configure(WebSecurity web) {
            web.ignoring().antMatchers(AUTH_WHITELIST);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {

            http
                    .addFilterAt(initAuthenticationFilter(), UsernameRequestHeaderAuthenticationFilter.class)
                    .authorizeRequests((authorizeRequests) ->
                            authorizeRequests
                                    .anyRequest().authenticated()
                    )
                    .sessionManagement((sessionManagement) ->
                            sessionManagement
                                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    )
                    .cors(withDefaults())
                    .csrf(CsrfConfigurer::disable);
        }

        private UsernameRequestHeaderAuthenticationFilter initAuthenticationFilter() throws Exception {
            UsernameRequestHeaderAuthenticationFilter usernameRequestHeaderAuthenticationFilter = new UsernameRequestHeaderAuthenticationFilter();
            usernameRequestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
            usernameRequestHeaderAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
            usernameRequestHeaderAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
            usernameRequestHeaderAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
            usernameRequestHeaderAuthenticationFilter.setPostOnly(false);
            usernameRequestHeaderAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                // Do nothing
            });
            return usernameRequestHeaderAuthenticationFilter;
        }
    }
}

身份验证过滤器:

public class UsernameRequestHeaderAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    protected String obtainUsername(HttpServletRequest request) {
        return request.getHeader(getUsernameParameter());
    }

    @Override
    protected String obtainPassword(HttpServletRequest request) {
        return request.getHeader(getPasswordParameter());
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);
        chain.doFilter(request, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {

        super.unsuccessfulAuthentication(request, response, failed);
    }
}

我们还调整了 AuthenticationManager 以使用 UserAuthorities

再次感谢大家