springcloudalibaba架构(31):SpringCloud实现用户信息在微服务之间传递(Feign和Dubbo)

Posted 逆水行舟没有退路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springcloudalibaba架构(31):SpringCloud实现用户信息在微服务之间传递(Feign和Dubbo)相关的知识,希望对你有一定的参考价值。

文章目录

前言

微服务间如何进行用户信息传递?只能依靠接口调用时显式通过参数传递吗?能否在传递过程中无感知呢?

本章代码已分享至Gitee: https://gitee.com/lengcz/springcloudalibaba01.git

第一节 微服务间用户信息传递问题

微服务由于跨服务了,我们如何在中间的某个服务知道当前的用户信息呢?
例如下面,用户下单的请求,用户的请求经过了 gateway>> order >> product,如果在order的时候,我们怎么样知道用户是谁?难道要请求接口传参时传递参数(用户身份信息)吗?

第二节 解决思路

请求流程

  1. 用户通过header传递用户身份信息,请求发送到gateway
  2. gateway 将请求路由到 order
  3. order调用product

我们知道一个微服务的请求有入口,也有出口(调用)其它微服务,请求进入时,可以通过filter进行拦截,
而调用其它请求时,我们可以将用户信息传递给被调用者(微服务),我们只需要通过AOP的思想,在入口处捕获用户信息,在出口时将用户信息传递出去,这样就保证了全链路都能获取到客户端的身份信息。当我们所有的微服务都在前后插入了接收和传出的操作,那么全链路就能够获取到用户信息了。

第三节 实践操作(基于Feign)

我们的用户信息传递是所有微服务的,因为所有微服务模块都需要进行用户信息传递,因为我们将实现定义在common模块

order,product,user均引用common

1. 调用链路说明

Feign的远程调用是Http的方式,所以我们只需要Filter 在入口取数据,在RequestInterceptor 出口时传递数据即可。

2. shop-common改造

  1. 引入依赖
 <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
  1. 定义ThreadLocal进行线程共享
package com.lcz.userinfo;
public class UserInfoContext 
    private static ThreadLocal<UserInfo> userInfo = new ThreadLocal<>();

    public static UserInfo getUser() 
        return (UserInfo) userInfo.get();
    

    public static void setUser(UserInfo user) 
        userInfo.set(user);
    
    
    public static void remove()
        userInfo.remove();
    

  1. 定义用户实体
package com.lcz.userinfo;
import lombok.Data;
import java.io.Serializable;

@Data
public class UserInfo implements Serializable 
    private Integer uid; //用户id
    private String username;//用户名

  1. 定义常量类
package com.lcz.userinfo;

public class UserInfoConstant 
    public static final String DUBBO_USER_KEY="DUBBO_USER_INFO";

    public static final String Authorization="USER_INFO";

  1. 编写拦截器
package com.lcz.userinfo;

import com.alibaba.fastjson.JSON;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

@Slf4j
public class TransmitUserInfoFeighClientIntercepter implements RequestInterceptor 

    public TransmitUserInfoFeighClientIntercepter() 
    

    @Override
    public void apply(RequestTemplate requestTemplate) 
        //从应用上下文中取出user信息,放入Feign的请求头中
        UserInfo user = UserInfoContext.getUser();
        log.info("传递用户信息:"+JSON.toJSONString(user));
        if (user != null) 
            try 
                String userJson = JSON.toJSONString(user);
                requestTemplate.header(UserInfoConstant.Authorization,new String[]URLDecoder.decode(userJson,"UTF-8"));
             catch (UnsupportedEncodingException e) 
                log.error("用户信息设置错误",e);
            finally 
                UserInfoContext.remove();
            
        
    

  1. 编写过滤器
package com.lcz.userinfo;

import com.alibaba.fastjson.JSON;
import com.lcz.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

