【feign】OpenFeign访问需要OAuth2授权的服务

文章目录

  • OpenFeign访问需要OAuth2授权的服务
    • 概述
    • 示例
      • OAuth2.0相关配置
        • 引入依赖
        • 配置application.yml
        • 配置资源服务器
      • OAuth2FeignConfiguration
        • 引入依赖
        • FeignClient使用
        • 编写OAuth2FeignClient
        • ==编写OAuth2FeignConfiguration(重点)==
        • OAuth2FeignConfiguration相关说明
      • 扩展
    • 参考

OpenFeign访问需要OAuth2授权的服务

概述

Spring Cloud 微服务架构下使用feign组件进行服务间的调用,该组件使用http协议进行服务间的通信,同时整合了Ribbion使其具有负载均衡和失败重试的功能,微服务service-a调用需要授权的service-b的流程中大概流程 :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gf1QUs9i-1618850710274)(https://img2018.cnblogs.com/blog/733995/201810/733995-20181031151136077-1818990556.png “微服务service-a调用需要授权的service-b流程图”)]

随着微服务安全性的增强,需要携带token才能访问其API,然而feign组件默认并不会将 token 放到 Header 中,那么如何使用OpenFeign实现自动设置授权信息并访问需要OAuth2授权的服务呢?

本文重点讲述如何通过RequestInterceptor实现自动设置授权信息,并访问需要OAuth2的client模式授权的服务。需要重点理解下面两点:

  • OAuth2.0配置
  • OAuth2FeignRequestInterceptor

本文依赖:

  • spring-boot-starter-parent:2.4.2
  • spring-cloud-starter-openfeign:3.0.0
  • spring-cloud-starter-oauth2:2.2.4.RELEASE

示例

OAuth2.0相关配置

引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <version>2.2.4.RELEASE</version>
</dependency>

配置application.yml

auth.service:: http://localhost:8080

security:
  oauth2:
    client:
      client-id: car-client
      client-secret: 123456
      grant-type: client_credentials
      access-token-uri: ${auth.service}/oauth/token #请求令牌的地址
      scope:
        - all
    resource:
      jwt:
        key-uri: ${auth.service}/oauth/token_key
      user-info-uri: ${auth.service}/api/sso/user/me

配置资源服务器

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().antMatchers("/**")
		        .and().authorizeRequests()
                .antMatchers("/**").permitAll()
				.anyRequest().authenticated();
    }
}

OAuth2FeignConfiguration

引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.0.2</version>
</dependency>

FeignClient使用

@Resource
private OAuth2FeignClient oAuth2FeignClient;
...
String vo = oAuth2FeignClient.getMemberInfo();

编写OAuth2FeignClient

oauth2.api.url: http://localhost:8081
@FeignClient(url = "${oauth2.api.url}", name = "oauth2FeignClient", configuration = OAuth2FeignConfiguration.class)
public interface OAuth2FeignClient {
    @PostMapping("/car/info")
    String getCarInfo();
}

编写OAuth2FeignConfiguration(重点)

public class OAuth2FeignConfiguration {
    /** feign的OAuth2ClientContext */
    private final OAuth2ClientContext feignOAuth2ClientContext = new DefaultOAuth2ClientContext();

