Защита API REST с помощью OAuth2: Ошибка при создании bean с именем 'scopedTarget.oauth2ClientContext': Scope 'session' неактивен

Я работаю в течение нескольких дней, чтобы попытаться реализовать защиту oauth2 в REST API. Я пробовал много разных конфигураций, но до сих пор не смог заставить его работать.

Я доказываю код, который у меня есть сейчас, но я ничем не женат на этой реализации. Если вы можете показать мне совершенно другой способ добиться того, чего я хочу достичь, отлично.

Мой поток выглядит следующим образом:

  • Клиент проверяет Auth Server, получает токен.
  • Клиент отправляет токен на сервер ресурсов.
  • Сервер ресурсов использует Auth Server, чтобы убедиться, что токен действителен.

Сервер Auth отлично работает. У меня возникли проблемы с настройкой сервера ресурсов.

Конфигурации на сервере ресурсов

Вот некоторые из моих конфигураций. У меня есть этот bean:

Шаблон отдыха Uuath

@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 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, некоторые из beans, которые, как я считаю, менее актуальны, но здесь они на случай, если они вам понадобятся:

@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;
}

Ошибка

Сервер ресурсов запускается просто отлично. Клиент получает свой токен аутентификации с сайтаAuthenticationServer.com и отправляет его в заголовок запроса на api url. И я получаю следующую ошибку:

HTTP Status 500 - Ошибка при создании bean с именем 'scopedTarget.oauth2ClientContext': Scope 'session' неактивен для текущего потока; рассмотрите возможность определения расширенного прокси-сервера для этого bean, если вы намерены ссылаться на него из singleton; Вложенное исключение - это java.lang.IllegalStateException: не найден нисходящий запрос: вы ссылаетесь на атрибуты запроса вне фактического веб-запроса или обрабатываете запрос вне исходного потока? Если вы действительно работаете в веб-запросе и все еще получаете это сообщение, ваш код, вероятно, работает за пределами DispatcherServlet/DispatcherPortlet: в этом случае используйте RequestContextListener или RequestContextFilter, чтобы выставить текущий запрос.

Отчет об исключении

Ошибка создания bean с именем 'scopedTarget.oauth2ClientContext': Scope 'session' неактивен для текущего потока; рассмотрите возможность определения расширенного прокси-сервера для этого bean, если вы намерены ссылаться на него из singleton; Вложенное исключение - это java.lang.IllegalStateException: не найден нисходящий запрос: вы ссылаетесь на атрибуты запроса вне фактического веб-запроса или обрабатываете запрос вне исходного потока? Если вы действительно работаете в веб-запросе и все еще получаете это сообщение, ваш код, вероятно, работает за пределами 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.

Оказалось, что контекст области видимости на самом деле не существует в моем приложении, потому что я не инициализировал его.

Я инициализировал его, добавив этот слушатель:

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

Ответ 2

Еще более простой способ включить прослушиватель контекста запроса - добавить в приложение аннотацию bean.

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

Ответ 3

Я доказываю код, который у меня есть сейчас, но я ничуть не женат к этой реализации. Если вы можете показать мне радикально разные способ выполнить то, что я хочу достичь, отличный

Если ваша основная проблема заключается в реализации сервера ресурсов, а также вы можете использовать совершенно разные решения, вы можете использовать автоматические конфигурации сервера загрузки Spring. Таким образом, у вас будет 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
    }
}

С конфигурационным файлом application.yml в src/main/resources:

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

Вы должны добавить свои client-id, client-secret и token-info-uri там. token-info-uri - это конечная точка на сервере авторизации, на которой наш сервер ресурсов собирается проконсультироваться о действительности прошедших токенов доступа.

С этими соглашениями, если клиент запускает запрос, скажем, /api/greet API:

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

Наш сервер ресурсов извлечет токен доступа к переносчику из запроса и отправит следующий запрос на сервер авторизации для проверки токена доступа:

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

Если токен действителен, сервер авторизации отправляет ответ 200 OK с телом JSON следующим образом:

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

В противном случае он вернет 4xx Client Error.

Это был проект maven с pom.xml следующим образом:

<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);
    }
}

Вы можете проверить загрузочную документацию Spring на автоконфигурации сервера ресурсов здесь.

Ответ 4

Я считаю, что корень проблемы заключается в том, что вы создаете OAuth2ClientAuthenticationProcessingFilter и OAuth2ClientContextFilter с оператором new.

Если вы посмотрите на stacktrace

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)

есть цепочка, как она идет от OAuth2ClientAuthenticationProcessingFilter до JdkDynamicAopProxy и пытается получить bean. И я могу предположить, из-за этого bean был создан из контейнера Spring, он не может получить bean из области сеанса.

Попробуйте обернуть ваши фильтры в аннотацию @Bean, чтобы поместить их в контекст. Кроме того, я считаю, что стоит установить правильную область: request будет лучше всего соответствовать.

Ответ 5

Я столкнулся с той же проблемой при использовании spring -boot 1.4.1 с spock-spring 1.1- groovy -2.4-rc-2. Самый простой способ исправить это - использовать Spock 1.0.

Сообщается об ошибке:
https://github.com/spockframework/spock/issues/655