@Slf4j
public class TransmitUserInfoFilter implements Filter 

    public TransmitUserInfoFilter() 
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
        this.initUserInfo((HttpServletRequest)request);
        chain.doFilter(request,response);
    

    private void initUserInfo(HttpServletRequest request)
        String userJson = request.getHeader(UserInfoConstant.Authorization);
        log.info("接收用户信息:"+userJson);
        if (StringUtils.isNotBlank(userJson)) 
            try 
                userJson = URLDecoder.decode(userJson,"UTF-8");
                UserInfo userInfo = (UserInfo) JSON.parseObject(userJson,UserInfo.class);
                //将UserInfo放入上下文中
                UserInfoContext.setUser(userInfo);
             catch (UnsupportedEncodingException e) 
                log.error("init userInfo error",e);
            
        
    

  1. 编写注解实现类
package com.lcz.userinfo;

import org.springframework.context.annotation.Bean;
public class EnableUserInfoTransmitterAutoConfiguration 

    public EnableUserInfoTransmitterAutoConfiguration() 
    

    @Bean
    public TransmitUserInfoFeighClientIntercepter transmitUserInfo2FeighHttpHeader()
        return new TransmitUserInfoFeighClientIntercepter();
    

    @Bean
    public TransmitUserInfoFilter transmitUserInfoFromHttpHeader()
        return new TransmitUserInfoFilter();
    

8 . 编写Enable注解,实现注解式注入

package com.lcz.userinfo;

import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(EnableUserInfoTransmitterAutoConfiguration.class)
public @interface EnableUserInfoTransmitter 

3. 开启注解

在order,product,user模块开启用户信息传递,使用@EnableUserInfoTransmitter

注意: common公共模块修改了代码,注意将common模块重新maven install发布到本地仓库

4. 启动服务器测试

回到我们的流程图,下单流程有这些微服务参与了,我们启动这些服务。只要我们在product中能够获取到用户信息,就证明用户信息传递成功了。

发起下单请求,由于我们的身份信息通过header传递,我们需要先生成一个格式正确的身份,再写入到header进行传递

    @Test
    public void testUser() throws UnsupportedEncodingException 
        UserInfo user=new UserInfo();
        user.setUid(111);
        user.setUsername("xiaowang");
        String str=  URLEncoder.encode(JSONObject.toJSONString(user),"UTF-8");
        System.out.println(str);
    

生成的信息

%7B%22uid%22%3A111%2C%22username%22%3A%22xiaowang%22%7D

将这个token(身份信息)放在header中发送下单请求

我们再product微服务的日志中,可以输出用户信息,表明了用户信息从api-gateway >> order >> product 这个流程调用中,用户信息传递成功了。

第四节 dubbo传递用户信息

在第三节,我们的改造只适合微服务之间基于feign的调用,才能传递用户信息到其它微服务。而事实上,我们现在很多微服务之间的调用是基于dubbo的,微服务之间如何通过dubbo 传递用户信息呢?
我们的解决思路依然一样的,需要微服务的前后进行插板,这样就能保证微服务通过dubbo调用时,也能实现跨服务传递用户身份了。

1. 调用链路说明

通过官方的代码架构调用链路图,我们可以知道dubbo远程调用都需要经过Filter(全类名org.apache.dubbo.rpc.Filter),因此我们只需要在消费方和生产方两边的Filter进行附带数据传输,即可实现信息的隐式传递。

我们利用org.apache.dubbo.rpc.Filter来拦截信息,并通过其传递用户信息,而接收方可以通过org.apache.dubbo.rpc.Filter接收信息

2. shop-common 改造

基于前面的代码,我们继续改造。

  1. 引入dubbo的依赖
<!--引入dubbo-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
        </dependency>
  1. 消费者传入用户信息
package com.lcz.userinfo;

import com.alibaba.dubbo.common.extension.Activate;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.rpc.*;

@Slf4j
@Activate(group = UserInfoConstant.CONSUMER)
public class UserInfoConsumerFilter implements org.apache.dubbo.rpc.Filter 


    @Override
    public Result invoke(Invoker<?> invoker, org.apache.dubbo.rpc.Invocation invocation) throws RpcException 