    @Resource
    private ClientCredentialsResourceDetails clientCredentialsResourceDetails;

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails);
    }

    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor() {
        return new OAuth2FeignRequestInterceptor(feignOAuth2ClientContext, clientCredentialsResourceDetails);
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public Retryer retry() {
        // default Retryer will retry 5 times waiting waiting
        // 100 ms per retry with a 1.5* back off multiplier
        return new Retryer.Default(100, SECONDS.toMillis(1), 3);
    }

    @Bean
    public Decoder feignDecoder() {
        return new CustomResponseEntityDecoder(new SpringDecoder(this.messageConverters), feignOAuth2ClientContext);
    }

    /**
     * Http响应成功 但是token失效,需要定制 ResponseEntityDecoder
     * @author maxianming
     * @date 2018/10/30 9:47
     */
    class CustomResponseEntityDecoder implements Decoder {
        private org.slf4j.Logger log = LoggerFactory.getLogger(CustomResponseEntityDecoder.class);

        private Decoder decoder;

        private OAuth2ClientContext context;

        public CustomResponseEntityDecoder(Decoder decoder, OAuth2ClientContext context) {
            this.decoder = decoder;
            this.context = context;
        }

        @Override
        public Object decode(final Response response, Type type) throws IOException, FeignException {
            if (log.isDebugEnabled()) {
                log.debug("feign decode type:{},reponse:{}", type, response.body());
            }
            if (isParameterizeHttpEntity(type)) {
                type = ((ParameterizedType) type).getActualTypeArguments()[0];
                Object decodedObject = decoder.decode(response, type);
                return createResponse(decodedObject, response);
            } else if (isHttpEntity(type)) {
                return createResponse(null, response);
            } else {
                // custom ResponseEntityDecoder if token is valid then go to errorDecoder
                String body = Util.toString(response.body().asReader(Util.UTF_8));
                if (body.contains("401 Unauthorized")) {
                    clearTokenAndRetry(response, body);
                }
                return decoder.decode(response, type);
            }
        }

        /**
         * token失效 则将token设置为null 然后重试
         * @param response response
         * @param body     body
         * @author maxianming
         * @date 2018/10/30 10:05
         */
        private void clearTokenAndRetry(Response response, String body) throws FeignException {
            log.error("接收到Feign请求资源响应,响应内容:{}", body);
            context.setAccessToken(null);
            throw new RetryableException(
                    response.status(),
                    "access_token过期,即将进行重试",
                    response.request().httpMethod(),
                    new Date(),
                    response.request());
        }

        private boolean isParameterizeHttpEntity(Type type) {
            if (type instanceof ParameterizedType) {
                return isHttpEntity(((ParameterizedType) type).getRawType());
            }
            return false;
        }

        private boolean isHttpEntity(Type type) {
            if (type instanceof Class) {
                Class c = (Class) type;
                return HttpEntity.class.isAssignableFrom(c);
            }
            return false;
        }

        @SuppressWarnings("unchecked")
        private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
            MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
            for (String key : response.headers().keySet()) {
                headers.put(key, new LinkedList<>(response.headers().get(key)));
            }
            return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response.status()));
        }
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return new RestClientErrorDecoder(feignOAuth2ClientContext);
    }

    /**
     * Feign调用HTTP返回响应码错误时候,定制错误的解码
     * @author liudong
     * @date 2018/10/30 9:45
     */
    class RestClientErrorDecoder implements ErrorDecoder {
        private org.slf4j.Logger logger = LoggerFactory.getLogger(RestClientErrorDecoder.class);

        private OAuth2ClientContext context;

        RestClientErrorDecoder(OAuth2ClientContext context) {
            this.context = context;
        }

        @Override
        public Exception decode(String methodKey, Response response) {
            FeignException exception = errorStatus(methodKey, response);
            logger.error("Feign调用异常,异常methodKey:{}, token:{}, response:{}", methodKey, context.getAccessToken(), response.body());
            if (HttpStatus.UNAUTHORIZED.value() == response.status()) {
                logger.error("接收到Feign请求资源响应401,access_token已经过期,重置access_token为null待重新获取。");
                context.setAccessToken(null);
                return new RetryableException(
                        response.status(),
                        "疑似access_token过期,即将进行重试",
                        response.request().httpMethod(),
                        exception,
                        new Date(),
                        response.request());
            }
            return exception;
        }
    }

}

OAuth2FeignConfiguration相关说明

  1. 使用ClientCredentialsResourceDetails(client_id、client-secret、jwt.key-uri/user-info-uri等信息配置在配置中心)初始化OAuth2RestTemplate,用户请求创建token时候验证基本信息;
  2. 主要定义了拦截器初始化了OAuth2FeignRequestInterceptor,使得Feign进行RestTemplate调用请求前进行token拦截。如果不存在token则需要从auth-server中获取token;
  3. 注意上下文对象OAuth2ClientContext建立后不放在Bean容器中:由于Spring mvc的前置处理器, 会复制用户的token到OAuth2ClientContext中,如果放在Bean容器中,用户的token会覆盖服务间的token,当两个token的权限不同时,将导致验证不通过;
  4. 重新定义了Decoder,对RestTemple http调用的响应进行了解码,对token失效的情况进行了扩展:
    1. 默认情况下:对于由于token失效返回401错误的http响应,导致进入ErrorDecoder的情况,在ErrorDecoder中进行清空token操作,并返回RetryableException,让Feign重试。
    2. 扩展后:对于接口200响应token失效的错误码的情况,将会走Decoder流程,所以对ResponseEntityDecoder进行了扩展,如果响应无效token错误码,则清空token并重试。

