【问题标题】:How to overcome intermittent Scope 'session' is not active for the current thread for OauthClientContext?如何克服 OauthClientContext 的当前线程的间歇性范围“会话”不活动?
【发布时间】:2019-08-22 23:11:33
【问题描述】:

我正在尝试在 Spring Framework 4.2.3 上使用 Spring Security 3.2.5 在 Spring Boot 1.3.0 应用程序中实现 OpenId Connect 登录。实现与这个问题非常相似:Protecting REST API with OAuth2: Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active,除了我已经为RequestContextFilter 实现了建议的bean。

@Configuration
@EnableOAuth2Client
public class OpenIdConnectConfig {
    @Value("${oidc.clientId}")
    private String clientId;

    @Value("${oidc.clientSecret}")
    private String clientSecret;

    @Value("${oidc.accessTokenUrl}")
    private String accessTokenUri;

    @Value("${oidc.userAuthorizationUri}")
    private String userAuthorizationUri;

    @Value("${oidc.redirectUri}")
    private String redirectUri;

    @Value("#{'${oidc.scopes}'.split(',')}")
    private List<String> oidcScopes;

    @Bean
    public OAuth2ProtectedResourceDetails openIdResourceDetails() {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setAccessTokenUri(accessTokenUri);
        details.setUserAuthorizationUri(userAuthorizationUri);
        details.setClientAuthenticationScheme(AuthenticationScheme.form);
        details.setScope(oidcScopes);
        details.setPreEstablishedRedirectUri(redirectUri);
        details.setUseCurrentUri(false);
        return details;
    }

    @Bean(name = "my.company.ui.security.OpenIdRestTemplate")
    // ToDo: fix org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active for the current thread
    public OAuth2RestTemplate OpenIdRestTemplate(OAuth2ClientContext clientContext) {
        return new OAuth2RestTemplate(openIdResourceDetails(), clientContext);
    }

    @Bean
    public RequestContextListener requestContextListener() {
        return new RequestContextListener();
    }
}

异常堆栈跟踪也类似

Caused by:
org.springframework.beans.factory.BeanCreationException: 
  Error creating bean with name 'scopedTarget.oauth2ClientContext':
  Scope 'session' is not active for the current thread; consider 
  defining a scoped proxy for this bean if you intend to refer to it
  from a singleton; 
nested exception is java.lang.IllegalStateException: No thread-bound
  request found: Are you referring to request attributes outside of an
  actual web request, or processing a request outside of the originally 
  receiving thread? If you are actually operating within a web request
  and still receive this message, your code is probably running outside 
  of DispatcherServlet/DispatcherPortlet: In this case, use 
  RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:355)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:187)
    at com.sun.proxy.$Proxy78.getAccessToken(Unknown Source)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:169)
    at my.company.ui.security.OpenIdConnectFilter.attemptAuthentication(OpenIdConnectFilter.java:118)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:211)
    at my.company.ui.security.OpenIdConnectFilter.doFilter(OpenIdConnectFilter.java:93)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter(OAuth2ClientContextFilter.java:60)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at my.company.ui.security.UserCookieFilter.doFilter(UserCookieFilter.java:29)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at my.company.server.filter.UncaughtExceptionRequestFilter.doFilter(UncaughtExceptionRequestFilter.java:45)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at com.rmn.commons.web.metrics.AbstractInstrumentedFilter.doFilter(AbstractInstrumentedFilter.java:143)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:167)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:80)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:215)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
    at org.eclipse.jetty.server.Server.handle(Server.java:499)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:311)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
    at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:544)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
    at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    at org.springframework.web.context.request.SessionScope.get(SessionScope.java:91)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340)
    ... 48 more
Caused by:
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    at org.springframework.web.context.request.SessionScope.get(SessionScope.java:91)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:187)
    at com.sun.proxy.$Proxy78.getAccessToken(Unknown Source)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:169)
    at my.company.ui.security.OpenIdConnectFilter.attemptAuthentication(OpenIdConnectFilter.java:118)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:211)
    at my.company.ui.security.OpenIdConnectFilter.doFilter(OpenIdConnectFilter.java:93)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter(OAuth2ClientContextFilter.java:60)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at my.company.ui.security.UserCookieFilter.doFilter(UserCookieFilter.java:29)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at my.company.server.filter.UncaughtExceptionRequestFilter.doFilter(UncaughtExceptionRequestFilter.java:45)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at com.rmn.commons.web.metrics.AbstractInstrumentedFilter.doFilter(AbstractInstrumentedFilter.java:143)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:167)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:80)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:215)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
    at org.eclipse.jetty.server.Server.handle(Server.java:499)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:311)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
    at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:544)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
    at java.lang.Thread.run(Thread.java:748)
