当前位置:首页 > 科技  > 软件

深入探讨API网关APISIX中自定义Java插件在真实项目中的运用

来源: 责编: 时间:2024-01-18 17:40:18 149观看
导读环境:APISIX3.4.1 + JDK11 + SpringBoot2.7.12一. APISIX简介APISIX 网关作为所有业务的流量入口,它提供了动态路由、动态上游、动态证书、A/B 测试、灰度发布(金丝雀发布)、蓝绿部署、限速、防攻击、收集指标、监控报警

环境:APISIX3.4.1 + JDK11 + SpringBoot2.7.12dOb28资讯网——每日最新资讯28at.com

一. APISIX简介

APISIX 网关作为所有业务的流量入口,它提供了动态路由、动态上游、动态证书、A/B 测试、灰度发布(金丝雀发布)、蓝绿部署、限速、防攻击、收集指标、监控报警、可观测、服务治理等功能。dOb28资讯网——每日最新资讯28at.com

为什么使用APISIX?dOb28资讯网——每日最新资讯28at.com

  1. 高性能和可扩展性:APISIX是基于Nginx和OpenResty构建的,具有高性能和可扩展性。它支持动态路由、限流、缓存、认证等功能,并可以通过插件扩展其他功能。
  2. 社区活跃,易于使用:APISIX的社区非常活跃,提供了完整的文档,使其易于使用。此外,它也支持类似于Kubernetes中的自动化部署,适合对网络部署和管理要求高的团队。
  3. 处理API和微服务流量的强大工具:Apache APISIX是一个动态、实时、高性能的开源API网关,可以快速、安全地处理API和微服务流量,包括网关、Kubernetes Ingress和服务网格等。全球已有数百家企业使用Apache APISIX处理关键业务流量,涵盖金融、互联网、制造、零售、运营商等各个领域。
  4. 云原生技术体系统一:APISIX从底层架构上避免了宕机、丢失数据等情况的发生。在控制面上,APISIX使用了etcd存储配置信息,这与云原生技术体系更为统一,能更好地体现高可用特性。
  5. 实时配置更新:使用etcd作为存储后,APISIX可以在毫秒级别内获取到最新的配置信息,实现实时生效。相比之下,如果采用轮询数据库的方式,可能需要5-10秒才能获取到最新的配置信息。

综上所述,APISIX是一个优秀的开源API网关,具有高性能、可扩展性、社区活跃、易于使用等特点,并且能够处理API和微服务流量,避免宕机、丢失数据等问题,实现实时配置更新。因此,许多企业和团队选择使用APISIX作为其API网关解决方案。dOb28资讯网——每日最新资讯28at.com

二. APISIX安装

关于APISIX的安装参考官方文档即可dOb28资讯网——每日最新资讯28at.com


dOb28资讯网——每日最新资讯28at.com

APISIX安装指南dOb28资讯网——每日最新资讯28at.com


dOb28资讯网——每日最新资讯28at.com

https://apisix.apache.org/zh/docs/apisix/installation-guide/dOb28资讯网——每日最新资讯28at.com

我使用的docker部署dOb28资讯网——每日最新资讯28at.com

三. 需求说明

  1. 目的与背景:

由于安全需要,现有系统接口的请求数据需要加密(调用方必须加密传输)。dOb28资讯网——每日最新资讯28at.com

考虑到对现有系统的最小化影响,决定采用APISIX作为中间件,通过自定义Java插件来实现数据加密的功能。dOb28资讯网——每日最新资讯28at.com

  1. 功能需求:
  • 数据加密:插件需要能够接收并解析请求数据,然后对数据进行解密处理(解密后的数据再提交到上游服务)。dOb28资讯网——每日最新资讯28at.com

  • 安全性:加密算法和密钥管理应遵循业界最佳实践,确保数据安全。dOb28资讯网——每日最新资讯28at.com

  • 错误处理与日志记录:插件应具备良好的错误处理机制,并能够记录详细的日志,以便于问题排查。(这通过记录日志即可)dOb28资讯网——每日最新资讯28at.com

  1. 非功能需求:dOb28资讯网——每日最新资讯28at.com

  • 可维护性:插件代码应清晰、模块化,便于后续的维护和升级。dOb28资讯网——每日最新资讯28at.com

  • 可扩展性:考虑到未来可能的加密需求变化,插件应具备良好的扩展性。dOb28资讯网——每日最新资讯28at.com

