11
votes

Is there any good way to filter JSON output based on Spring Security roles? I'm looking for something like @JsonIgnore, but for role, like @HasRole("ROLE_ADMIN"). How should I implement this?

3

3 Answers

19
votes

For those landing here from Google, here is a similar solution with Spring Boot 1.4.

Define interfaces for each of your roles, e.g.

public class View {
    public interface Anonymous {}

    public interface Guest extends Anonymous {}

    public interface Organizer extends Guest {}

    public interface BusinessAdmin extends Organizer {}

    public interface TechnicalAdmin extends BusinessAdmin {}
}

Declare @JsonView in your entities, e.g.

@Entity
public class SomeEntity {
    @JsonView(View.Anonymous.class)
    String anonymousField;

    @JsonView(View.BusinessAdmin.class)
    String adminField;
}

And define a @ControllerAdvice to pick up the right JsonView based on the roles:

@ControllerAdvice
public class JsonViewConfiguration extends AbstractMappingJacksonResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return super.supports(returnType, converterType);
    }

    @Override
    protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
                                           MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {

        Class<?> viewClass = View.Anonymous.class;

        if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) {
            Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();

            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.GUEST.getValue()))) {
                viewClass = View.Guest.class;
            }
            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.ORGANIZER.getValue()))) {
                viewClass = View.Organizer.class;
            }
            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.BUSINESS_ADMIN.getValue()))) {
                viewClass = View.BusinessAdmin.class;
            }
            if (authorities.stream().anyMatch(o -> o.getAuthority().equals(Role.TECHNICAL_ADMIN.getValue()))) {
                viewClass = View.TechnicalAdmin.class;
            }
        }
        bodyContainer.setSerializationView(viewClass);
    }
}
9
votes

Update: The new Answer

you should consider using rkonovalov/jfilter. specially @DynamicFilterComponent helps a lot. you can see a good guide in this DZone article.

@DynamicFilterComponent is explained here.

The old answer

I've just implemented the requirement you've mentioned above. My system uses Restful Jersey 1.17, Spring Security 3.0.7, Jackson 1.9.2. But the solution has nothing to do with Jersey Restful API and you can use it on any other kind of Servlet implementations.

This is the entire 5 steps of my solution:

  1. First you should create an Annotation class for your purpose, Like this:

    JsonSpringView.java

    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface JsonSpringView {
        String springRoles();
    }
    
  2. Then an Annotation Introspector, most of it's Methods should return null, Fill the Methods based on your need, for my requirments i had just used isIgnorableField. Feature is My Implementation For GrantedAuthority interface. Like this:

    JsonSpringViewAnnotationIntrospector.java

    @Component
    public class JsonSpringViewAnnotationIntrospector extends AnnotationIntrospector implements Versioned 
    {
        // SOME METHODS HERE
        @Override
        public boolean isIgnorableField(AnnotatedField)
        {
            if(annotatedField.hasAnnotation(JsonSpringView.class))
            {
                JsonSpringView jsv = annotatedField.getAnnotation(JsonSpringView.class);
                if(jsv.springRoles() != null)
                {
                    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                    if(principal != null && principal instanceof UserDetails)
                    {
                        UserDetails principalUserDetails = (UserDetails) principal;
                        Collection<? extends  GrantedAuthority> authorities = principalUserDetails.getAuthorities();
                        List<String> requiredRoles = Arrays.asList(jsv.springRoles().split(","));
    
                        for(String requiredRole : requiredRoles)
                        {
                            Feature f = new Feature();
                            f.setName(requiredRole);
                            if(authorities.contains(f))
                            // if The Method Have @JsonSpringView Behind it, and Current User has The Required Permission(Feature, Right, ... . Anything You may Name It).
                            return false;
                        }
                        // if The Method Have @JsonSpringView Behind it, but the Current User doesn't have The required Permission(Feature, Right, ... . Anything You may Name It).
                        return true;
                    }
                }
            }
            // if The Method Doesn't Have @JsonSpringView Behind it.
            return false;
        }
    }
    
  3. Jersey servers have a default ObjectMapper for their serialization/deserialization purposes. If you're using such system and you want to change it's default ObjectMapper, Steps 3, 4 and 5 is yours, else you can read this step and your job is done here.

    JsonSpringObjectMapperProvider.java

    @Provider
    public class JsonSpringObjectMapperProvider implements ContextResolver<ObjectMapper>
    {
        ObjectMapper mapper;
    
        public JsonSpringObjectMapperProvider()
        {
            mapper = new ObjectMapper();
            AnnotationIntrospector one = new JsonSpringViewAnnotationIntrospector();
            AnnotationIntrospector two = new JacksonAnnotationIntrospector();
            AnnotationIntrospector three = AnnotationIntrospector.pair(one, two);
    
            mapper.setAnnotationIntrospector(three);
        }
    
        @Override
        public ObjectMapper getContext(Class<?> arg0) {
            return this.mapper;
        }
    }
    
  4. You should extend javax.ws.rs.core.Application and mention Your class Name in Web.xml. Mine is RestApplication.Like this:

    RestApplication.java

    import java.util.HashSet;
    import java.util.Set;
    
    import javax.ws.rs.core.Application;
    
    public class RestApplication extends Application
    {
        public Set<Class<?>> getClasses() 
        {
            Set<Class<?>> classes = new HashSet<Class<?>>();
            classes.add(JsonSpringObjectMapperProvider.class);
            return classes ;
        }
    }
    
  5. and this is the Last Step. you should mention your Application class (from step 4) in your web.xml:

    A part of my web.xml

    <servlet>
        <servlet-name>RestService</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>com.sun.jersey.config.property.package</param-name>
            <param-value>your_restful_resources_package_here</param-value>
        </init-param>
        <init-param>
        <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
            <param-value>true</param-value>
        </init-param>
        <!-- THIS IS THE PART YOU SHOULD PPPAYYY ATTTTENTTTTION TO-->
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>your_package_name_here.RestApplication</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    

and from now on You only need to mention the @JsonSpringView annotation Behind Any Property you want. Like this:

PersonDataTransferObject.java

public class PersonDataTransferObject
{
    private String name;

    @JsonSpringView(springRoles="ADMIN, SUPERUSER")  // Only Admins And Super Users Will See the person National Code in the automatically produced Json.
    private String nationalCode;
}
3
votes

Althou it is possible to write custom JSON processing filter (e.g. based on JSON Pointers), it will be a little bit complex to do.

The simplest way is to create your own DTO and map only those properties, which the user is authorized to get.