//        log.info("消费方:------");
        try
            UserInfo userInfo = UserInfoContext.getUser();
            if (null == userInfo) 
                return invoker.invoke(invocation);
            
            invocation.getObjectAttachments().put(UserInfoConstant.DUBBO_USER_KEY, userInfo);
            return invoker.invoke(invocation);
        finally 
            UserInfoContext.remove();
        
    

  1. 生产者接收用户信息
package com.it.userinfo;

import com.alibaba.dubbo.common.extension.Activate;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcException;

@Slf4j
@Activate(group = CommonConstants.PROVIDER)
public class UserInfoProviderFilter implements org.apache.dubbo.rpc.Filter 

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException 
//        log.info("生成方:------");
        Object userInfo = invocation.getObjectAttachment(UserInfoConstant.DUBBO_USER_KEY);
        if (null != userInfo) 
            UserInfoContext.setUser((UserInfo) userInfo);
        
        return invoker.invoke(invocation);
    

  1. 添加配置文件,META-INF/dubbo目录下,创建com.alibaba.dubbo.rpc.Filter文件,内容如下
userInfoConsumerFilter=com.lcz.userinfo.UserInfoConsumerFilter
userInfoProviderFilter=com.lcz.userinfo.UserInfoProviderFilter

3. 测试

第五节 Feign和Dubbo跨服务传递用户信息

基于我们上面的改造,各微服务不管使用Feign调用还是Dubbo调用,都可以实现用户信息的跨服务传递,解决了无法获取用户身份的问题。让跨服务之间,可以直接使用UserInfoContext.getUser(),具有单体应用的信息获取能力

UserInfo userInfo = UserInfoContext.getUser(); //获取用户信息

同时,直接传递用户身份信息,基于这个用户信息的传递思想,我们可以实现链路跟踪(Sleuth),也可以在gateway网关封装好某些内容,传递一些request信息,比如客户端IP,用户的权限。

当然了,上面的demo直接传递用户信息是不具有防伪性,我们可能使用JWT相关的技术进行鉴权等处理,在穿越网关时,将其转换为明文可操作的UserInfo,这样就保证了传递的用户信息的安全可靠性。

关于错误

如果打包common时,提示repackage failed: Unable to find main class
可以配置

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <mainClass>none</mainClass>     <!-- 取消查找本项目下的Main方法:为了解决Unable to find main class的问题 -->
                <classifier>execute</classifier>    <!-- 为了解决依赖模块找不到此模块中的类或属性 -->
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

SpringCloudAlibaba微服务分布式架构

一、SpringCloudAlibaba简介

待更新

二、Nacos概述

1、什么是nacos?

        一个更易于构建云原生应用的动态服务发现配置管理服务管理平台,相当于注册中心 + 配置中心的组合,等价于Eureka+Config +Bus。

2、能干嘛?

  • 替代Eureka做服务注册中心
  • 替代config做服务配置中心 

3、去哪下?      

        官网下载:点击访问

 这里我下载的是Windows版本,下载完成后进入文件夹的bin目录下启动nacos:

4、怎么运行访问?

上面我们已经将 Nacos 初步安装好了,在启动时需要注意以下问题:

  • 如果我们直接通过cmd启动startup.cmd 发现能启动但是会报错,因为nacos默认启动方式是集群,所以我们需要更改为单机模式 startup.cmd -m standalone

  • 这个时候可以正常启动了,但是我们使用Nacos是用来搞微服务的,为什么还要搞单机呢?
    Nacos默认是以集群方式启动的,但是对于初学者来说可能是初次安装没有集群环境需要进行一些配置,在入门阶段我们还是以单机为主进行练手。
  • 配置集群模式启动

        将cluster.conf.example文件复制一份去掉.example后缀,然后进入文件修改,添加端口,然后就可以通过 startup.cmd 进行启动。

  • 访问 localhost:8848/nacos

 三、Nacos之服务注册中心