四. 插件工作原理

apisix-java-plugin-runner 设计为使用 netty 构建的 TCP 服务器,它提供了一个 PluginFilter 接口供用户实现。用户只需关注其业务逻辑,而无需关注 apisix java 插件运行程序如何与 APISIX 通信的细节;它们之间的进程间通信如下图所示。dOb28资讯网——每日最新资讯28at.com

图片图片dOb28资讯网——每日最新资讯28at.com

核心运行原理

官方的包是基于springboot,所以它自身提供了一个CommandLineRunner类,该类会在Springboot容器启动完成后运行,也就是下面的地方执行:
dOb28资讯网——每日最新资讯28at.com

public class SpringApplication {  public ConfigurableApplicationContext run(String... args) {    // ...这里ApplicationContext等相关的初始化    callRunners(context, applicationArguments);  }}

核心Runner类

public class ApplicationRunner implements CommandLineRunner {  private ObjectProvider<PluginFilter> filterProvider;  public void run(String... args) throws Exception {    if (socketFile.startsWith("unix:")) {      socketFile = socketFile.substring("unix:".length());    }    Path socketPath = Paths.get(socketFile);    Files.deleteIfExists(socketPath);    // 启动netty服务    start(socketPath.toString());  }  public void start(String path) throws Exception {    EventLoopGroup group;    ServerBootstrap bootstrap = new ServerBootstrap();      // 判断使用什么channel      bootstrap.group(group).channel(...)    try {      // 初始化netty服务      initServerBootstrap(bootstrap);      ChannelFuture future = bootstrap.bind(new DomainSocketAddress(path)).sync();      Runtime.getRuntime().exec("chmod 777 " + socketFile);      future.channel().closeFuture().sync();    } finally {      group.shutdownGracefully().sync();    }  }  private void initServerBootstrap(ServerBootstrap bootstrap) {    bootstrap.childHandler(new ChannelInitializer<DomainSocketChannel>() {      @Override      protected void initChannel(DomainSocketChannel channel) {        channel.pipeline().addFirst("logger", new LoggingHandler())          //...          // 核心Handler          .addAfter("payloadDecoder", "prepareConfHandler", createConfigReqHandler(cache, filterProvider, watcherProvider))          // ...      }    });  }}

五. 插件开发

5.1 依赖管理

<properties>  <java.version>11</java.version>  <spring-boot.version>2.7.12</spring-boot.version>  <apisix.version>0.4.0</apisix.version>  <keys.version>1.1.4</keys.version></properties><dependencies>  <dependency>    <groupId>org.apache.apisix</groupId>    <artifactId>apisix-runner-starter</artifactId>    <version>${apisix.version}</version>  </dependency>  <!-- 封装了各类加解密功能如:SM,AES,RSA等算法-->  <dependency>    <groupId>com.pack.components</groupId>    <artifactId>pack-keys</artifactId>    <version>${keys.version}</version>  </dependency>  <dependency>    <groupId>com.fasterxml.jackson.core</groupId>    <artifactId>jackson-databind</artifactId>  </dependency>  <dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-web</artifactId>  </dependency></dependencies>

5.2 配置文件

这里的配置可有可无,都有默认值dOb28资讯网——每日最新资讯28at.com

cache.config:  expired: ${APISIX_CONF_EXPIRE_TIME}  capacity: 1000socket:  file: ${APISIX_LISTEN_ADDRESS}

5.3 启动类配置

@SpringBootApplication(scanBasePackages = { "com.pack", "org.apache.apisix.plugin.runner" })public class CryptoApisixPluginRunnerApplication {  public static void main(String[] args) {    new SpringApplicationBuilder(CryptoApisixPluginRunnerApplication.class).web(NONE).run(args);  }}

注意:关键就是上面的"org.apache.apisix.plugin.runner"包路径。dOb28资讯网——每日最新资讯28at.com

5.4 Filter开发

总共2个插件:dOb28资讯网——每日最新资讯28at.com

