使用 OAuth2 保护 REST API:创建名称为“scopedTarget.oauth2ClientContext”的 bean 时出错:范围“会话”未激活

Posted

技术标签:

【中文标题】使用 OAuth2 保护 REST API:创建名称为“scopedTarget.oauth2ClientContext”的 bean 时出错:范围“会话”未激活【英文标题】:Protecting REST API with OAuth2: Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active 【发布时间】:2016-06-22 20:30:08 【问题描述】:

我已经工作了几天,试图在 REST API 上实现 oauth2 保护。我已经尝试了很多不同的配置,但仍然没有设法让它工作。

我正在证明我现在拥有的代码,但我绝不会与这个实现结婚。如果你能告诉我一些完全不同的方式来完成我想要完成的事情,那就太好了。

我的流程如下所示:

    客户端检查身份验证服务器,获取令牌。 客户端向资源服务器发送令牌。 资源服务器使用身份验证服务器来确保令牌有效。

身份验证服务器工作正常。我在配置资源服务器时遇到问题。

资源服务器上的配置

这是我的一些配置。我有这个豆子:

誓言休息模板

@EnableOAuth2Client
@Configuration
@Import(PropertiesConfig.class) //Imports properties from properties files.
public class OauthRestTemplateConfig 



 @Bean
    public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ClientContext oauth2ClientContext) 
        OAuth2RestTemplate template = new OAuth2RestTemplate(oauth2ResourceDetails(), oauth2ClientContext);
        return template;
    

    @Bean
    OAuth2ProtectedResourceDetails oauth2ResourceDetails() 
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setId("theOauth");
        details.setClientId("clientID");
        details.setClientSecret("SecretKey");
        details.setAccessTokenUri("https://theAuthenticationServer.com/oauthserver/oauth2/token");
        details.setUserAuthorizationUri("https://theAuthenticationServer.com/oauthserver/oauth2/token");
        details.setTokenName("oauth_token");
        details.setPreEstablishedRedirectUri("http://localhost/login");
        details.setUseCurrentUri(true);
        return details;
    

安全配置

我在资源服务器的主要安全配置中使用该 bean:

