Return Client Specific Response from the Same REST Endpoint in Spring 4

This article is about my research related to the question which I posted on Stackoverflow - https://stackoverflow.com/questions/44004602/return-client-specific-response-from-the-same-rest-endpoint-in-spring-4

I wanted a way to customize my response from a Spring Rest Controller based on the client which made the request. Client identification can be done via a lot of things, but I had to do it via the scopes.

Now, the way I will have to eventually proceed is by creating separate end-points for the clients for unmentionable reasons whatsoever, but I couldn't help myself writing down here so that somebody might just find useful and a better use-case for it.

So, here is what I wanted things to work. Let me draw a picture. That would really help to make you understand.


Here are the three challenges for me to achieve this:

  1. How to get the scope in my code somewhere so that I can do something about it?
  2. I needed a way to to intercept/filter the JSON response that was sent from the Rest Controller?
  3. I needed a way to define how the code information about the scopes in such a way that my backend doesn't change with new clients(or minimum code changes and literally no change to the way employee object was fetched or creating new end-points all over again)?
Let's go about solving this.

Problem 1

Sometimes, the answer doesn't come to you and you will have to dig it yourself. That is how I got the solution to the first problem. I pulled out the Authentication object out of SecurityContextHolder and did some digging on the object. I figured out that the Authentication object is actually a OAuth2Authentication object(obviously, that is what I was working with and this was no big surpise). I was earlier trying to get the scopes via OAuth2ClientContext.getAccessToken().getScope(), but it didn't work. So, I got hold of SecurityContextHolder to get me the Authentication object and it had what I needed. I just had to peep inside the object in Debug mode and I got it.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Set scopes = ((OAuth2Authentication) authentication).getOAuth2Request().getScope();

Problem 2

The solution to the second problem is pretty straight forward. You would need a filter or interceptor. You would find examples of HandlerInterceptorAdapter everywhere, but that is not what I wanted. The problem with this was that the response is already rendered or to be more precise serialized to HttpResponse and you really can't do much about the filtration of fields here, unless you do the whole reading the json and reparsing it and then re-writing it(This is all just a thought, don't really know for sure if that is possible at all or not). Never-mind, I wanted to intercept the response before getting serialized and that is when I got the thing I wanted - AbstractMappingJacksonResponseBodyAdvice. So, this class is the AOP advice which gets applied to all the Json conversions from object to json as Response from the Rest Controllers. Eurekaaaaa!!!!

Problem 3

Now, comes the tricky part and I am glad somebody pointed in the right direction of JsonViews from the Stackoverflow answer to my question. Here is more about it from the Spring blog themselves. What I could do it is create separate classes which represented the clients to which I need to serve this response and annotate my model class with these. So, whichever fields I wanted to show for client1, I added the corresponding class in the JsonView. Here is what I did from code perspective.

public final class Views {

    public static final class Client1 {
    }

    public static final class Client2 {
    }
}

public class Employee {

    @JsonView({Views.Client1.class, Views.Client2.class})
    private int id;

    @JsonView({Views.Client1.class})
    private String firstName;

    @JsonView({Views.Client1.class})
    private String lastName;

    @JsonView({Views.Client2.class})
    private String department;
}


After this I created my custom AbstractMappingJacksonResponseBodyAdvice as below and this where all the magic happens - setting the SerializationView on the MappingJacksonValue. See the code below.

@ControllerAdvice
public class CustomMappingJacksonResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue container, MediaType contentType,
            MethodParameter returnType, ServerHttpRequest req, ServerHttpResponse res) {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Set scopes = ((OAuth2Authentication) authentication).getOAuth2Request().getScope();

        if (scopes.contains("client1")) {
            container.setSerializationView(Views.Client1.class);
        } else {
            container.setSerializationView(Views.Client2.class);
        }
    }
}

And that's it. I was happy as I could be. :-)

Comments

Popular posts from this blog

Personal Contact Manager

An apt quote

Alone