2023-09-19  阅读(2)
原文作者:墨家巨子@俏如来 原文地址: https://blog.csdn.net/u014494148/article/details/109287738

前言

Oauth2的授权流程是客户端向认证服务器提交认证获取Token,认证服务器颁发JWT格式的Token客户端进行Token的存储,接着客户端带着Token请求资源服务器,资源服务器校验Token并对资源授权,授权成功返回资源
有这么一种情况,就是客户端的请求可能需要多个资源服务器共同完成,即:一个请求过来到达资源服务器A,资源服务器A需要调用资源服务器B才能完成请求,如果资源服务器B也需要做授权,那我们的请求可能会失败,因为我们的Token通过请求到达了资源服务器A,默认情况下资源服务A调用资源服务器B并不能把Token转发过去,所以资源服务器B可能会授权失败 , 所以我们要做服务之间的授权

1.服务之间授权方案

原理其实很简单,我们的调用关系是客户端(浏览器)调用资源服务器A通过请求头传递Token,资源服务器A通过Feign调用资源服务器B请求是没有Token的,我们只需要编写一个Feign的拦截器,将客户端请求A的请求头中的Token设置到资源服务器A调用资源服务器B的Feign的请求头中即可

202309192313233941.png

2.搭建第二个资源服务器

这里需要搭建第二个资源服务器来演示服务器之间的授权,只需要把security-resource-server 复制一份命名为 security-resource2-server 即可,当然security-resource-server 需要集成Feign去调用security-resource2-server ,这里就省略了,没有思路??您可以参照 《负载均衡Feign

这里还要注意一个问题,就是用户如果要能访问到资源服务器A和资源服务器B,那么它应该同时拥有资源服务器A和资源服务器B的访问权限(permisson),所以不要忘记给用户把需要的权限都配置上

3.Feign的拦截器

为了通用,我们可以搭建一个公共的模块编写Feign的拦截器,谁需要转发Token就只需要依赖它即可 , 搭建模块 “security-resource-common” ,

3.1.导入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

3.2.定义Feign的拦截器

RequestInterceptor 是Feign的拦截器接口,提供了apply方法让我们可以通过RequestTemplate 对请求进行自定义,注意:该拦截器类需要给Spirng扫描到

    @Component
    @Slf4j
    public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
    
        //请求头中的token
        private final String AUTHORIZATION_HEADER = "Authorization";
        
        @Override
        public void apply(RequestTemplate requestTemplate) {
            //requestTemplate:feign底层用来发封装请求的对象
    
            //1.获取请求对象
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            
            //2.获取请求头中的Token
            String token = requestAttributes.getRequest().getHeader(AUTHORIZATION_HEADER);
            log.info("Feign拦截器添加请求头:{}",token);
            
            //3.添加到Feign的请求头
            requestTemplate.header(AUTHORIZATION_HEADER,token);
        }
    }
    代码挺简单的,通过RequestContextHolder得到请求对象,获取请求头中的Token设置到RequestTemplate 的header中即可

4.修改Hystrix并发策略

4.1.问题描述

理论上来说做了如上配置即可完成Token的转发了,但如果我们集成了Hystrix那么在Feign的拦截器中是没办法获取到请求对象的,这是因为Hystrix默认的隔离策略是线程池,每个请都会被分配到一个新的线程执行,导致请对象无法获取,不知道如何降级?见《Feign开启Hystrix

4.2.解决方案

解决方案有两种,一是使用信号量隔离,在配置文件中加入如下配置:“hystrix.command.default.execution.isolation.strategy=SEMAPHORE”,解决方案二是修改Hystrix的隔离策略,我们这里使用第二种方式,因为使用信号量隔离会让请求变成单线程执行,官方也不推荐。

202309192313238882.png
如何修改Hystrix的隔离策略呢?原理就是把调用线程A中的请求通过RequestContextHolder获取到,放到新的线程B中的RequestContextHolder中即可 。

4.3.定义Hystrix并发策略配置

    @Configuration
    public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
    
        private HystrixConcurrencyStrategy hystrixConcurrencyStrategy;
    
        public FeignHystrixConcurrencyStrategy() {
            try {
                this.hystrixConcurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
                if (this.hystrixConcurrencyStrategy instanceof FeignHystrixConcurrencyStrategy) {
                    return;
                }
                HystrixCommandExecutionHook commandExecutionHook =
                        HystrixPlugins.getInstance().getCommandExecutionHook();
                HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
                HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
                HystrixPropertiesStrategy propertiesStrategy =
                        HystrixPlugins.getInstance().getPropertiesStrategy();
    
                HystrixPlugins.reset();
                HystrixPlugins instance = HystrixPlugins.getInstance();
                instance.registerConcurrencyStrategy(this);
                instance.registerCommandExecutionHook(commandExecutionHook);
                instance.registerEventNotifier(eventNotifier);
                instance.registerMetricsPublisher(metricsPublisher);
                instance.registerPropertiesStrategy(propertiesStrategy);
            } catch (Exception e) {
                System.out.println("策略注册失败");
            }
        }
    
        @Override
        public <T> Callable<T> wrapCallable(Callable<T> callable) {
            //线程A获取请求对象
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            //把请求对象放到新的线程中
            return new WrappedCallable<>(callable, requestAttributes);
        }
    
        @Override
        public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                                HystrixProperty<Integer> corePoolSize,
                                                HystrixProperty<Integer> maximumPoolSize,
                                                HystrixProperty<Integer> keepAliveTime,
                                                TimeUnit unit, BlockingQueue<Runnable> workQueue) {
            return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
                    unit, workQueue);
        }
    
        @Override
        public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                                HystrixThreadPoolProperties threadPoolProperties) {
            return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, threadPoolProperties);
        }
    
        @Override
        public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
            return this.hystrixConcurrencyStrategy.getBlockingQueue(maxQueueSize);
        }
    
        @Override
        public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
            return this.hystrixConcurrencyStrategy.getRequestVariable(rv);
        }
    
        static class WrappedCallable<T> implements Callable<T> {
            private final Callable<T> target;
            private final RequestAttributes requestAttributes;
    
            public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
                this.target = target;
                this.requestAttributes = requestAttributes;
            }
    
            @Override
            public T call() throws Exception {
                try {
                    //把A线程传入过来的请求对象,设置到B线程的RequestContextHolder
                    RequestContextHolder.setRequestAttributes(requestAttributes);
                    return target.call();
                } finally {
                    RequestContextHolder.resetRequestAttributes();
                }
            }
        }
    }