@Slf4j
@Configuration
@EnableWebSecurity
@EnableOAuth2Client
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true, proxyTargetClass = true)
@Import(PropertiesConfig.class, OauthRestTemplateConfig.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    @Autowired
    @Qualifier("oAuth2RestTemplate")
    private OAuth2RestTemplate oAuth2RestTemplate;

    @Override
    protected void configure(HttpSecurity http) throws Exception 

        http
                .authorizeRequests()
                .accessDecisionManager(accessDecisionManager()) //This is a WebExpressionVoter. I don't think it's related to the problem so didn't include the source.
                    .antMatchers("/login").permitAll()      
                .antMatchers("/api/**").authenticated()
                .anyRequest().authenticated();
        http
                .exceptionHandling()
                .authenticationEntryPoint(delegatingAuthenticationEntryPoint());
        http
                .addFilterBefore(new OAuth2ClientContextFilter(), BasicAuthenticationFilter.class)
                .addFilterAfter(oauth2ClientAuthenticationProcessingFilter(), OAuth2ClientContextFilter.class)
        ;
    

    private OAuth2ClientAuthenticationProcessingFilter oauth2ClientAuthenticationProcessingFilter() 
        OAuth2ClientAuthenticationProcessingFilter
                daFilter = new OAuth2ClientAuthenticationProcessingFilter("/api/**");
        daFilter.setRestTemplate(oAuth2RestTemplate);
        daFilter.setTokenServices(inMemoryTokenServices());
        return daFilter;
     

    private DefaultTokenServices inMemoryTokenServices() 
        InMemoryTokenStore tok = new InMemoryTokenStore();
        DefaultTokenServices tokenService = new DefaultTokenServices();
        tokenService.setTokenStore(tok);

        return tokenService;
    

安全配置中的额外内容

Aaand,一些我认为不太相关的 bean,但在这里它们以防你需要它们:

@Bean
public DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() 
    LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> matchers =
            Maps.newLinkedHashMap();

    //Match all HTTP methods
    matchers.put(new RegexRequestMatcher("\\/api\\/v\\d+\\/.*", null), oAuth2AuthenticationEntryPoint());
    matchers.put(AnyRequestMatcher.INSTANCE, casAuthenticationEntryPoint());

    DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(matchers);
    entryPoint.setDefaultEntryPoint(casAuthenticationEntryPoint());

    return entryPoint;

@Bean(name = "casEntryPoint")
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() 
    CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
    casAuthenticationEntryPoint.setLoginUrl(casUrl + "/login");
    casAuthenticationEntryPoint.setServiceProperties(serviceProperties());

    return casAuthenticationEntryPoint;

错误

资源服务器启动正常。客户端从 theAuthenticationServer.com 获取其身份验证令牌,并将其在请求标头中发送到 api url。我收到以下错误:

HTTP 状态 500 - 创建名为“scopedTarget.oauth2ClientContext”的 bean 时出错:范围“会话”对于当前线程不活动;如果您打算从单例中引用它,请考虑为该 bean 定义一个作用域代理;嵌套异常是 java.lang.IllegalStateException:未找到线程绑定请求:您是指实际 Web 请求之外的请求属性,还是在原始接收线程之外处理请求?如果您实际上是在 Web 请求中操作并且仍然收到此消息,则您的代码可能在 DispatcherServlet/DispatcherPortlet 之外运行:在这种情况下,请使用 RequestContextListener 或 RequestContextFilter 来公开当前请求。

异常报告

创建名为“scopedTarget.oauth2ClientContext”的 bean 时出错:范围“会话”对当前线程不活动;如果您打算从单例中引用它,请考虑为该 bean 定义一个作用域代理;嵌套异常是 java.lang.IllegalStateException:未找到线程绑定请求:您是指实际 Web 请求之外的请求属性,还是在原始接收线程之外处理请求?如果您实际上是在 Web 请求中操作并且仍然收到此消息,则您的代码可能在 DispatcherServlet/DispatcherPortlet 之外运行:在这种情况下,请使用 RequestContextListener 或 RequestContextFilter 来公开当前请求。

服务器遇到一个内部错误,导致它无法完成此请求。

        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.
    org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:355)
    org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
    org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:187)
    com.sun.proxy.$Proxy26.getAccessToken(Unknown Source)
    org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:169)
    org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:94)
    org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter(OAuth2ClientContextFilter.java:60)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:120)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

root cause
        <pre>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.
    org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    org.springframework.web.context.request.SessionScope.get(SessionScope.java:91)
    org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340)
    org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
    org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:187)
    com.sun.proxy.$Proxy26.getAccessToken(Unknown Source)
    org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:169)
    org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:94)
    org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.doFilter(OAuth2ClientContextFilter.java:60)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:120)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
    org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
    org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)

我尝试了很多不同的配置,在网上查找了大量资源,但我一无所获。我在使用正确的课程吗?知道我可能需要更改哪些配置吗?

【问题讨论】:

【参考方案1】:

我正在证明我现在拥有的密码,但我还没有结婚 到这个实现。如果你能给我看一些完全不同的东西 完成我想要完成的事情的方法,太棒了

如果您的主要问题是实现资源服务器,并且您对完全不同的解决方案持开放态度,您可以使用 Spring Boot 的资源服务器自动配置。这样你就会有一个ResourceServerConfiguration,如下所示:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter 
    @Override
    public void configure(HttpSecurity http) throws Exception 
        http
                .authorizeRequests()
                    .anyRequest().authenticated();
        // you can put your application specific configurations here
        // here i'm just authenticating every request
    

在您的src/main/resources 中有一个application.yml 配置文件:

security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
    resource:
      token-info-uri: http://localhost:8888/oauth/check_token

您应该在此处添加您的client-idclient-secrettoken-info-uritoken-info-uriAuthorization Server 上的端点,我们的资源服务器将就传递的 Access Tokens 的有效性进行咨询。

