成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深入探討API網關APISIX中自定義Java插件在真實項目中的運用

開發 前端
APISIX 網關作為所有業務的流量入口,它提供了動態路由、動態上游、動態證書、A/B 測試、灰度發布(金絲雀發布)、藍綠部署、限速、防攻擊、收集指標、監控報警、可觀測、服務治理等功能。

環境:APISIX3.4.1 + JDK11 + SpringBoot2.7.12

一. 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<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 配置文件

這里的配置可有可無,都有默認值

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個插件:

  • java插件
    該插件用來對數據解密。
  • LUA插件
    該插件用來改寫請求body,因為在java插件中無法改寫。

定義一個抽象類,實現了通過的功能

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)
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全家桶實戰案例源碼
相關推薦

2025-01-10 09:28:25

2023-03-31 07:17:16

2022-01-14 09:17:13

PythonAPISIX插件

2023-10-23 12:35:36

Golang追加操作

2024-12-26 01:07:13

2009-12-23 16:13:00

WPF Attache

2011-02-25 09:23:00

Java類加載器

2015-09-02 08:57:56

JavaHashMap工作原理

2017-05-10 21:28:00

Java異常與錯誤處理

2010-03-31 14:58:03

云計算

2024-01-04 07:42:44

JavaCGLIBJDK

2009-12-07 16:07:03

PHP類的繼承

2010-11-22 14:18:32

MySQL鎖機制

2010-07-21 09:38:15

PHP緩存技術

2009-11-20 17:17:08

Oracle函數索引

2021-05-17 05:36:02

CSS 文字動畫技巧

2009-06-16 10:51:14

Java源碼

2024-01-24 08:31:13

extends?接口規范

2009-12-01 16:34:21

PHP表單

2009-10-16 18:20:07

綜合布線系統
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲一区二区三区在线视频 | 日韩一区在线视频 | 欧美国产日韩在线观看成人 | 日韩av免费在线电影 | 欧美中文在线 | 国产激情视频网站 | 欧美一二三区 | 亚洲高清成人在线 | 国产亚洲一区二区在线观看 | 精品在线免费看 | 99国产欧美| 精品国产一区二区三区免费 | 日韩午夜电影在线观看 | 黄a在线观看 | 黄色免费三级 | 久久久久久国产精品免费免费男同 | 羞羞视频网 | 国产成人精品一区二区三区网站观看 | 国产精品久久久久久久久久 | 天天拍天天插 | 亚洲欧美一区二区三区在线 | 亚洲精品日韩一区二区电影 | 四虎成人免费电影 | 午夜播放器在线观看 | 亚洲日本欧美日韩高观看 | 欧美一区二区三区视频在线播放 | 国产美女一区 | 九色视频网站 | 啪啪免费网站 | 亚洲国产精品成人无久久精品 | 欧美日韩一区精品 | 国产成人免费视频 | 午夜免费视频 | 亚洲视频手机在线 | 成人免费网站视频 | 美女国产精品 | 欧美一级网站 | 色欧美片视频在线观看 | 日韩精品一区二区三区四区视频 | 国产成人91| 免费a在线|