  • java插件
    该插件用来对数据解密。
  • LUA插件
    该插件用来改写请求body,因为在java插件中无法改写。

定义一个抽象类,实现了通过的功能dOb28资讯网——每日最新资讯28at.com

public abstract class AbstractDecryptPreFilter implements PluginFilter {  // 具体细节由子类实现  protected abstract void doFilterInternal(HttpRequest request, HttpResponse response, PluginFilterChain chain,     CryptModel cryptModel, CacheModel cache);  // 工具类专门用来读取针对插件的配置信息  @Resource  protected ConfigProcessor<BaseCryptoModel> configCryptoProcessor;  // 工具类专门用来处理加解密  @Resource  protected CryptoProcessor cryptoProcessor;  // 工具类专门用来判断路径匹配  @Resource  protected PathProcessor pathProcessor ;  // 是否开启了插件功能  protected boolean isEnabled(HttpRequest request, BaseCryptoModel cryptoModel) {    if (request == null || cryptoModel == null) {      return false;    }    return cryptoModel.isEnabled();  }  // 检查请求,对有些请求是不进行处理的比如OPTIONS,HEADER。  protected boolean checkRequest(HttpRequest request, CryptModel cryptModel, CacheModel cache) {    if (isOptionsOrHeadOrTrace(request)) {      return false ;    }    String contentType = request.getHeader("content-type") ;    logger.info("request method: {}, content-type: {}", request.getMethod(), contentType) ;    if (isGetOrPostWithFormUrlEncoded(request, contentType)) {      Optional<Params> optionalParams = this.pathProcessor.queryParams(request, cryptModel.getParams()) ;      if (optionalParams.isPresent() && !optionalParams.get().getKeys().isEmpty()) {        cache.getParamNames().addAll(optionalParams.get().getKeys()) ;        return true  ;      }      return false ;    }    String body = request.getBody() ;    if (StringUtils.hasLength(body)) {      Body configBody = cryptModel.getBody();      if (this.pathProcessor.match(request, configBody.getExclude())) {        return false ;      }      if (configBody.getInclude().isEmpty()) {        return true ;      } else {        return this.pathProcessor.match(request, configBody.getInclude()) ;      }    }    return false ;  }    private boolean isOptionsOrHeadOrTrace(HttpRequest request) {      return request.getMethod() == Method.OPTIONS ||         request.getMethod() == Method.HEAD ||         request.getMethod() == Method.TRACE ;    }      private boolean isGetOrPostWithFormUrlEncoded(HttpRequest request, String contentType) {        return request.getMethod() == Method.GET ||                (request.getMethod() == Method.POST && PluginConfigConstants.X_WWW_FORM_URLENCODED.equalsIgnoreCase(contentType));    }  // PluginFilter的核心方法,内部实现都交给了子类实现doFilterInternal  @Override  public final void filter(HttpRequest request, HttpResponse response, PluginFilterChain chain) {    BaseCryptoModel cryptoModel = configCryptoProcessor.processor(request, this);    CryptModel model = null ;    if (cryptoModel instanceof CryptModel) {      model = (CryptModel) cryptoModel ;    }    logger.info("model: {}", model);    Assert.isNull(model, "错误的数据模型") ;    CacheModel cache = new CacheModel() ;    // 是否开启了加解密插件功能 && 当前请求路径是否与配置的路径匹配,只有匹配的才进行处理    if (isEnabled(request, cryptoModel) && checkRequest(request, model, cache)) {      this.doFilterInternal(request, response, chain, model, cache);    }    chain.filter(request, response);  }  // 插件中是否需要请求正文  @Override  public Boolean requiredBody() {    return Boolean.TRUE;  }}

解密插件

@Component@Order(1)public class DecryptFilter extends AbstractDecryptPreFilter {  private static final Logger logger = LoggerFactory.getLogger(DecryptFilter.class) ;    @Override  public String name() {    return "Decrypt";  }  @Override  protected void doFilterInternal(HttpRequest request, HttpResponse response, PluginFilterChain chain,      CryptModel cryptModel, CacheModel cache    SecretFacade sf = this.cryptoProcessor.getSecretFacade(request, cryptModel) ;    String body = request.getBody() ;    if (StringUtils.hasLength(body)) {      logger.info("request uri: {}", request.getPath()) ;      // 解密请求body      String plainText = sf.decrypt(body);      request.setBody(plainText) ;      plainText = request.getBody() ;      // 下面设置是为了吧内容传递到lua脚本写的插件中,因为在java插件中无法改写请求body      request.setHeader(PluginConfigConstants.DECRYPT_DATA_PREFIX, Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8))) ;      request.setHeader(PluginConfigConstants.X_O_E, "1") ;      // 必须设置,不然响应内容类型就成了text/plain      request.setHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) ;    }  }  @Override  public Boolean requiredBody() {    return Boolean.TRUE;  }}

LUA插件

local ngx = ngx;local core = require "apisix.core"local plugin_name = "modify-body"local process_java_plugin_decrypt_data = "p_j_p_decrypt_data_"local x_o_e_flag = "x-o-e-flag"local schema = {}local metadata_schema = {}local _M = {    version = 0.1,    priority = 10,    name = plugin_name,    schema = schema,    metadata_schema = metadata_schema,    run_policy = 'prefer_route',}function _M.check_schema(conf)    return core.schema.check(schema, conf)endfunction _M.access(conf, ctx)  -- local cjson = require 'cjson'  -- ngx.req.read_body()  -- local body = ngx.req.get_body_data()  -- ngx.log(ngx.STDERR, "access content: ", body)endfunction _M.rewrite(conf, ctx)  local params, err = ngx.req.get_headers() --ngx.req.get_uri_args()  local flag = params[x_o_e_flag]  ngx.log(ngx.STDERR, "processor body, flag: ", flag)  if flag and flag == '1' then     local plain_data = params[process_java_plugin_decrypt_data]    if plain_data then      local data = ngx.decode_base64(plain_data)      -- 清除附加请求header      ngx.req.set_header(process_java_plugin_decrypt_data, nil)      -- 重写body数据      ngx.req.set_body_data(data)      -- 这里如果计算不准,最好不传      ngx.req.set_header('Content-Length', nil)    end  endendfunction _M.body_filter(conf, ctx)endreturn _M ;

接下来就是将该项目打包成jar。dOb28资讯网——每日最新资讯28at.com

以上就完成插件的开发,接下来就是配置dOb28资讯网——每日最新资讯28at.com

5.5 插件配置

Java插件配置

将上一步打包后的jar长传到服务器,在config.yaml中配置插件dOb28资讯网——每日最新资讯28at.com

ext-plugin:  cmd: ['java', '-Dfile.encoding=UTF-8', '-jar', '/app/plugins/crypto-apisix-plugin-runner-1.0.0.jar']

LUA插件配置dOb28资讯网——每日最新资讯28at.com

将lua脚本上传到docker容器dOb28资讯网——每日最新资讯28at.com

docker cp modify-body.lua apisix-java-apisix-1:/usr/local/apisix/apisix/plugins/modify-body.lua

配置该插件dOb28资讯网——每日最新资讯28at.com

plugins:  - ext-plugin-pre-req  - ext-plugin-post-req  - ext-plugin-post-resp  - modify-body

要想在apisix-dashboard中能够使用,需要导出schema.json文件dOb28资讯网——每日最新资讯28at.com

docker exec -it apisix-java-apisix-1 curl http://localhost:9092/v1/schema > schema.json

上传该schema.json到apisix-dashboard中dOb28资讯网——每日最新资讯28at.com

docker cp schema.json apisix-java-apisix-dashboard-1:/usr/local/apisix-dashboard/conf

重启相应服务dOb28资讯网——每日最新资讯28at.com

docker restart apisix-java-apisix-dashboard-1docker restart apisix-java-apisix-1

完成以上步骤后,接下来就可以通过dashboard进行路径配置了。dOb28资讯网——每日最新资讯28at.com

六. 路由配置

这里直接贴插件的配置dOb28资讯网——每日最新资讯28at.com

"plugins": {  "ext-plugin-pre-req": {    "allow_degradation": false,    "conf": [      {        "name": "Decrypt",        "value": "{/"enabled/": /"true/",/"apiKey/": /"kzV7HpPsZfTwJnZbyWbUJw==/", /"alg/": /"sm/", /"params/": [{/"pattern/": /"/api-1/**/", /"keys/": [/"idNo/"]}],/"body/": {/"exclude/": [/"/api-a/**/"],/"include/": [/"/api-1/**/"]}}"      }    ]  },  "modify-body": {},  "proxy-rewrite": {    "regex_uri": [      "^/api-1/(.*)$",      "/$1"    ]  }}

注意:modify-body插件一定要配置,这个是专门用来改写请求body内容的。dOb28资讯网——每日最新资讯28at.com

到此一个完整的插件就开发完成了,希望本篇文章能够帮到你。如有需要,可提供其它代码。dOb28资讯网——每日最新资讯28at.com

完毕!!!dOb28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-64508-0.html深入探讨API网关APISIX中自定义Java插件在真实项目中的运用

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 针对大型数据库,如何优化MySQL事务的性能?

下一篇: 使用 Spring Boot 创建自己的 ChatGPT 应用程序

标签:
  • 热门焦点
  • 一加首款折叠屏!一加Open渲染图出炉:罕见单手可握小尺寸

    一加首款折叠屏!一加Open渲染图出炉:罕见单手可握小尺寸

    8月5日消息,此前就有爆料称,一加首款折叠屏手机将会在第三季度上市,如今随着时间临近,新机的各种消息也开始浮出水面。据悉,这款新机将会被命名为&ldquo;On
  • 十个可以手动编写的 JavaScript 数组 API

    十个可以手动编写的 JavaScript 数组 API

    JavaScript 中有很多API,使用得当,会很方便,省力不少。 你知道它的原理吗? 今天这篇文章,我们将对它们进行一次小总结。现在开始吧。1.forEach()forEach()用于遍历数组接收一参
  • 2023 年的 Node.js 生态系统

    2023 年的 Node.js 生态系统

    随着技术的不断演进和创新,Node.js 在 2023 年达到了一个新的高度。Node.js 拥有一个庞大的生态系统,可以帮助开发人员更快地实现复杂的应用。本文就来看看 Node.js 最新的生
  • 量化指标是与非:挽救被量化指标扼杀的技术团队

    量化指标是与非:挽救被量化指标扼杀的技术团队

    作者 | 刘新翠整理 | 徐杰承本文整理自快狗打车技术总监刘新翠在WOT2023大会上的主题分享,更多精彩内容及现场PPT,请关注51CTO技术栈公众号,发消息【WOT2023PPT】即可直接领取
  • JVM优化:实战OutOfMemoryError异常

    JVM优化:实战OutOfMemoryError异常

    一、Java堆溢出堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证 GC Roots 到对象之间有可达路径来避免垃 圾收集回收机制清除这些对象,当这些对象所占空间超过
  • 得物宠物生意「狂飙」,发力“它经济”

    得物宠物生意「狂飙」,发力“它经济”

    作者|花花小萌主近日,得物宣布正式上线宠物鉴别,通过得物App内的&ldquo;在线鉴别&rdquo;,可找到鉴别宠物的选项。通过上传自家宠物的部位细节,就能收获拥有专业资质认证的得物鉴
  • 阿里大调整

    阿里大调整

    来源:产品刘有媒体报道称,近期淘宝天猫集团启动了近年来最大的人力制度改革,涉及员工绩效、层级体系等多个核心事项,目前已形成一个初步的&ldquo;征求意见版&rdquo;:1、取消P序列
  • 苹果公司要求三星和LG Display生产「无边框」OLED iPhone显示屏

    苹果公司要求三星和LG Display生产「无边框」OLED iPhone显示屏

    据 The Elec 报道,苹果已要求其供应商为未来的 iPhone 型号开发「无边框」OLED 显示面板。苹果显然已要求三星和 LG Display 开发新的 OLED 显示面
  • AI艺术欣赏体验会在上海梅赛德斯奔驰中心音乐俱乐部上演

    AI艺术欣赏体验会在上海梅赛德斯奔驰中心音乐俱乐部上演

    光影交错的镜像世界,虚实幻化的视觉奇观,虚拟偶像与真人共同主持,这些场景都出现在2019世界人工智能大会的舞台上。8月29日至31日,“AI艺术欣赏体验会”在上海
Top