How can I implement one "PostFilter" in SpringCloudGateway? - filter

Forgive my poor English, please.
When using Spring Cloud Gateway, you can usually define global filters by implementing GlobalFilter. After doing some verification work in it, execute chain.filter(exchange). But if I want to do something with the response returned by the microservice in the filter, such as viewing the returned cookie, what should I do.
#Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// do something about the request
chain.filter(exchange);
// do something about the response
// does it works like above? I tried but failed, please help me .
}

Related

How to forcefully end filter-chain in an early servlet/spring boot filter

Having a multi-tenancy application, I want to end any request without a tenant very early.
Setup / Scenario
I run spring boot (web) with spring session and spring security.
In my case, I have several strategies to determine the tenant, TenantFromX, TenantFromY .. all running on order Ordered.HIGHEST_PRECEDENCE - so as early as possible.
On order Ordered.HIGHEST_PRECEDENCE+1 I run the TenantMustExistFilter filter to validate, that any strategy actually did match / find a tenant definition
#Log4j2
#Component
#RequiredArgsConstructor
// After TenantFromX(min) but before SessionRepositoryFilter(Session) which is min+50
#Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class TenantMandatoryFilter extends GenericFilterBean
{
#Override
public void doFilter(
ServletRequest request, ServletResponse response,
FilterChain filterChain
) throws ServletException, IOException
{
if (TenantStore.getInitMode().isEmpty()) {
throw new NoTenantException("No tenant identified for request - request blocked");
}
try {
filterChain.doFilter(request, response);
} finally {
TenantStore.clear();
}
}
}
Question / Problem
Eventhough I throw an Runtime exception NoTenantException all other filters after that filter are called anyway.
For example SessionRepositoryFilter is called on Ordered.HIGHEST_PRECEDENCE+50 and then the spring security FilterProxyChain is triggered.
How do I effectively stop the chain after TenantMandatoryFilter has failed to validate?
I expected either a return; or and Exception or just not calling filterChain.doFilter(request, response); to be just enough to end the chain execution.
Do I have a misunderstanding of the process or the idea here?
Attempts
I can successfully end the chain by replacing
if (TenantStore.getInitMode().isEmpty()) {
throw new NoTenantException("No tenant identified for request - request blocked");
}
with
if (TenantStore.getInitMode().isEmpty()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
Interestingly the below will not work, even though it seems semantically more correct. It does not work means, it will call the SessionRepositoryFilter and the FilterChainProxy after that nevertheless.
if (TenantStore.getInitMode().isEmpty()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid tenant");
return;
}
Which I assume is since sendError sets the response to suspended to complete any other chain (which is what I do not want).
It still does not seem right, so how to do this the right way? Or is this a conceptual flaw altogether?
Sending an error, either via an explicit call or by throwing an exception, will cause the request to be dispatched to the application’s error page handling which will invoke the filters again for that dispatch. Setting the status is the right thing to do.

Share data through a request lifecycle in Spring Webflux

In Spring MVC I can use ThreadLocal to share data between different components through a request, and the data will be automatically cleared when the request is fulfilled. With WebFlux, since a request can be handled by multiple threads, this solution will not work. How to implement a similar solution so that initially a WebFilter can set some data in the request context, then the data can be accessed and modified in the controllers, and any event handlers that the request has gone through?
I tried subscriberContext but it did not work. Here is my code. Why this does not work? Is there another way to share the data?
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getResponse().beforeCommit(() -> {
return Mono.subscriberContext()
.map(context -> {
context.hasKey("test"); // this returns false
})
}
return Mono.subscriberContext()
.map(context -> {context.put("test", "test");})
.flatMap(chain.filter(exchange))
}
If I am not mistaken, it is because of .beforeCommit(). This Mono and the filter Mono do not share the same Subscriber.
To add the value, to the subscriberContext of the request. Try this:
chain.filter(exchange)
.subscriberContext(context -> context.put("KEY", "VALUE"));
You can also share values in ServerWebExchange attributes. This is Spring Web Flux not Reactor though. For example:
exchange.getAttributes().put("KEY", "VALUE");
Now, anywhere in your application where you have access to ServerWebExchange you can access the attribute using
exchange.getAttributes().get("KEY");

Change body of ServerWebExchange response in WebFilter

I am trying to make an application in spring webflux. But there is a lot of stuff that I can't find in the documentation.
I have a webfilter where I want to add a wrapper around the response. Example:
Reponse before:
{
"id": 1,
"name": "asdf"
}
Response after:
{
"status": "success",
"data": {
"id": 1,
"name": "asdf"
}
}
This is the WebFilter at the moment:
#Component
public class ResponseWrapperFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
// Add wrapper to the response content...
return webFilterChain.filter(serverWebExchange);
}
}
But I can't find a way to get or edit the body of the response.
How can I change the response output in a WebFilter?
As stated in the javadoc, WebFilter is made for application-agnostic, cross-cutting concerns. WebFilters usually mutate the request headers, add attributes to the exchange or handle the response altogether without delegating to the rest of the chain.
I don't think that what you're trying to achieve is easily doable or even something that should be achieved this way. At the raw exchange level, you're dealing with a Flux<DataBuffer>, which is the response body split randomly in groups of bytes.
You could somehow wrap the response in a filter and use Flux.concat to prepend and append data to the actual response written by the handler. But there are a lot of questions regarding encoding, infinite streams, etc.
Maybe this is a concern that should be achieved at the Encoder level, since there you could restrict that behavior to a particular media type.