官方文档:点击查看

1、服务提供者模块(建moudle、改pom、写yml、主启动、业务类)

(1)创建moudle:cloudalibaba-provider-payment9001

(2)改pom(添加依赖):

  • 父pom(公用):
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.1.0.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
  • 子pom:
<!--SpringCloud ailibaba nacos -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

(3)编写配置文件 application.yml

server:
  port: 9001

spring:
  application:
    name: nacos-payment-provider #注册服务名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址
#打开全部监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

(4)主启动类 PaymentMain9001.java

@EnableDiscoveryClient  //开启服务提供者或消费者,客户端的支持,用来注册服务或连接到注册中心
@SpringBootApplication  //主启动
public class PaymentMain9001

    public static void main(String[] args) 
            SpringApplication.run(PaymentMain9001.class, args);
    

(5)编写业务类 PaymentController.java

@RestController
public class PaymentController

    @Value("$server.port")
    private String serverPort;

    @GetMapping(value = "/payment/nacos/id")
    public String getPayment(@PathVariable("id") Integer id)
    
        return "nacos registry, serverPort: "+ serverPort+"\\t id"+id;
    

(6)测试

  • 启动Nacos和cloudalibaba-provider-payment9001模块。
  • 访问http://localhost:9001/payment/nacos/1
  • 在服务列表中发现注册到Nacos服务注册中心中的服务:nacos-payment-provider

 2、服务提供者模块(副本,测试nacos的负载均衡功能)

(1)复制一份9001模块的代码改一下文件名和端口号,不过多赘述;

(2)在同一服务名下存在两个健康的实例。

 3、服务消费者(建moudle、改pom、写yml、主启动、业务类)

(1)创建moudle:cloudalibaba-consumer-nacos-order83

(2)改pom(添加依赖):

<!--SpringCloud ailibaba nacos -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

(3)编写配置文件 application.yml

server:
  port: 83


spring:
  application:
    name: nacos-order-consumer  #注册服务名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848


#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://nacos-payment-provider 
 

(4)主启动类 OrderNacosMain83.java

@EnableDiscoveryClient  //开启服务提供者或消费者,客户端的支持,用来注册服务或连接到注册中心
@SpringBootApplication  //主启动
public class OrderNacosMain83

    public static void main(String[] args)
    
        SpringApplication.run(OrderNacosMain83.class,args);
    
 

(5)编写业务类,需要添加一个RestTemplate的配置类:

@Configuration
public class ApplicationContextBean

    //这里就是创建一个轮询负载均衡的RestTemplate Bean
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate()
    
        return new RestTemplate();
    
@RestController
public class OrderNacosController

    @Resource
    private RestTemplate restTemplate;

    @Value("$service-url.nacos-user-service")
    private String serverURL;

    @GetMapping("/consumer/payment/nacos/id")
    public String paymentInfo(@PathVariable("id") Long id)
    
        return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
    


(6)测试

 4、服务注册中心对比

这里先来了解一下什么是CAP模型?

  • 一致性(Consistency)同一时刻同一请求的实例返回结果相同,所有数据具有强一致性。
  • 可用性(Availability):所有实例的读写请求在一定时间内可以得到正确的响应。
  • 分区容错性(Partition tolerance):在网络异常的情况下系统仍能提供正常的服务。

        这就是CAP原则,又被称为CAP定理,但是三个特性不可能同时被满足,所以分布式系统设计要考虑的是在满足P(分区容错性)的前提下选择C(一致性)还是A(可用性),即:CP或AP

        CP:

        AP:

服务注册与发现框架CAP模型控制台管理社区活跃度
EurekaAP支持低(2.x版本闭源)
ZookeeperCP不支持
ConsulCP支持
NacosAP/CP支持

四、Nacos之服务配置中心

