带有 OAuth2FeignRequestInterceptor 的 Spring @FeignClient 不起作用

Posted

技术标签:

【中文标题】带有 OAuth2FeignRequestInterceptor 的 Spring @FeignClient 不起作用【英文标题】:Spring @FeignClient with OAuth2FeignRequestInterceptor not working 【发布时间】:2016-04-13 03:51:45 【问题描述】:

我正在尝试使用 OAuth2 设置 FeignClient 以实现“中继令牌”。我只希望 FeignClient 中继/传播来自 ZuulProxy(启用 SSO)的 OAuth2 令牌。 我使用 Spring 1.3.1-RELEASESpring Cloud Brixton.M4

我在自定义@FeignClient 配置中添加了一个拦截器:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;

import feign.RequestInterceptor;

@Configuration
public class FeignClientConfiguration 

@Value("$security.oauth2.client.userAuthorizationUri")
private String authorizeUrl;

@Value("$security.oauth2.client.accessTokenUri")
private String tokenUrl;

@Value("$security.oauth2.client.client-id")
private String clientId;


// See https://github.com/spring-cloud/spring-cloud-netflix/issues/675
@Bean
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oauth2ClientContext)
    return new OAuth2FeignRequestInterceptor(oauth2ClientContext, resource());


@Bean
protected OAuth2ProtectedResourceDetails resource() 
    AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();
    resource.setAccessTokenUri(tokenUrl);
    resource.setUserAuthorizationUri(authorizeUrl);
    resource.setClientId(clientId);
    // TODO: Remove this harcode 
    resource.setClientSecret("secret");
    return resource;
   

然后我像这样将配置添加到我的@FeignClient:

@FeignClient(name = "car-service", configuration =     FeignClientConfiguration.class)
interface CarClient                
    @RequestMapping(value = "car-service/api/car", method = GET)
    List<CarVO> getAllCars();
   

应用程序启动,但是当我从我的服务中使用 Feign 客户端时,我得到:

2016-01-08 13:14:29.757 ERROR 3308 --- [nio-9081-exec-1] o.a.c.c.C.[.[.[.    [dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in 

    context with path [/user-service] threw exception [Request processing failed; nested exception is com.netflix.hystrix.exception.HystrixRuntimeException: getAllCars failed and no fallback available.] with root cause

    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) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:41) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]

我希望我的应用程序/微服务(使用@FeingClient 调用另一个应用程序/微服务的那个)是无状态的。但是,我已经尝试了 security.sessions=STATELESS (SpringBoot 默认)和 security.sessions=ALWAYS (只是为了尝试)。 在这两种情况下,我都遇到了同样的异常。

查看代码,我发现 OAuth2ClientContext 保存在 Session(会话范围 bean)中。当您想要实现启用 STATELESS OAuth2 的应用程序/微服务时,它是如何工作的?正是在我当前的场景中,这是使用 OAuth2 的一大优势。但是,正如我所说,结果是相同的启用会话。

有人可以帮忙吗?

非常感谢! :-)

【问题讨论】:

【参考方案1】:

我发现问题在于 Hystrix 强制在另一个线程中执行代码,因此您无法访问请求/会话范围的 bean。 我在启用 Hystrix 的情况下使用 @FeignClient。当我使用 feign.hystrix.enabled: false 禁用 Hystrix 从微服务 A 到微服务 B 的调用中继令牌(使用 OAuth2FeignRequestInterceptor)工作正常。

但是,最好能够保持 Hystrix 处于启用状态。 我在这篇帖子中看到有一个新的模块在这方面改进了 Hystrix - Feign(feign-hystrix 模块):

Does Spring Cloud Feign client call execute inside hystrix command?

但是,我看不到如何使用 feign-hystrix 正确进行设置,也找不到示例。请问,你能帮忙解决这个问题或提供一个使用 feign-hystrix 的例子吗?

非常感谢!

【讨论】:

【参考方案2】:

我不确定我是否理解正确,但以下内容对我有用。

见https://jfconavarrete.wordpress.com/2014/09/15/make-spring-security-context-available-inside-a-hystrix-command/

基本上,本教程展示了如何使用额外的“插件”设置/增强 hystrix,以便通过线程局部变量在 hystrix 包装调用中提供安全上下文

使用此设置,您需要做的就是定义一个假请求拦截器,如下所示:

@Bean
public RequestInterceptor requestTokenBearerInterceptor() 

    return new RequestInterceptor() 
        @Override
        public void apply(RequestTemplate requestTemplate) 
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
            requestTemplate.header("Authorization", "Bearer " + details.getTokenValue());                   
        
    ;

通过此设置,请求中包含的令牌可供 feign 请求拦截器使用,因此您可以使用来自经过身份验证的用户的令牌在 feign 请求上设置 Authorization 标头。

另请注意,通过这种方法,您可以将 SessionManagementStrategy 保持为“STATELESS”,因为不必在服务器端“存储”任何数据

【讨论】:

【参考方案3】:

当您用作功能区客户端而不是此处我们将使用 oauth2restTemplate 时,请使用此代码和评论重新模板配置

@EnableOAuth2Client
@Configuration
public class OAuthClientConfig 

    @Value("$config.oauth2.accessTokenUri")
    private String tokenUri;

    @Value("$app.client.id")
    private String clientId;

    @Value("$app.client.secret")
    private String clientSecret;

    @Bean
    protected OAuth2ProtectedResourceDetails resource() 
        ResourceOwnerPasswordResourceDetails resource;
        resource = new ResourceOwnerPasswordResourceDetails();

        List<String> scopes = new ArrayList<String>(2);
        scopes.add("write");
        scopes.add("read");
        resource.setAccessTokenUri(tokenUri);
        resource.setClientId(clientId);
        resource.setClientSecret(clientSecret);
        resource.setGrantType("password");
        resource.setScope(scopes);
        return resource;
    

    @Bean
    public OAuth2ClientContext oauth2ClientContext() 
        DefaultOAuth2ClientContext defaultOAuth2ClientContext = new DefaultOAuth2ClientContext();
        return defaultOAuth2ClientContext;
    

    @Bean
    @Primary
    @LoadBalanced
    public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
            OAuth2ClientContext oauth2ClientContext) 
        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, oauth2ClientContext);
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        restTemplate.setRequestFactory(factory);
        return restTemplate;
    

    @Bean
    public OAuth2FeignRequestInterceptor aauthRequestInterceptor(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
            OAuth2ClientContext oauth2ClientContext)
    
        OAuth2FeignRequestInterceptor auth2FeignRequestInterceptor=new OAuth2FeignRequestInterceptor(oauth2ClientContext, oAuth2ProtectedResourceDetails);
        return auth2FeignRequestInterceptor;
    

【讨论】:

以上是关于带有 OAuth2FeignRequestInterceptor 的 Spring @FeignClient 不起作用的主要内容,如果未能解决你的问题,请参考以下文章

带有和不带有聚合的 sql 查询

如何翻转正面带有标签而背面带有另一个标签的视图 - 参见图片

CakePHP 如何处理带有/不带有 'id' 字段的 HABTM 表?

带有滚动的 Div 和带有绝对位置的内容

带有 RecyclerView 的 DialogFragment 比带有 Recyclerview 的 Fragment 慢

访问控制允许带有和不带有 www 的来源