How to Build a Secure Java Encryption Plugin for Apache APISIX Gateway
This guide walks through installing APISIX, defining security and functional requirements, developing a Java decryption plugin and a Lua body‑rewrite plugin, configuring them with Docker and APISIX, and setting up route rules to enable encrypted API traffic in a cloud‑native environment.
Environment: APISIX 3.4.1, JDK 11, Spring Boot 2.7.12.
1. APISIX Overview
APISIX is an open‑source API gateway that serves as the traffic entry for all services, offering dynamic routing, upstream, certificates, A/B testing, canary releases, blue‑green deployment, rate limiting, attack protection, metrics, monitoring, observability, and service governance.
Why use APISIX?
High performance and scalability built on Nginx and OpenResty, supporting dynamic routing, rate limiting, caching, authentication, and extensible plugins.
Active community and comprehensive documentation, with easy Kubernetes‑style automated deployment.
Powerful tool for handling API and micro‑service traffic; used by hundreds of enterprises across finance, internet, manufacturing, retail, and telecom.
Unified cloud‑native stack: configuration stored in etcd, aligning with cloud‑native high‑availability principles.
Real‑time configuration updates via etcd with millisecond latency.
2. APISIX Installation
Refer to the official installation guide; the author uses Docker deployment.
APISIX installation guide: https://apisix.apache.org/zh/docs/apisix/installation-guide/
3. Requirements
Purpose & Background : Encrypt request data for security while minimizing impact on existing systems by inserting APISIX as middleware with a custom Java plugin.
Functional Requirements : Data encryption, secure algorithm and key management, error handling and detailed logging.
Non‑functional Requirements : Maintainable, modular plugin code and extensibility for future encryption needs.
4. Plugin Working Principle
The apisix-java-plugin-runner is a Netty‑based TCP server that provides a PluginFilter interface for users. Communication between the runner and APISIX is illustrated below.
APISIX stores configuration in etcd; the runner reads updates in milliseconds, enabling real‑time configuration changes.
SpringBoot CommandLineRunner
<code>public class SpringApplication { public static void main(String[] args) { /* ... */ } }</code>ApplicationRunner
<code>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); start(socketPath.toString()); } public void start(String path) throws Exception { EventLoopGroup group; ServerBootstrap bootstrap = new ServerBootstrap(); // ... initialize Netty server bootstrap.group(group).channel(...); try { bootstrap.childHandler(new ChannelInitializer<DomainSocketChannel>() { @Override protected void initChannel(DomainSocketChannel channel) { channel.pipeline().addFirst("logger", new LoggingHandler()) .addAfter("payloadDecoder", "prepareConfHandler", createConfigReqHandler(cache, filterProvider, watcherProvider)); } }); ChannelFuture future = bootstrap.bind(new DomainSocketAddress(path)).sync(); Runtime.getRuntime().exec("chmod 777 " + socketFile); future.channel().closeFuture().sync(); } finally { group.shutdownGracefully().sync(); } } }</code>5. Plugin Development
5.1 Dependency Management
<code><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><!-- encryption utilities --><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></code>5.2 Configuration File
<code>cache.config:<br/> expired: ${APISIX_CONF_EXPIRE_TIME}<br/> capacity: 1000<br/>socket:<br/> file: ${APISIX_LISTEN_ADDRESS}</code>5.3 Startup Class
<code>@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);
}
}</code>5.4 Filter Development
Two plugins are needed: a Java plugin for decryption and a Lua plugin to rewrite the request body.
Abstract Decrypt Filter (Java)
<code>public abstract class AbstractDecryptPreFilter implements PluginFilter {
// Subclass implements doFilterInternal
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(); }
protected boolean checkRequest(HttpRequest request, CryptModel cryptModel, CacheModel cache) { /* ... */ }
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)); }
@Override public final void filter(HttpRequest request, HttpResponse response, PluginFilterChain chain) { BaseCryptoModel cryptoModel = configCryptoProcessor.processor(request, this); CryptModel model = (cryptoModel instanceof CryptModel) ? (CryptModel) cryptoModel : null; CacheModel cache = new CacheModel(); if (isEnabled(request, cryptoModel) && checkRequest(request, model, cache)) { doFilterInternal(request, response, chain, model, cache); } chain.filter(request, response); }
@Override public Boolean requiredBody() { return Boolean.TRUE; }
}</code>DecryptFilter Implementation
<code>@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());
String plainText = sf.decrypt(body);
request.setBody(plainText);
request.setHeader(PluginConfigConstants.DECRYPT_DATA_PREFIX, Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8)));
request.setHeader(PluginConfigConstants.X_O_E, "1");
request.setHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE);
}
}
@Override public Boolean requiredBody() { return Boolean.TRUE; }
}</code>Lua Plugin (modify-body.lua)
<code>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) end
function _M.rewrite(conf, ctx)
local params, err = ngx.req.get_headers()
local flag = params[x_o_e_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)
ngx.req.set_header(process_java_plugin_decrypt_data, nil)
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</code>5.5 Plugin Configuration
Package the JAR and reference it in config.yaml :
<code>ext-plugin:
cmd: ['java','-Dfile.encoding=UTF-8','-jar','/app/plugins/crypto-apisix-plugin-runner-1.0.0.jar']</code>Upload the Lua script into the Docker container:
<code>docker cp modify-body.lua apisix-java-apisix-1:/usr/local/apisix/apisix/plugins/modify-body.lua</code>Add the plugins to the plugins list:
<code>plugins:
- ext-plugin-pre-req
- ext-plugin-post-req
- ext-plugin-post-resp
- modify-body</code>Export schema.json from the running APISIX instance and copy it to the dashboard configuration:
<code>docker exec -it apisix-java-apisix-1 curl http://localhost:9092/v1/schema > schema.json
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</code>6. Route Configuration
Example JSON to enable the Decrypt plugin and the modify‑body plugin for specific paths:
<code>{
"plugins": {
"ext-plugin-pre-req": {
"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"]
}
}
}</code>After restarting the services, the plugins can be managed through the APISIX dashboard.
End of tutorial.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.