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 Answers
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);
}
}
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:
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(); }
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 usedisIgnorableField
.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; } }
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; } }
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 ; } }
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;
}
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.