扩展

  • OAuth2FeignRequestInterceptor copy OAuth2RestTemplate 的获取token内容, 后者实现了获取token并存入context未超时时不会再次请求授权服务器,减轻了授权服务器的开销
  • ClientCredentialsResourceDetails可以拓展为其他3种授权模式的Details, 有兴趣的请移步至OAuth2ProtectedResourceDetails的源码
  • Bean容器中的OAuth2ClientContext的token与服务间调用所需的token权限不同; 或者当前上下文中没有token,但后者调用需要token(Spring mvc的前置处理器, 会复制token到OAuth2ClientContext中); 这两种情况均可以建立不放入Bean容器中的OAuth2ClientContext
  • 如果Bean容器中的OAuth2ClientContext的token与服务间调用所需的token权限相同, 可以注入Bean容器中的OAuth2ClientContext; 也可以参考SpringCloud 中 Feign 调用添加 Oauth2 Authorization Header, 或者 feign之间传递oauth2-token的问题和解决 来实现, 其获取token的核心逻辑可以参考源码org.springframework.cloud.commons.security.AccessTokenContextRelay;
  • (未解决)OAuth2RestTemplateOAuth2FeignConfiguration担任什么角色? 有什么作用?
  • (未解决)启动类不能配置@EnableOAuth2Client, 否则无法启动项目, 有木有大佬知道原因? 个人猜测和AccessTokenContextRelay有关系
  • (未解决)该文档学会了OAuth2FeignRequestInterceptor的用法, 那么BasicAuthRequestInterceptor又用于什么场景呢?
  • (未解决)尝试使用ResponseMappingDecoder的设计思路实现CustomResponseEntityDecoder
  • (未解决)spring-cloud-starter-oauth2于2020年8月1日发布了2.2.4.RELEASE版本后一直没有更新, 如果其不维护又该怎么办呢? 有木有其他实现方式呢?
  • (未解决)spring-boot-starter-oauth2-clientspring-boot-starter-oauth2-resource-server刚刚发布了2.4.5版本, 其一直在更新, 是否可以用来实现OAuth2配置部分? 可以参考官方文档进行评估(前者支持配置多个APP的client信息并分别获取授权)

参考

  • 优秀文档-浮生半日
  • 参考文档

热门文章

暂无图片
编程学习 ·

那些年让我们目瞪口呆的bug

程序员一生与bug奋战&#xff0c;可谓是杀敌无数&#xff0c;见怪不怪了&#xff01;在某知识社交平台中&#xff0c;一个“有哪些让程序员目瞪口呆的bug”的话题引来了6700多万的阅读&#xff0c;可见程序员们对一个话题的敏感度有多高。 1、麻省理工“只能发500英里的邮件” …
暂无图片
编程学习 ·

redis的下载与安装

下载redis wget http://download.redis.io/releases/redis-5.0.0.tar.gz解压redis tar -zxvf redis-5.0.0.tar.gz编译 make安装 make install快链方便进入redis ln -s redis-5.0.0 redis
暂无图片
编程学习 ·

《大话数据结构》第三章学习笔记--线性表(一)

线性表的定义 线性表&#xff1a;零个或多个数据元素的有限序列。 线性表元素的个数n定义为线性表的长度。n为0时&#xff0c;为空表。 在比较复杂的线性表中&#xff0c;一个数据元素可以由若干个数据项组成。 线性表的存储结构 顺序存储结构 可以用C语言中的一维数组来…
暂无图片
编程学习 ·

对象的扩展

文章目录对象的扩展属性的简洁表示法属性名表达式方法的name属性属性的可枚举性和遍历可枚举性属性的遍历super关键字对象的扩展运算符解构赋值扩展运算符AggregateError错误对象对象的扩展 属性的简洁表示法 const foo bar; const baz {foo}; baz // {foo: "bar"…
暂无图片
编程学习 ·

让程序员最头疼的5种编程语言

世界上的编程语言&#xff0c;按照其应用领域&#xff0c;可以粗略地分成三类。 有的语言是多面手&#xff0c;在很多不同的领域都能派上用场。大家学过的编程语言很多都属于这一类&#xff0c;比如说 C&#xff0c;Java&#xff0c; Python。 有的语言专注于某一特定的领域&…
暂无图片
编程学习 ·

