環境:APISIX3.4.1 + JDK11 + SpringBoot2.7.12
APISIX 網關作為所有業務的流量入口,它提供了動態路由、動態上游、動態證書、A/B 測試、灰度發布(金絲雀發布)、藍綠部署、限速、防攻擊、收集指標、監控報警、可觀測、服務治理等功能。
為什么使用APISIX?
綜上所述,APISIX是一個優秀的開源API網關,具有高性能、可擴展性、社區活躍、易于使用等特點,并且能夠處理API和微服務流量,避免宕機、丟失數據等問題,實現實時配置更新。因此,許多企業和團隊選擇使用APISIX作為其API網關解決方案。
關于APISIX的安裝參考官方文檔即可
APISIX安裝指南
https://apisix.apache.org/zh/docs/apisix/installation-guide/
我使用的docker部署
由于安全需要,現有系統接口的請求數據需要加密(調用方必須加密傳輸)。
考慮到對現有系統的最小化影響,決定采用APISIX作為中間件,通過自定義Java插件來實現數據加密的功能。
數據加密:插件需要能夠接收并解析請求數據,然后對數據進行解密處理(解密后的數據再提交到上游服務)。
安全性:加密算法和密鑰管理應遵循業界最佳實踐,確保數據安全。
錯誤處理與日志記錄:插件應具備良好的錯誤處理機制,并能夠記錄詳細的日志,以便于問題排查。(這通過記錄日志即可)
非功能需求:
可維護性:插件代碼應清晰、模塊化,便于后續的維護和升級。
可擴展性:考慮到未來可能的加密需求變化,插件應具備良好的擴展性。
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); }}
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)) // ... } }); }}
<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>
這里的配置可有可無,都有默認值
cache.config: expired: ${APISIX_CONF_EXPIRE_TIME} capacity: 1000socket: file: ${APISIX_LISTEN_ADDRESS}
@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"包路徑。
總共2個插件:
定義一個抽象類,實現了通過的功能
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; }}
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。
以上就完成插件的開發,接下來就是配置
將上一步打包后的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-1docker 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內容的。
到此一個完整的插件就開發完成了,希望本篇文章能夠幫到你。如有需要,可提供其它代碼。
完畢!!!
本文鏈接:http://www.www897cc.com/showinfo-26-64508-0.html深入探討API網關APISIX中自定義Java插件在真實項目中的運用
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com