Not able to access request payload in Spring filter when request type is www-form-urlencoded

A pretty simple use case:
class MyFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
String body = IOUtils.toString(req.getInputStream());
// body is a always empty
}
}
When I send a request with the content-type application/json, I get the actual payload. But when I send it as x-www-form-urlencoded, I don't happen to get any data. I am assuming one of the spring filters is already consuming the data and leaving my filter down the line with nothing.
Do note that this issue is NOT related to the fact that we can read a stream only once. That part I have already figured out and I am creating another HttpServletRequest with a buffered input stream for other filters. The problem is that something else in Spring Boot is reading all of my data leaving me with nothing.
I just want a way to get my hands on the payload. It doesn't have to come from the InputStream, if spring could just give me the payload as a byte[], it would serve my purpose.
I am using Spring Boot 1.3.5.
you can disable this via application.properties
spring.mvc.hiddenmethod.filter.enabled=false
I guess the problem here is HiddenHttpMethodFilter which consumes the stream.
Please try to disable the filter by adding below code and then check if it works
#Bean
public FilterRegistrationBean registration(HiddenHttpMethodFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}

Spring reading request body twice

In spring I have a controller with an endpoint like so:
#RequestMapping(method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
#ResponseBody
public OutputStuff createStuff(#RequestBody Stuff stuff) {
//my logic here
}
This way if doing a POST on this endpoint, the JSON in request body will be automatically deserialized to my model (Stuff). The problem is, I just got a requirement to log the raw JSON as it is coming in! I tried different approaches.
Inject HttpServletRequest into createStuff, read the body there and log:
Code:
#RequestMapping(method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
#ResponseBody
public OutputStuff createStuff(#RequestBody Stuff stuff, HttpServletRequest req) {
StringBuilder sb = new StringBuilder();
req.getReader().getLines().forEach(line -> {
sb.append(line);
});
//log sb.toString();
//my logic here
}
The problem with this is that by the time I execute this, the reader's InputStream would have already been executed to deserialize JSON into Stuff. So I will get an error because I can't read the same input stream twice.
Use custom HandlerInterceptorAdapter that would log raw JSON before the actual handler is called.
Code (part of it):
public class RawRequestLoggerInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
StringBuilder sb = new StringBuilder();
req.getReader().getLines().forEach(line -> {
sb.append(line);
});
//log sb.toString();
return true;
}
}
The problem with this tho is, that by the time the deserialization to stuff happens, the InputStream from the request would have been read already! So I would get an exception again.
Another option I considered, but not implemented yet, would be somehow forcing Spring to use my custom implementation of HttpServletRequest that would cache the input stream and allow multiple read of it. I have no idea if this is doable tho and I can't find any documentation or examples of that!
Yet another option would be not to read Stuff on my endpoint, but rather read the request body as String, log it and then deserialize it to Stuff using ObjectMapper or something like that. I do not like this idea either tho.
Are there better solutions, that I did not mention and/or am not aware of? I would appreciate help. I am using the latest release of SpringBoot.
To read the request body multiple times, we must cache the initial payload. Because once the original InputStream is consumed we can't read it again.
Firstly, Spring MVC provides the ContentCachingRequestWrapper class which stores the original content. So we can retrieve the body multiple times calling the getContentAsByteArray() method.
So in your case, you can make use of this class in a Filter:
#Component
public class CachingRequestBodyFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest);
// Other details
chain.doFilter(wrappedRequest, servletResponse);
}
}
Alternatively, you can register CommonsRequestLoggingFilter in your application. This filter uses ContentCachingRequestWrapper behind the scenes and is designed for logging the requests.
As referenced in this post: How to Log HttpRequest and HttpResponse in a file?, spring provides the AbstractRequestLoggingFilter you can use to log the request.
AbstractRequestLoggingFilter API Docs, found here
I also tried to do that in Spring but i could not find way to pass my custom http request to chain so what did was,i have written traditional j2ee filter in that i have passed my custom http request to chain that is it then onward i can read http request more than once
Check this example http://www.myjavarecipes.com/how-to-read-post-request-data-twice-in-spring/

Resources