十三.SpringCloud+Security+Oauth2实现微服务授权 - 服务之间授权

简介: SpringCloud+Security+Oauth2实现微服务授权 - 服务之间授权

1.服务之间授权方案

原理其实很简单,我们的调用关系是客户端(浏览器)调用资源服务器A通过请求头传递Token,资源服务器A通过Feign调用资源服务器B请求是没有Token的,我们只需要编写一个Feign的拦截器,将客户端请求A的请求头中的Token设置到资源服务器A调用资源服务器B的Feign的请求头中即可
image.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的隔离策略,我们这里使用第二种方式,因为使用信号量隔离会让请求变成单线程执行,官方也不推荐。
image.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转发给下游服务的方式,希望对你有所帮助

相关文章
|
30天前
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
153 69
利用Spring Cloud Gateway Predicate优化微服务路由策略
|
14天前
|
搜索推荐 NoSQL Java
微服务架构设计与实践:用Spring Cloud实现抖音的推荐系统
本文基于Spring Cloud实现了一个简化的抖音推荐系统,涵盖用户行为管理、视频资源管理、个性化推荐和实时数据处理四大核心功能。通过Eureka进行服务注册与发现,使用Feign实现服务间调用,并借助Redis缓存用户画像,Kafka传递用户行为数据。文章详细介绍了项目搭建、服务创建及配置过程,包括用户服务、视频服务、推荐服务和数据处理服务的开发步骤。最后,通过业务测试验证了系统的功能,并引入Resilience4j实现服务降级,确保系统在部分服务故障时仍能正常运行。此示例旨在帮助读者理解微服务架构的设计思路与实践方法。
65 16
|
17天前
|
监控 JavaScript 数据可视化
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
|
22天前
|
Java 关系型数据库 数据库
微服务SpringCloud分布式事务之Seata
SpringCloud+SpringCloudAlibaba的Seata实现分布式事务,步骤超详细,附带视频教程
48 1
|
4月前
|
SpringCloudAlibaba API 开发者
新版-SpringCloud+SpringCloud Alibaba
新版-SpringCloud+SpringCloud Alibaba
|
7天前
|
人工智能 安全 Java
AI 时代:从 Spring Cloud Alibaba 到 Spring AI Alibaba
本次分享由阿里云智能集团云原生微服务技术负责人李艳林主讲,主题为“AI时代:从Spring Cloud Alibaba到Spring AI Alibaba”。内容涵盖应用架构演进、AI agent框架发展趋势及Spring AI Alibaba的重磅发布。分享介绍了AI原生架构与传统架构的融合,强调了API优先、事件驱动和AI运维的重要性。同时,详细解析了Spring AI Alibaba的三层抽象设计,包括模型支持、工作流智能体编排及生产可用性构建能力,确保安全合规、高效部署与可观测性。最后,结合实际案例展示了如何利用私域数据优化AI应用,提升业务价值。
|
1月前
|
SpringCloudAlibaba 负载均衡 Dubbo
【SpringCloud Alibaba系列】Dubbo高级特性篇
本章我们介绍Dubbo的常用高级特性,包括序列化、地址缓存、超时与重试机制、多版本、负载均衡。集群容错、服务降级等。
【SpringCloud Alibaba系列】Dubbo高级特性篇
|
1月前
|
存储 SpringCloudAlibaba Java
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论。
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
|
1月前
|
SpringCloudAlibaba JavaScript Dubbo
【SpringCloud Alibaba系列】Dubbo dubbo-admin安装教程篇
本文介绍了 Dubbo-Admin 的安装和使用步骤。Dubbo-Admin 是一个前后端分离的项目,前端基于 Vue,后端基于 Spring Boot。安装前需确保开发环境(Windows 10)已安装 JDK、Maven 和 Node.js,并在 Linux CentOS 7 上部署 Zookeeper 作为注册中心。
【SpringCloud Alibaba系列】Dubbo dubbo-admin安装教程篇
|
1月前
|
SpringCloudAlibaba Dubbo Java
【SpringCloud Alibaba系列】Dubbo基础入门篇
Dubbo是一款高性能、轻量级的开源Java RPC框架,提供面向接口代理的高性能RPC调用、智能负载均衡、服务自动注册和发现、运行期流量调度、可视化服务治理和运维等功能。
【SpringCloud Alibaba系列】Dubbo基础入门篇