Powered by Jetty://

您可以清楚地看到过滤器调用是 Spring Security FilterChainProxy 的一部分,所以我不知道如何理解错误消息的建议,即“您的代码可能在 DispatcherServlet/DispatcherPortlet 之外运行”。另外,我尝试为建议的替代方案 RequestContextFilter 添加一个 bean,并引发了相同的异常。

执行身份验证的过滤器(删除了一些异常处理和用户处理代码):

public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {

    public OpenIdConnectFilter(
            RequestMatcher requiresAuthenticationRequestMatcher,
            AuthenticationService authenticationService
    ) {
        super(requiresAuthenticationRequestMatcher);
        setAuthenticationManager(new NoopAuthenticationManager());
    }

    @SuppressWarnings("RedundantThrows") // Matching overridden method
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response
    ) throws AuthenticationException, IOException, ServletException {

        // Required parameters (one-time access code, state) are retrieved from the context
        OAuth2AccessToken oAuth2AccessToken = restTemplate.getAccessToken();

        // Process the token, get the user details, return an Authentication object.
    }

    public void setRestTemplate(OAuth2RestTemplate restTemplate) {
        this.restTemplate = restTemplate;

    }

    private static class NoopAuthenticationManager implements AuthenticationManager {

        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            throw new UnsupportedOperationException("No authentication should be done with this AuthenticationManager");
        }

    }


    private static final Logger LOGGER = LoggerFactory.getLogger(OpenIdConnectFilter.class);

    @Value("${oidc.clientId}")
    private String clientId;

    @Value("${oidc.issuer}")
    private String issuer;

    @Value("${oidc.jwt.jwk.url}")
    private String jwkUrl;

    private final AuthenticationService authenticationService;

    private OAuth2RestTemplate restTemplate;
}

以及设置 Spring Security FilterProxyChain 的 Security Config:

@Configuration
@EnableWebSecurity
@EnableOAuth2Client
public class SecurityConfig extends WebSecurityConfigurerAdapter{

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

        http
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .and()
            .csrf()
                .disable()
            .authorizeRequests()
                .expressionHandler(securityExpressionHandler)
                .antMatchers("/asset/**").access("permitAll")
                .antMatchers("/ws/ssoEnabled").access("permitAll")
                .antMatchers("/**").access("hasRole('ROLE_USER') or hasRole('ROLE_TOKEN_ACCESS')")
                .and()
            .httpBasic()
                .authenticationEntryPoint(ajaxAwareLoginUrlAuthenticationEntryPoint)
                .and()
            // Handles unauthenticated requests, catching UserRedirectRequiredExceptions and redirecting to OAuth provider
            .addFilterAfter(new OAuth2ClientContextFilter(), SecurityContextPersistenceFilter.class)
            // Handles the oauth callback, exchanging the one-time code for a durable token
            .addFilterAfter(openIdConnectFilter, OAuth2ClientContextFilter.class)
            .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/logincheck")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(ajaxAwareAuthenticationSuccessHandler)
                .failureHandler(ajaxAwareAuthenticationFailureHandler)
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
                .and()
            .rememberMe()
                .rememberMeServices(rememberMeServices)
                // Even though this key has been added directly to the rememberMeServices instance, the RememberMeConfigurer
                // can instantiate a new RememberMeServices with a made-up key if the same key is not provided.
                .key("the key value")
        ;

        // We do not configure a bean for the SessionAuthenticationStrategy. We want to use the Spring default strategy,
        // which is configured by the above builder chain. In order to share the correct, configured instance with our
        // custom OpenIdConnectFilter, we first tell the builder to perform the configuration (normally this would be
        // done long after this method returns)...
        http.getConfigurer(SessionManagementConfigurer.class).init(http);
        // ... then we get the shared object by interface (SessionAuthenticationStrategy) class name...
        final SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
        // ... then set it in our custom filter.
        openIdConnectFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
    }
}

最后一个问题:我在我的机器上运行时遇到了这个错误。然后,添加RequestContextListener 解决了它。将其部署到我们的测试环境时,异常消息再次出现。然而,几个小时或几天后,有时在 CI/CD 管道重新部署相同版本后,问题自行解决,OpenID 集成按预期工作,直到我们进行下一次更改、错误修复或改进,然后它通常会再次发生类似的短间隔。


