Cloud Native 16 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Build a Secure Java Encryption Plugin for Apache APISIX Gateway

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&lt;PluginFilter&gt; 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&lt;DomainSocketChannel&gt;() { @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&lt;BaseCryptoModel&gt; 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.

Cloud NativeAPI GatewaySpring BootEncryptionAPISIXJava plugin
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.