Blage's Coding Blage's Coding
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
  • 网关

    • Api-gateway-core
      • 1、服务端处理Http请求
      • 2、代理RPC泛化调用
        • 网关接口映射功能
        • 泛化调用
        • 缓存设计
        • 思考
      • 3、重构:使用分治方法重构会话
      • 4.重构:引入数据源重构RPC连接
      • 5.HTTP请求参数解析
        • 请求参数解析
        • RPC接口入参设置
      • 6.重构:引入执行器执行RPC泛化调用
      • 7.Shiro+JWT
        • JWT认证流程
        • Shiro核心原理
        • 会话鉴权融入网关
      • 8.排除内嵌Tomcat运行war包&排除数据源启动
    • Api-gateway-center
    • Api-gateway-assist
    • Api-gateway-sdk
    • 部署配置
  • 抽奖

  • 电商

  • 外卖

  • 项目笔记
  • 网关
phan
2023-05-15
目录

Api-gateway-core

# Api-gateway-core

# 1、服务端处理Http请求

功能:实现将Http连接请求过程引入Netty服务端IO。核心在于通道需要配置Netty自带的Http解码器和编码器,获取Http对象后用户可以进一步处理。

channelRead0:事件触发后,封装DefaultFullHttpResponse响应对象,包括配置响应体和响应头信息,以及解决跨域问题。另外,channelRead0事件中不需要关心释放资源。

SimpleChannelInboundHandler<FullHttpRequest>:事件处理器接口,指定FullHttpRequest作为接收的消息对象。在页面和服务端之间的通道取出http请求对象,可以通过uri()方法拿到后面资源标识路径。

image-20230420104118203

# 2、代理RPC泛化调用

# 网关接口映射功能

SpringBoot在Controller中通过注解RequestMapping,把Http请求路径映射到每个微服务具体方法,但同时也增加了维护成本,服务改变那么访问路径也需要改变。

此处采用Netty+Dubbo的全新微服务架构方式,所有包装提供的微服务只需要通过Dubbo暴露接口方法,不需要配置请求路径。当用户发起http请求时会发送给Api网关(此处网关基于Netty服务端与所有用户请求建立连接),网关会根据配置的映射规则,向微服务注册中心发送Dubbo-RPC请求,调用到对应的服务接口方法。因此核心就是配置HTTP和RPC的映射规则。

image-20230421140658162

# 泛化调用

  • 获取泛化服务调用对象

DubboBootstrap对象提供好应用名称、注册中心配置、引用服务配置,进行初始化和远程连接后,就可以从Dubbo配置缓存中取出该接口的泛化调用代理对象GenericService。

消费服务时只需要调用GenericService#$invoke方法,并提供方法名、入参类型、入参对象,即可调用RPC服务接口的方法。

  • 泛化调用对象的二次封装

每个RPC服务的实现和方法名称都是不同的,因此不能直接调用GenericService#$invoke,可能还需要拦截方法实现增强。因此这里对genericService代理对象二次封装,再创建一个新的代理对象。

GenericReferenceProxyFactory:代理工厂采用Cglib的动态代理方式,其中设置了两个代理接口,一个是统一的泛化调用接口IGenericReference,给整个网关调用提供一个统一的方法。而另一个接口是手动生成创建的RPC描述性接口,标记当前代理对象间接代理的是哪个RPC接口方法。