问题:

  1. DispatcherServlet/DispatcherPortlet 在 Spring Boot 中运行时是否是一个因素?
  2. 如何确定应该使用RequestContextFilter 还是RequestContextListener?有实际区别吗?在我看来,(链接的)文档在这里没有多大帮助。

    通过 LocaleContextHolder 和 RequestContextHolder 向当前线程公开请求的 Servlet 侦听器。在 web.xml 中注册为监听器。

    另外,Spring 的 RequestContextFilter 和 Spring 的 DispatcherServlet 也向当前线程公开相同的请求上下文。与此侦听器相比,那里提供了高级选项(例如“threadContextInheritable”)。

    此侦听器主要用于第三方 servlet,例如JSF FacesServlet。在 Spring 自己的 web 支持范围内,DispatcherServlet 的处理就完全够用了。

  3. 在 Spring Boot 应用程序中,RequestContextFilter/Listener 的 Bean 应该在哪里定义?我们没有web.xml。 (我的理解是 web.xml 仅适用于 Spring MVC - 如果有错误请纠正我。)
  4. 什么可能导致有时找到上下文?为什么我们的应用在经历了一段时间的故障后还能运行?

【问题讨论】:

  • 当你注册监听器时这个堆栈跟踪吗?您实际上在哪里注册这些过滤器/侦听器?你的听众的顺序是什么? (注意: 在 Boot 中也支持 web.xml,它只是可选的:baeldung.com/spring-xml-vs-java-config)
  • 注意:如果您注册过滤器而不是监听器,您会在堆栈跟踪中看到(或看不到,取决于过滤器顺序)。
  • 堆栈跟踪来自任何命中登录过滤器的请求。我为过滤器和配置添加了附加代码。当您说“注册”时,除了声明 @Bean 之外,还有什么其他需要做的吗?因为这就是我发现的所有示例和类似的 SO 问题。感谢您提供有关 web.xml 的提示,我将更仔细地阅读那篇文章,尽管它似乎没有提到 Spring Boot。堆栈跟踪似乎不包含来自过滤器或侦听器的任何行。
  • 它不应该真正包含来自侦听器的行(如果已注册),但应该包含来自过滤器的行(如果已注册)。我认为您实际上并不需要侦听器 bean(不要介意答案),但您确实需要在 configure 中使用侦听器或过滤器 - 并且过滤器应该在 OAuth 之前触发。
  • 堆栈跟踪不包含来自RequestContextFilter 的任何行;似乎OncePerRequestFilter(父类)的每个堆栈跟踪出现都由另一个过滤器的doFilterInternal 方法调用紧随其后。我在 SecurityConfig configure 方法中添加了一个显式的 .addFilterAfter(requestContextFilter, SecurityContextPersistenceFilter.class) 行并进行了调试:我可以看到 RequestContextFilter 由 Spring Boot 执行一次,然后由 FilterChainProxy 中的 Spring Security 执行一次 - 至少在本地运行时是这样。下次会尝试部署

标签: java spring spring-boot spring-security spring-security-oauth2


【解决方案1】:
public class ContextAwareCallable<T> implements Callable<T> {

    private Callable<T> task;
    private RequestAttributes context;

    public ContextAwareCallable(Callable<T> task, RequestAttributes context) {
        this.task = task;
        this.context = cloneRequestAttributes(context);
    }

    @Override
    public T call() throws Exception {
        if (context != null) {
            RequestContextHolder.setRequestAttributes(context);

        }
        try {
            return task.call();
        } finally {
            RequestContextHolder.resetRequestAttributes();
        }
    }

    private RequestAttributes cloneRequestAttributes(RequestAttributes requestAttributes) {
        RequestAttributes clonedRequestAttribute = null;
        try {
            clonedRequestAttribute =
                    new ServletRequestAttributes(((ServletRequestAttributes) requestAttributes).getRequest(),
                    ((ServletRequestAttributes) requestAttributes).getResponse());
            if (requestAttributes.getAttributeNames(RequestAttributes.SCOPE_REQUEST).length > 0) {
                for (String name : requestAttributes.getAttributeNames(RequestAttributes.SCOPE_REQUEST)) {
                    clonedRequestAttribute.setAttribute(name,
                            requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST),
                            RequestAttributes.SCOPE_REQUEST);
                }
            }
            return clonedRequestAttribute;
        } catch (Exception e) {
            return requestAttributes;
        }
    }
}