到这里拦截器模块就搭建结束了,你还需要把这个模块依赖到资源服务器中,即给“security-resource-server” 导入依赖 “security-resource-common”,然后启动服务测试。

5.服务之间授权-客户端模式

5.1.问题描述

上面服务之间授权的场景是用户发起的请求需求多个服务完成,然后有了服务之间的授权,有一种场景就是服务之间的调用可能和用户上下文无关。

比如有这样一个场景,由于项目设计的问题,在做微服务重构时导致认证中心AuthServer在做认证时需要调用另外一个微服务SystemServer来加载用来的权限列表,即:用户认证表和用户权限表不在同一个数据库中,当然解决方案也有很多,比如强行把权限和认证表放在一个库,这个不是今天讨论的重点。

现在的问题是SystemServer是做了权限控制的,也就是说客户端向AuthServer提交认证请求获取Token,而AuthServer需要调用SystemServer加载用户的权限才能生成Token,而加载权限又需要先得有一个Token才行,这不就矛盾了么?

这里我要说的解决方案就是给AuthServer添加一个Feign的拦截器,在拦截器中采用客户端模式生成一个临时的Token向SystemServer发起请求加载权限列表然后再为客户端颁发正式的Token

为什么采用客户端模式?因为服务之间是绝对信任的,而且只是需要颁发一个临时的Token而已,和用户上下文无关,这个Token并不是为用户生成的Token,只是服务之间调用需要的临时Token

5.2.Feign的拦截器

这次的拦截器和上一篇文章的拦截器的定义方式一样,只不过上一次是转发用户的Token,而这一次是自己生成一个临时的Token,代码如下:

    @Component
    @Slf4j
    public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
    
        private static String TEMPTOKENURL = "http://localhost:3000/oauth/token?client_id=%s&client_secret=%s&grant_type=client_credentials";
    
        //请求头中的token
        private final String AUTHORIZATION_HEADER = "Authorization";
    
        @Override
        public void apply(RequestTemplate requestTemplate) {
            //requestTemplate:feign底层用来发请求的http客户端
            //1.使用客户端模式生成一个临时的Token
            Map<String, String> tokenMap = HttpUtil.sendPost(String.format(TEMPTOKENURL, "temp", "123"));
            ValidUtils.assertNotNull(tokenMap,"服务调用失败[临时Token获取失败]");
    
            log.info("Feign拦截器添加临时Token到请求头:{}",tokenMap);
    
            //2.添加到Feign的请求头
            requestTemplate.header(AUTHORIZATION_HEADER,"Bearer "+tokenMap.get("access_token"));
        }
    }

5.4.Http工具类

http工具用到了httpclient包

     <dependencies>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
             <version>4.5.5</version>
         </dependency>
       <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>
             <version>1.2.59</version>
         </dependency>
     </dependencies>

工具类

    public class HttpUtil {
    
        //发送Post请求,注意:参数使用?方式带着URL后面
        public static Map<String,String> sendPost(String url) {
            // 获得Http客户端(可以理解为:你得先有一个浏览器;注意:实际上HttpClient与浏览器是不一样的)
            CloseableHttpClient httpClient = HttpClientBuilder.create().build();
            // 创建Post请求
            HttpPost httpPost = new HttpPost(url);
            // 响应模型
            CloseableHttpResponse response = null;
            try {
                // 由客户端执行(发送)Post请求
                response = httpClient.execute(httpPost);
                // 从响应模型中获取响应实体
                HttpEntity responseEntity = response.getEntity();
                if (responseEntity != null) {
                    return JSON.parseObject(EntityUtils.toString(responseEntity),Map.class);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    // 释放资源
                    if (httpClient != null) {
                        httpClient.close();
                    }
                    if (response != null) {
                        response.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    }

总结

到这里就结束了,文章介绍了两种微服务之间的授权方式,一种是和用户上下文有关的请求,通过Feign的拦截器转发用户请求中的Token的给下游微服务,二种是微服务之间调用可能和当前用户上下文无关,我们采用在拦截器中采用客户端模式生成一个Token转发给下游服务的方式,希望对你有所帮助


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文