写论文注意事项

参考链接 给研究生修改了一篇论文后&#xff0c;该985博导几近崩溃…… 重点分析 摘要与结论几乎重合 这一条是我见过研究生论文中最常出现的事情&#xff0c;很多情况下&#xff0c;他们论文中摘要部分与结论部分重复率超过70%。对于摘要而言&#xff0c;首先要用一小句话引…
暂无图片
编程学习 ·

安卓 串口开发

上图&#xff1a; 上码&#xff1a; 在APP grable添加 // 串口 需要配合在项目build.gradle中的repositories添加 maven {url "https://jitpack.io" }implementation com.github.licheedev.Android-SerialPort-API:serialport:1.0.1implementation com.jakewhart…
暂无图片
编程学习 ·

2021-2027年中国铪市场调研与发展趋势分析报告

2021-2027年中国铪市场调研与发展趋势分析报告 本报告研究中国市场铪的生产、消费及进出口情况&#xff0c;重点关注在中国市场扮演重要角色的全球及本土铪生产商&#xff0c;呈现这些厂商在中国市场的铪销量、收入、价格、毛利率、市场份额等关键指标。此外&#xff0c;针对…
暂无图片
编程学习 ·

Aggressive cows题目翻译

描述&#xff1a; Farmer John has built a new long barn, with N (2 < N < 100,000) stalls.&#xff08;John农民已经新建了一个长畜棚带有N&#xff08;2<N<100000&#xff09;个牛棚&#xff09; The stalls are located along a straight line at positions…
暂无图片
编程学习 ·

剖析组建PMO的6个大坑︱PMO深度实践

随着事业环境因素的不断纷繁演进&#xff0c;项目时代正在悄悄来临。设立项目经理转岗、要求PMP等项目管理证书已是基操&#xff0c;越来越多的组织开始组建PMO团队&#xff0c;大有曾经公司纷纷建造中台的气质&#xff08;当然两者的本质并不相同&#xff0c;只是说明这个趋势…
暂无图片
编程学习 ·

Flowable入门系列文章118 - 进程实例 07

1、获取流程实例的变量 GET运行时/进程实例/ {processInstanceId} /变量/ {变量名} 表1.获取流程实例的变量 - URL参数 参数需要值描述processInstanceId是串将流程实例的id添加到变量中。变量名是串要获取的变量的名称。 表2.获取流程实例的变量 - 响应代码 响应码描述200指…
暂无图片
编程学习 ·

微信每天自动给女[男]朋友发早安和土味情话

微信通知&#xff0c;每天给女朋友发早安、情话、诗句、天气信息等~ 前言 之前逛GitHub的时候发现了一个自动签到的小工具&#xff0c;b站、掘金等都可以&#xff0c;我看了下源码发现也是很简洁&#xff0c;也尝试用了一下&#xff0c;配置也都很简单&#xff0c;主要是他有一…
暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

项目经理,你有什么优势吗?

大侠被一个问题问住了&#xff1a;你和别人比&#xff0c;你的优势是什么呢? 大侠听到这个问题后&#xff0c;脱口而出道&#xff1a;“项目管理能力和经验啊。” 听者抬头看了一下大侠&#xff0c;显然听者对大侠的这个回答不是很满意&#xff0c;但也没有继续追问。 大侠回家…
暂无图片
编程学习 ·

nginx的负载均衡和故障转移

#注&#xff1a;proxy_temp_path和proxy_cache_path指定的路径必须在同一分区 proxy_temp_path /data0/proxy_temp_dir; #设置Web缓存区名称为cache_one&#xff0c;内存缓存空间大小为200MB&#xff0c;1天没有被访问的内容自动清除&#xff0c;硬盘缓存空间大小为30GB。 pro…
暂无图片
编程学习 ·

业务逻辑漏洞

身份认证安全 绕过身份认证的几种方法 暴力破解 测试方法∶在没有验证码限制或者一次验证码可以多次使用的地方&#xff0c;可以分为以下几种情况︰ (1)爆破用户名。当输入的用户名不存在时&#xff0c;会显示请输入正确用户名&#xff0c;或者用户名不存在 (2)已知用户名。…