Modifying Zuul Request URI and Query Params
Over the last week, I found myself going down a rabbit hole. I ran into an issue where some of the requests to our Zuul-based BFF have an encrypted string included in the URI or query parameters that needs to be decrypted before passing through to the destination. I was unable to find much documentation on this subject and had to do a decent amount of digging in the Zuul source to figure out how to handle this situation.
This blog post is an explanation of how I went about handling this issue. We’ll be working in a test-driven manner. The test code will come first, then the code to make the test pass, and finally an explanation of what the code is doing/how it works. That pattern will be repeated until we’ve reached the end.
Who Is This Post For?
Anyone working with Zuul who needs to modify the URI or query parameters of an incoming request before passing it through to its destination.
- Basic knowledge of Kotlin. If you don’t yet know Kotlin (I highly recommend it) the official Kotlin Reference is a great place to get started.
- Basic knowledge of Zuul. This spring.io guide provides a decent overview.
Code In Full
You can find the code for this post in full on my Github: https://github.com/lionize/zuul_request_filters
Before we get to the solution, we need to review how a few things in Zuul function.
Zuul uses a RequestContext to track information related to the request and response. We can access the current context using RequestContext.getCurrentContext(). RequestContext is a map with a number of different keys, two of which are important to our issue:
request: The HttpServletRequest object which stores information about the current request. The requestURI field of this object contains the part of the request URL from the protocol name to the query string, e.g. “/foo/bar” is the uri for “http://foobar.com/foo/bar”.
requestQueryParams: A map whose keys are the names of the query params and values are a list of the query param values. The value baz of query param bar in “http://foobar.com/foo?bar=baz” can be accessed using requestQueryParams[“bar”]!!.
Implementing a Solution
As is usually the case with Zuul, the first step is to create filters. For this case we will create two filters–UriDecryptionFilter and QueryParamsDecryptionFilter. These will both be “pre” filters as we need them to run before we send the request to the destination.
We’ll start by building the UriDecryptionFilter and then move on to the QueryParamsDecryptionFilter.
For the purposes of this post, we will assume that the encrypted number is a string of at least twelve characters consisting of both uppercase letters and numbers, e.g. “A1B2C3D4E5F6”.
We’re also not actually going to decrypt anything since this post is about manipulating the request URI and query params and not encryption. So when you see “decrypted”, just know that it means the encrypted string in reverse order.
The first step is to get a basic working filter. We’ll started with the initial test setup:
We create a UriDecryptionFilter which will be the subject of our tests, and a MockHttpServletRequest which we can modify in order to provide fake data to the filter. We then create the init method which will run before test–its purpose is to reset the current context and then set the context’s request to the mock http request we created.
Now that we have basic setup out of the way, we can move on to implementing the basic filter:
The “basic properties” test asserts that the filter’s three basic properties (which every ZuulFilter has to implement) are as expected.
We specified filterOrder as 1 so that the filter runs before any other filters with a higher number, and we set filterType to “pre” so that the filter runs before the request is sent off to its destination.
Now we’ll modify shouldFilter so that it only filters if the encrypted value is found in the request URI:
shouldFilter now gets the current request’s URI and runs it by the encryptedStringRegex. If the result is not null, we should run the filter because the encrypted string exists in the URI.
The last step is to implement the run method so that this filter will actually do something:
There’s a lot going on here, so we’ll go through it piece by piece.
We started by getting the uri from the current context’s request object. We then use the encryptedStringRegex to find the encrypted value in the uri, “decrypt” that value, and then replace the encrypted value in the uri with the new decrypted value.
In order to use this new uri, we have to completely replace the current context’s request object with a new request. In order to do that, we create a new HttpServletRequestWrapper wrapped around the old request and then override the getRequestURI method to return the new uri. Basically what this does is makes the new request exactly the same as the old request, except for returning a different uri.
We then replace the context’s request with the new request.
This completes the manipulation of the request uri. Any requests with a uri that contains an encrypted value will now have that value replaced with its decrypted version. We’ll now move on to the query params filter.
The basic setup of this filter is a lot like the UriDecryptionFilter, so I’ll skip reviewing most of the basic implementation details and provide the code we’ll start with:
As with the last filter, we’ll start by implementing shouldFilter. In this case, we should only run the filter if we find a query param with the name of encryptedValue containing an encrypted string.
This test inserts an encrypted string into the request’s query params under the key encryptedValue.
We get the request’s query params from the current context. If the params are null, we return false. We then check to see if the params have the encryptedValue key and if not, we return false. Finally, we check that the value of encryptedValue is actually an encrypted string. If it is, return true; otherwise, return false.
Now all that’s left is to implement run for this filter:
We get the encryptedValue from the query params, “decrypt” it, and then insert it back into the query params.
In this post, we learned how to:
- Create filters and only have them run if certain conditions are met
- Access an incoming request’s URI and query parameters
- Modify and replace the current context’s request
- Modify the request’s query parameters