Security Top

Finally announcing my next course. The intro price of the upcoming “Learn Spring” course will permanently increase by $50 at end-of-day today:

>> HAVE A LOOK

Jackson Top

Finally announcing my next course. The intro price of the upcoming “Learn Spring” course will permanently increase by $50 at end-of-day today:

>> HAVE A LOOK

1. Overview

In this quick tutorial, we’ll show how to filter JSON serialization output depending on a user role defined in Spring Security.

2. Why Do We Need To Filter?

Let’s consider a simple yet common use case where we have a web application that serves users with different roles. For example, let these roles be User and Admin.

To begin with, let’s define a requirement that Admins have full access to the internal state of objects exposed via a public REST API. On the contrary, Users should see only a predefined set of objects’ properties.

We’ll use the Spring Security framework to prevent unauthorized access to web application resources.

Let’s define an object that we’ll return as a REST response payload in our API:

class Item {
    private int id;
    private String name;
    private String ownerName;

    // getters
}

Of course, we could’ve defined a separate data transfer object class for each role present in our application. However, this approach will introduce useless duplications or sophisticated class hierarchies to our codebase.

On the other hand, we can employ the use of the Jackson library’s JSON views feature. As we’ll see in the next section, it makes customizing JSON representation as easy as adding an annotation on a field.

3. @JsonView Annotation

The Jackson library supports defining multiple serialization/deserialization contexts by marking fields we want to include in JSON representation with the @JsonView annotation. This annotation has a required parameter of a Class type that is used to tell contexts apart.

When marking fields in our class with @JsonView, we should keep in mind that, by default, the serialization context includes all the properties that are not explicitly marked as being part of a view. In order to override this behavior, we can disable the DEFAULT_VIEW_INCLUSION mapper feature.

Firstly, let’s define a View class with some inner classes that we’ll be using as an argument for the @JsonView annotation:

class View {
    public static class User {}
    public static class Admin extends User {}
}

Next, we add @JsonView annotations to our class, making ownerName accessible to the admin role only:

@JsonView(View.User.class)
private int id;
@JsonView(View.User.class)
private String name;
@JsonView(View.Admin.class)
private String ownerName;

4. How to Integrate @JsonView Annotation With Spring Security

Now, let’s add an enumeration containing all the roles and their names. After that, let’s introduce a mapping between JSON views and security roles:

enum Role {
    ROLE_USER,
    ROLE_ADMIN
}

class View {

    public static final Map<Role, Class> MAPPING = new HashMap<>();

    static {
        MAPPING.put(Role.ADMIN, Admin.class);
        MAPPING.put(Role.USER, User.class);
    }

    //...
}

Finally, we’ve come to the central point of our integration. In order to tie up JSON views and Spring Security roles, we need to define controller advice that applies to all the controller methods in our application.

And so far, the only thing we need to do is to override the beforeBodyWriteInternal method of the AbstractMappingJacksonResponseBodyAdvice class:

@RestControllerAdvice
class SecurityJsonViewControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice {

    @Override
    protected void beforeBodyWriteInternal(
      MappingJacksonValue bodyContainer,
      MediaType contentType,
      MethodParameter returnType,
      ServerHttpRequest request,
      ServerHttpResponse response) {
        if (SecurityContextHolder.getContext().getAuthentication() != null
          && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) {
            Collection<? extends GrantedAuthority> authorities
              = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
            List<Class> jsonViews = authorities.stream()
              .map(GrantedAuthority::getAuthority)
              .map(AppConfig.Role::valueOf)
              .map(View.MAPPING::get)
              .collect(Collectors.toList());
            if (jsonViews.size() == 1) {
                bodyContainer.setSerializationView(jsonViews.get(0));
                return;
            }
            throw new IllegalArgumentException("Ambiguous @JsonView declaration for roles "
              + authorities.stream()
              .map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
        }
    }
}

This way, every response from our application will go through this advice, and it’ll find the appropriate view representation according to the role mapping we have defined. Note that this approach requires us to be careful when dealing with users that have multiple roles.

5. Conclusion

In this brief tutorial, we’ve learned how to filter JSON output in a web application based on a Spring Security role.

All the related code can be found over on Github.

Security bottom

Finally announcing my next course. The intro price of the upcoming “Learn Spring” course will permanently increase by $50 at end-of-day today:

>> HAVE A LOOK

Jackson bottom

Finally announcing my next course. The intro price of the upcoming “Learn Spring” course will permanently increase by $50 at end-of-day today:

>> HAVE A LOOK

Leave a Reply

avatar
  Subscribe  
Notify of