1、nacos作为配置中心----基本配置

(1)创建子模块: cloudalibaba-config-nacos-client3377

(2)编写pom文件:

<!--nacos-config-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

(3)编写application.yml:

        Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取之后,才能保证项目的正常启动。

        bootstrap.yml

server:
  port: 3377
spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #nacos服务注册中心地址
      config:
        server-addr: localhost:8848 #nacos作为配置中心地址
        file-extension: yaml #指定yaml格式的配置

        application.yml

spring:
  profiles:
    active: dev # 表示开发环境

(4)创建主启动类:

@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientMain3377

    public static void main(String[] args) 
            SpringApplication.run(NacosConfigClientMain3377.class, args);
    

(5)编写业务类:

@RestController
@RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class ConfigClientController

    @Value("$config.info")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo() 
        return configInfo;
    

2、在Nacos中添加配置信息

(1)Nacos中的匹配规则:官方文档

(2)在Nacos中新增配置

  • 配置文件命名规则

  •  Nacos 配置页面对应:

 (3)测试,启动3377模块

        发起请求访问: localhost:3377/config/info

        修改配置文件内容,如果可以动态刷新,那么Nacos Config中心配置成功!

3、Nacos作为配置中心----分类配置(针对多环境多项目管理)

        首先先来说一下为什么会产生分类配置?它在我们实际的开发中又起到了一个什么样的作用呢?最后我们来实际测试一下分类配置该如何使用。

  • 在实际的开发中,一个系统通常情况下会有dev开发环境、test测试环境、prod生产环境。那么我们该如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
  • 一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境......我们该怎么对这些微服务配置进行管理呢?

        针对上述问题,nacos提供了一种便捷的解决方案,我们先来看一下它的图形化管理界面:

(1)Namespace+Group+Data ID三者之间的关系

        如上图,简单来说,这种分类配置的设计思想类似于java中的包名-类名-...最外层的namespace用来区分部署环境,Group和DataID逻辑上区分两个目标对象(默认情况下:Namespac=public,Group=DEFAULT_GROUP,默认Cluster【集群】是DEFAULT)。

  • Namespace主要用来实现隔离(也就是nacos默认命名空间为public)。

        假设我们现在有三个环境:开发、测试、生产环境,那么我们就可以创建三个Namespace,不同的Namespace之间是相互隔离的。

  • Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去。
  • Service就是微服务。一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是指对指定微服务的一个虚拟划分。

        比如说为了容灾,将Service微服务分别部署在黄杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名称(GZ),还可以让同一机房的微服务相互调用,以提升性能。

  • 最后是Instance,就是微服务的实例。

(2)DataID配置

首先我们先使用【默认空间+默认分组】来新建dev和test两个DataID。

在yml配置文件中通过spring.profile.active属性就能进行多环境下配置文件的读取:

 (3)Group分组方案(实现环境的区分

 在config下增加一条group的配置即可。 可配置为DEV_GROUP或TEST_GROUP

 

 (4)Namespace命名空间

 新建dev和test的Namespace:

回到服务列表,可以发现在public的基础上多了dev和test:

 进入配置列表dev命名空间下创建三个配置文件:

         通过bootstrap+application实现命名空间区分开发环境,在config下增加一条namespace的配置即可。其值即为命名空间的ID

五、Nacos集群和持久化配置(重要)

篇幅过长,见下一篇文章:https://blog.csdn.net/friggly/article/details/126832789

以上是关于springcloudalibaba架构(31):SpringCloud实现用户信息在微服务之间传递(Feign和Dubbo)的主要内容,如果未能解决你的问题,请参考以下文章

微服务架构技术选型

前后端分离SpringBoot+SpringCloudAlibaba+VUE一 || 项目架构简介

SpringCloudAlibaba微服务分布式架构

SpringCloudAlibaba微服务组件Nacos注册中心

Spring Cloud Alibaba介绍

聊聊Spring Cloud Alibaba的架构思想