public IGenericReference newInstance(String method) {
    return genericReferenceMap.computeIfAbsent(method, k -> {
        //泛化调用
        GenericReferenceProxy genericReferenceProxy = new GenericReferenceProxy(genericService, method);
        //创建接口,指定名称,返回类型,行参类型
        InterfaceMaker interfaceMaker = new InterfaceMaker();
        interfaceMaker.add(new Signature(method, Type.getType(String.class), new Type[]{Type.getType(String.class)}), null);
        Class interfaceClass = interfaceMaker.create();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Object.class);
        //设置统一泛化调用接口+手动创建RPC接口
        enhancer.setInterfaces(new Class[]{IGenericReference.class, interfaceClass});
        enhancer.setCallback(genericReferenceProxy);
        return (IGenericReference) enhancer.create();

    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

GenericReferenceProxy:泛用代理对象,实现了MethodIntercptor接口,用于增强RPC接口方法(设置搜集入参信息),并设置代理对象的回调setCallback。当网关的泛化代理对象调用$invoke后,会进入到intercept进行拦截增强,并通过外部的method方法搜集入参信息,最后通过genericService.$invoke调用真正的RPC接口。

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    Class<?>[] parameterTypes = method.getParameterTypes();
    String[] parameters = new String[parameterTypes.length];
    for (int i = 0; i < parameterTypes.length; i++) {
        parameters[i] = parameterTypes[i].getName();
    }
    return genericService.$invoke(methodName, parameters, objects);
}
1
2
3
4
5
6
7
8
9

# 缓存设计

①Configuration:因为ApplicationConfig,RegistryConfig,ReferenceConfig三种类型对象创建成本高,因此需要存放入内存Map中管理。后面需要从Spring配置文件当中读取。

②ReferenceConfigCache:因为ReferenceConfig对象是一个比较重的实例,其中init初始化方法里实现了通过与注册中心与提供者连接创建代理的过程。因此需要创建缓存,根据referenceConfig对象生成的key获取对应的泛化代理对象。最终Dubbo-RPC代理对象是通过Javassist字节码增强技术创建。

此处key的生成规则是group/版本/接口,可以根据业务和场景需求通过SPI修改生成策略。

③网关代理工厂中也设置了Map作为网关代理对象的单例池,根据方法名获取对应的代理对象。

# 思考

①RPC接口调用的入参在http请求路径上(REST)如何获取:通过uri()解析获得请求参数。form表单数据呢?

②网关代理单例池存放新创建的代理对象时,出现同名的RPC方法调用时会覆盖。

③SPI在dubbo的使用,加载ConfigManager,修改注册中心生成key方法

# 3、重构:使用分治方法重构会话

  • 目标

对工程进行重构,拆分成bind、mapping、session、socket四部分。

①bind:负责网关代理封装与创建。(移除了泛化调用部分)

②mapping:HttpStatement对象建立了HTTP网关(uri和请求方式)到RPC服务(应用、接口、方法名)的映射关系。并且都采用String类型,避免使用比较重的Dubbo实例。

③session:会话中间层,包括缓存、泛化调用获取。

④socket:负责Netty服务端网络通信。

整个流程首先session准备配置,socket启动服务端,从session中通过mapperRegistry获取映射器对象(网关代理对象),通过代理对象调用RPC接口服务。

image-20230421212631236

  • 细节

各个模块功能是隔离的却又不是完全孤立的,通过调用依赖的方式实现模块之间的通信。

DefaultGatewaySessionFactory:socket通信模块启动Netty服务端依赖于session的会话工厂。

MapperRegistry:session模块Configuration依赖于bind模块的映射器注册中心。getMapper方法又会基于GatewaySession中的configuration对象创建新的代理实例。

另外bind代理部分的缓存Map中的key全部改为用uri来代替,解决了RPC接口方法重名的问题。

核心看MapperProxy#intercept拦截方法的实现。此处把RPC远程连接注册中心并从Dubbo缓存取出GenericReference对象,以及通过泛化调用对象调用RPC接口$invoke两个过程合并在一起,放到MapperMethod实现。

  • 问题

①MappingRegistry和Configuration存在循环依赖问题。HttpStatement在初始化阶段就准备好了,因此可以直接放入Configuration缓存,不需要在添加映射器时再进行,所以MappingRegistry注册中心不需要依赖Configuration。

②将泛化调用对象的获取和RPC接口调用绑定的问题在于,GatewaySession#get方法没有缓存GenericReference对象,而获取GenericReference对象需要远程调用,是一个很重的方法。因此用户第一次调用服务会非常慢,此外每次调用相同RPC接口都会进行远程调用,十分损耗系统资源。

所以这个地方必须要拆分,把GenericReference对象获取和初始化过程放到Netty服务端启动之前,和Configuration一起进行初始化。不过这种做法相当于一次性往内存加载所有RPC服务,拿空间换时间,具体需要结合时机场景考虑。

# 4.重构:引入数据源重构RPC连接

  • 目标

把RPC远程连接获取GenericService泛化调用对象的部分从DefaultGatewaySession会话中抽离出来,把RPC抽象成一种连接资源,整合放入数据源管理当中。这样针对不同的资源请求,可以扩展不同的数据源连接方式。而在Gatewaysession中需要做的,只需要初始化DataSource数据源,并且在需要调用远程数据或者远程服务的地方getConnection获取数据源连接,进一步拿到数据执行对应的业务处理即可。

  • 细节

整个依赖关系:DefaultGatewaySession—>(DateSourceFactory—>DateSource)—>Connection

UnpooledDataSource:根据网关中的HTTP映射信息对象HttpStatement,从配置Configuration中取出建立RPC连接所需要的Dubbo配置对象,并交给连接对象(DubboConnection)进行实例化。

DataSourceType:定义数据源类型

DubboConnection:根据数据源传入的Dubbo配置对象,获取泛化调用对象并创建连接。execute方法执行RPC服务。

数据源的引入:DefaultGatewaySessionFactory#openSession中初始化数据源工厂,并通过工厂获取数据源对象,交给DefaultGatewaySession保存。

  • 问题

①RPC服务调用交给DefaultGatewaySession不合适,应该直接由Mapper代理调用connection对象执行。

# 5.HTTP请求参数解析

目标:GatewayServerHandler接收到HTTP请求后,针对HTTP的GET/POST请求类型,以及该请求的参数类型Content-Type进一步细化参数解析过程。

# 请求参数解析

RequestParser#parse:请求参数解析。核心基于FullHttpRequset对象,根据不同的请求方式取出数据。

  • GET方式:数据可以直接从uri中解析获取。

  • POST方式:前端数据会以不同的请求体方式提交,因此需要区分请求头中不同的Content-Type信息。另外解析参数类型Content-Type时,过滤时需要考虑到分号后面的boundary或charset。

    • multipart/form-data----form-data
    • application/json----raw(json)
    • application/x-www-from-urlencoded----x-www-from-urlencoded

# RPC接口入参设置

通过泛化对象调用RPC接口时,需要根据不同的入参类型传入正确的入参对象。如果传入的是JAVA八大基本数据类型,那么直接把数据填入到入参对象数组当中;如果传入的是自定义类型对象,那么需要把该对象的所有成员属性封装成一个Map对象,作为RPC接口的一个入参对象。

此处下面的params已经提前转化为Map对象。

return connection.execute(methodName, new String[]{parameterType}, new String[]{"name"}
	,SimpleTypeRegistry.isSimpleType(parameterType)?params.values().toArray(): new Object[]{params});
1
2

另外,此处暂且规定RPC接口只传入一个入参。如果是多入参的情况下,需要拿到RPC接口的Method对应的所有入参参数名,代理调用时需要根据入参名把入参对象放入对应的位置。另外没有解决DTO转化问题。

# 6.重构:引入执行器执行RPC泛化调用

  • 目标

将Session会话中获取连接源,并通过连接源执行泛化调用的整个执行过程抽离出来,由连接器负责。

会话只需要串联上下文,包括①根据外部uri信息创建数据源②获取执行器执行结果返回给外部HTTP响应。

  • 细节

BaseExecutor#exec:采用模板模式定义整个执行过程:①准备入参信息②执行器通过连接源对象执行泛化调用③封装执行结果对象GatewayResult。

DefaultGatewaySessionFactory#openSession:根据连接源创建执行器对象,根据外部uri开启会话。最后把执行器存入会话中。

ResponseParser:将执行结果统一封装HTTP响应对象。

executordrawio

# 7.Shiro+JWT

HTTP请求进来时需要校验所携带的token,从而够验证当前HTTP请求的有效性,防止恶意请求攻击网关。

# JWT认证流程

用户在第一次登录成功后,后端会通过JWT使用服务器私钥signingKey对用户的uId进行加密编码,得到一串密文作为token,并把token在第一次登录验证成功后(一般是数据库密码匹配)返回给用户。

也就是说uId与token可以理解是一对明文密文对,只有通过服务器(校验方)的私钥才能正确解密。

每次发送HTTP请求时,先从header头部取出uId和token,然后shiro会对明文密文对进行认证校验,通过Jwt的私钥对token进行解密(解码),如果结果与明文(uId)不一致,那么说明校验信息很可能经过伪造,校验失败认证不通过。

# Shiro核心原理

  • 认证授权调用链

subject->securityManager->..->Realm。前端校验信息AuthenticationToken最终会传递到Realm对象进行认证。认证时首先调用doGetAuthenticationInfo方法获取真正的账号密码信息,并封装成AuthenticationInfo对象,然后执行doCredentialsMatch方法将前端token信息和数据库账户info信息进行匹配。

而目前来看网关系统①使用认证功能②shiro通过Jwt来对token进行验证,不走数据库

GatewayAuthorizingRealm:自定义Realm对象,继承了AuthorizingRealm,重写doGetAuthenticationInfo方法,在方法中使用Jwt解码token进行校验,如果校验失败则直接抛出异常。暂不走数据库认证。

GatewayAuthorizingToken:实现了AuthenticationToken接口,用于封装前端传入的uId和token。

  • 主流程

①工厂对象配置自定义Realm的配置,获取SecurityManager对象。

②在SecurityUtils中设置管理对象,并获取Subject对象。

③Subject#login:将前端校验信息封装成Shiro的token对象,并传入SecurityManager内部,进行认证授权。

public AuthService() {
    IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");

    SecurityManager securityManager = factory.getInstance();

    SecurityUtils.setSecurityManager(securityManager);
    this.subject = SecurityUtils.getSubject();
}
@Override
public boolean validate(String id, String token) {
    try {
        subject.login(new GatewayAuthorizingToken(id, token));
        return subject.isAuthenticated();
    } finally {
        subject.logout();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 会话鉴权融入网关

  • 核心

AuthService作为Shiro鉴权的门面对象,暴露validate方法实现认证功能。

另外对Netty服务端的Headler处理器进行重构,拆分成三个模块:

①GatewayServerHandler:校验uri,并根据uri拿到网关映射器HttpStatement,存入通道的AttributeMap。

②AuthorizationHandler:从request头部取出用户校验信息,进行接口鉴权。

③ProtocolDataHandler:解析获取HTTP请求的数据信息,并执行泛化调用。

  • 细节

HttpStatement#isAuth:后续鉴权后,考虑到游客用户对基本欢迎页信息的get请求不需要认证校验,因此扩充auth字段用来判断当前HTTP请求是否需要进行鉴权。

消息对象:SessionResult作为执行泛化调用后的结果对象。GatewayResultMessage作为网关响应消息对象,用来反馈给用户的整个HTTP请求的结果。在HTTPResponse响应报文解析器中,会把SessionResult封装成GatewayResultMessage对象并填入data字段。

通道多处理器:①当前通道处理器处理完后,通过fireChannelRead将request请求传递给下一个处理器的read事件,当前通道放行,同时执行request#retain引用计数加一。

②通道AttributeMap:把HttpStatement对象存放在通道的AttributeMap中(ctx之间的Map是隔离的),相当于全局变量,整个通道的所有处理器都可以共享。channel.attr()通过指定key获取对应的Attribute对象。

# 8.排除内嵌Tomcat运行war包&排除数据源启动

  • SpringBoot部署项目有两种方式:

①jar包

打包jar包,使用spring-boot-starter-web内置tomcat,直接通过java-jar运行。

②war包

如果需要配置定制化Tomcat服务器,那么需要使用外置的Tomcat服务器部署war包。把war包打包后放入到Tomcat文件夹下,然后启动tomcat进行发布。

因此引入Springboot依赖后需要排除掉内置的tomcat,否则会冲突。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
1
2
3
4
5
6
7
8
9
10

或者引入spring-boot-starter-tomcat指明scope:provided,依赖由容器或者jdk提供。

  • 排除数据源

如果yml配置中没有配置数据源,那么导入的相关jar包里面如果有用到数据库,就会报错,因此springboot启动时需要排除数据源。

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
1
编辑 (opens new window)
#网关
上次更新: 2023/12/15, 15:49:57
Api-gateway-center

Api-gateway-center→

Theme by Vdoing | Copyright © 2023-2024 blageCoder
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式