文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-11-30 02:55

关注

一. APISIX简介

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

为什么使用APISIX?

  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网关解决方案。

二. APISIX安装

关于APISIX的安装参考官方文档即可


APISIX安装指南


https://apisix.apache.org/zh/docs/apisix/installation-guide/

我使用的docker部署

三. 需求说明

  1. 目的与背景:

由于安全需要,现有系统接口的请求数据需要加密(调用方必须加密传输)。

考虑到对现有系统的最小化影响,决定采用APISIX作为中间件,通过自定义Java插件来实现数据加密的功能。

  1. 功能需求:
  1. 非功能需求:

四. 插件工作原理

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

图片

核心运行原理

官方的包是基于springboot,所以它自身提供了一个CommandLineRunner类,该类会在Springboot容器启动完成后运行,也就是下面的地方执行:

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

核心Runner类

public class ApplicationRunner implements CommandLineRunner {
  private ObjectProvider 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() {
      @Override
      protected void initChannel(DomainSocketChannel channel) {
        channel.pipeline().addFirst("logger", new LoggingHandler())
          //...
          // 核心Handler
          .addAfter("payloadDecoder", "prepareConfHandler", createConfigReqHandler(cache, filterProvider, watcherProvider))
          // ...
      }
    });
  }
}

五. 插件开发

5.1 依赖管理


  11
  2.7.12
  0.4.0
  1.1.4


  
    org.apache.apisix
    apisix-runner-starter
    ${apisix.version}
  
  
  
    com.pack.components
    pack-keys
    ${keys.version}
  
  
    com.fasterxml.jackson.core
    jackson-databind
  
  
    org.springframework
    spring-web
  

5.2 配置文件

这里的配置可有可无,都有默认值

cache.config:
  expired: ${APISIX_CONF_EXPIRE_TIME}
  capacity: 1000
socket:
  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"包路径。

5.4 Filter开发

总共2个插件:

定义一个抽象类,实现了通过的功能

public abstract class AbstractDecryptPreFilter implements PluginFilter {
  // 具体细节由子类实现
  protected abstract void doFilterInternal(HttpRequest request, HttpResponse response, PluginFilterChain chain,
     CryptModel cryptModel, CacheModel cache);
  // 工具类专门用来读取针对插件的配置信息
  @Resource
  protected ConfigProcessor 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 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)
end


function _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)
end


function _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
  end
end


function _M.body_filter(conf, ctx)
end




return _M ;

接下来就是将该项目打包成jar。

以上就完成插件的开发,接下来就是配置

5.5 插件配置

Java插件配置

将上一步打包后的jar长传到服务器,在config.yaml中配置插件

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

LUA插件配置

将lua脚本上传到docker容器

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

配置该插件

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

要想在apisix-dashboard中能够使用,需要导出schema.json文件

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

上传该schema.json到apisix-dashboard中

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

重启相应服务

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

完成以上步骤后,接下来就可以通过dashboard进行路径配置了。

六. 路由配置

这里直接贴插件的配置

"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内容的。

到此一个完整的插件就开发完成了,希望本篇文章能够帮到你。如有需要,可提供其它代码。

完毕!!!

来源:Spring全家桶实战案例源码内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