通过这些安排,如果客户端向/api/greet API 发起请求:

GET /api/greet HTTP/1.1
Host: localhost:8080
Authorization: bearer cef63a29-f9aa-4dcf-9155-41fb035a6cdb

我们的资源服务器将从请求中提取Bearer访问令牌,并将以下请求发送到授权服务器以验证访问令牌:

GET /oauth/check_token?token=cef63a29-f9aa-4dcf-9155-41fb035a6cdb HTTP/1.1
Host: localhost:8888
Authorization: basic base64(client-id:client-secret)

如果令牌有效,授权服务器会发送一个带有如下 JSON 正文的 200 OK 响应:

"exp":1457684735,"user_name":"me","authorities":["ROLE_USER"],"client_id":"client","scope":["auth"]

否则,它将返回4xx Client Error

这是一个带有 pom.xml 的 maven 项目,如下所示:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.3.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
    </dependency>
</dependencies>

还有一个典型的Application 类:

@SpringBootApplication
public class Application 
    public static void main(String[] args) 
        SpringApplication.run(Application.class, args);
    

您可以查看有关资源服务器自动配置here 的 Spring Boot 文档。

【讨论】:

【参考方案2】:

我认为问题的根源在于您使用new 运算符创建了OAuth2ClientAuthenticationProcessingFilterOAuth2ClientContextFilter

如果你查看堆栈跟踪

org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
org.springframework.web.context.request.SessionScope.get(SessionScope.java:91)
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340)
org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:187)
com.sun.proxy.$Proxy26.getAccessToken(Unknown Source)
org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:169)
org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:94)

OAuth2ClientAuthenticationProcessingFilterJdkDynamicAopProxy 有一条链,并试图获取bean。而且我可以假设因为该 bean 是在 Spring 容器中创建的,所以它无法从会话范围中获取 bean。

尝试将您的过滤器包装到@Bean 注释中,以便将它们放入上下文中。另外,我认为设置正确的范围是值得的:request 在这里匹配得最好。

【讨论】:

【参考方案3】:

在查看 Spring 文档后,我最终解决了这个问题。

原来我的应用程序中实际上并不存在范围上下文,因为我还没有初始化它。

我通过添加这个监听器来初始化它:

<listener>
 <listener-class>
        org.springframework.web.context.request.RequestContextListener
 </listener-class>
</listener>

【讨论】:

【参考方案4】:

将 spring-boot 1.4.1 与 spock-spring 1.1-groovy-2.4-rc-2 一起使用时,我遇到了同样的问题。修复它的最简单方法是使用 Spock 1.0。

已经报告了一个错误:https://github.com/spockframework/spock/issues/655

【讨论】:

【参考方案5】:

启用请求上下文侦听器的更简单方法是在您的应用程序中添加一个 bean 注释。

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

【讨论】:

我遇到了与提问者相同的尝试设置相同的异常,除了我还声明了@Bean RequestContextListener。这个声明应该在哪里?我已经在 Oauth 配置、调用 configure(HttpSecurity http) 的主要 SecurityConfig 以及包含一堆额外 bean 定义的辅助配置中进行了尝试。【参考方案6】:

公共类 ContextAwareCallable 实现 Callable

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;
    


然后创建执行器

公共类 ContextAwarePoolExecutor 扩展 ThreadPoolTask​​Executor

@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()));

【讨论】:

以上是关于使用 OAuth2 保护 REST API:创建名称为“scopedTarget.oauth2ClientContext”的 bean 时出错:范围“会话”未激活的主要内容,如果未能解决你的问题,请参考以下文章

使用 OAuth2 客户端凭据流保护用 PHP 编写的 REST API

需要使用 Keycloak 和 oAuth2 保护普通 Spring REST API 的示例

使用 OAuth2.0 或 Azure Active Directory 保护 REST API

需要使用Keycloack和oAuth2保护普通Spring REST API的示例

如何在使用 OAuth2 社交登录保护 REST API 的同时配置 Spring Boot 登录页面

如何使用 REST + CodeceptJS 测试 API,访问受 Auth0 保护?