然后创建执行器

public class ContextAwarePoolExecutor extends ThreadPoolTaskExecutor {

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes()));
    }

    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        return super.submitListenable(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes()));
    }

【讨论】:

    【解决方案2】:

    进一步调查,我发现RequestContextFilter 在本地运行时肯定在Oauth2ClientContextFilter 和OpenIdConnectFilter 之前执行。我决定尝试一下来自 cmets 的 M. Prokhorov 的建议,并将RequestContextFilter 注册到 Spring Security 的过滤器中;它扩展了 OncePerRequestFilter 以防止它执行多次。

    总而言之,我进行了以下修改:

    从 OpenIdConnectConfig 中删除了 RequestContextLister bean 的声明。这是在应用程序的其他地方注册的,在我验证正在接受配置扫描的父模块中。

    @Bean
    public ServletListenerRegistrationBean<RequestContextListener> requestContextListener() {
        return new ServletListenerRegistrationBean<>(new RequestContextListener());
    }
    

    将Oauth2ClientContextFilter 移动到 bean 声明中。在这个过滤器中似乎没有任何东西依赖于作为 Spring bean 填充 - 在测试中,客户端配置附加到 UserRedirectRequiredException,这个过滤器处理 之前 OAuth2RestTemplate (它确实有客户端配置 OAuth2ProtectedResourceDetails 由 Spring 注入其中)抛出它。但是,如果将来的 Spring 版本会更改此过滤器、它处理的异常以及 OAuth 客户端详细信息之间的相互作用,这感觉就像是防御性编程。


    在SecurityConfig.java 中的WebSecurityConfigurerAdapter.configure() 调用中将过滤器添加到HttpSecurity 对象。该类的最终代码如下所示:

    @Configuration
    @EnableWebSecurity
    @EnableOAuth2Client
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
    
        @Override
        @SuppressWarnings("unchecked")
        protected void configure(HttpSecurity http)
                throws Exception {
    
            http
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .csrf()
                    .disable()
                .authorizeRequests()
                    .expressionHandler(securityExpressionHandler)
                    .antMatchers("/asset/**").access("permitAll")
                    .antMatchers("/ws/ssoEnabled").access("permitAll")
                    .antMatchers("/**").access("hasRole('ROLE_USER') or hasRole('ROLE_TOKEN_ACCESS')")
                    .and()
                .httpBasic()
                    .authenticationEntryPoint(ajaxAwareLoginUrlAuthenticationEntryPoint)
                    .and()
    
                // OAuth filters
                // Explicitly add a RequestContextFilter ahead of the Oauth filters to facilitate retrieval of a session-scoped oauth2ClientContext object
                .addFilterAfter(requestContextFilter, SecurityContextPersistenceFilter.class)
                // Handles unauthenticated requests, catching UserRedirectRequiredExceptions and redirecting to OAuth provider
                .addFilterAfter(oAuth2ClientContextFilter, RequestContextFilter.class)
                // Handles the oauth callback, exchanging the one-time code for a durable token
                .addFilterAfter(openIdConnectFilter, OAuth2ClientContextFilter.class)
    
                .formLogin()
                    .loginPage("/login")
                    .loginProcessingUrl("/logincheck")
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .successHandler(ajaxAwareAuthenticationSuccessHandler)
                    .failureHandler(ajaxAwareAuthenticationFailureHandler)
                    .and()
                .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/login")
                    .and()
                .rememberMe()
                    .rememberMeServices(rememberMeServices)
                    // Even though this key has been added directly to the rememberMeServices instance, the RememberMeConfigurer
                    // can instantiate a new RememberMeServices with a made-up key if the same key is not provided.
                    .key("the key value")
            ;
    
            // We do not configure a bean for the SessionAuthenticationStrategy. We want to use the Spring default strategy,
            // which is configured by the above builder chain. In order to share the correct, configured instance with our
            // custom OpenIdConnectFilter, we first tell the builder to perform the configuration (normally this would be
            // done long after this method returns)...
            http.getConfigurer(SessionManagementConfigurer.class).init(http);
            // ... then we get the shared object by interface (SessionAuthenticationStrategy) class name...
            final SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
            // ... then set it in our custom filter.
            openIdConnectFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2019-11-08
      • 1970-01-01
      • 2014-02-12
      • 2015-01-29
      • 2018-10-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-01
      相关资源
      最近更新 更多