【问题标题】:Spring 3.2: Filtering Jackson JSON output based on Spring Security roleSpring 3.2:基于 Spring Security 角色过滤 Jackson JSON 输出
【发布时间】:2013-06-20 23:53:41
【问题描述】:

有什么好的方法可以根据 Spring Security 角色过滤 JSON 输出吗?我正在寻找类似@JsonIgnore 的东西,但对于角色,例如@HasRole("ROLE_ADMIN")。我应该如何实现这个?

【问题讨论】:

    标签: spring spring-security jackson


    【解决方案1】:

    对于那些从 Google 登陆的用户,这里有一个与 Spring Boot 1.4 类似的解决方案。

    为每个角色定义接口,例如

    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 {}
    }
    

    在您的实体中声明@JsonView,例如

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

    并定义一个@ControllerAdvice 以根据角色选择正确的JsonView

    @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);
        }
    }
    

    【讨论】:

    • 帮了我大忙,但请注意,您可能还需要设置 spring 属性 spring.jackson.mapper.default-view-inclusion=true,这样您就不必注释实体的每个属性
    • 这是一个有趣的方法。但请注意,如果您使用 Spring Data 并定义投影,它将优先于视图,可能让用户通过将投影查询参数传递给查询来排除限制。
    • 是否可以有一个从多个视图扩展的视图?如果我想拥有一个拥有View1和View2权限的视图怎么办?
    【解决方案2】:

    更新:新答案

    您应该考虑使用rkonovalov/jfilter。特别是@DynamicFilterComponent 有很大帮助。 您可以在这篇 DZone 文章中看到一个很好的指南。

    @DynamicFilterComponent 解释为here

    旧答案

    我刚刚实现了您上面提到的要求。我的系统使用 Restful Jersey 1.17Spring Security 3.0.7Jackson 1.9.2。但该解决方案与 Jersey Restful API 无关,您可以在任何其他类型的 Servlet 实现中使用它。

    这是我的解决方案的全部 5 个步骤:

    1. 首先你应该为你的目的创建一个 Annotation 类,像这样:

      JsonSpringView.java

      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      
      @Retention(RetentionPolicy.RUNTIME)
      public @interface JsonSpringView {
          String springRoles();
      }
      
    2. 然后是 Annotation Introspector,它的大部分方法应该返回 null,根据您的需要填写方法,因为我刚刚使用了 isIgnorableFieldFeature 是我对 GrantedAuthority 接口的实现。像这样:

      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 服务器有一个默认的 ObjectMapper 用于其序列化/反序列化目的。如果你正在使用这样的系统并且你想改变它的默认 ObjectMapper,步骤 3、4 和 5 是你的,否则你可以阅读这一步,你的工作就在这里完成。

      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. 您应该扩展 javax.ws.rs.core.Application 并在 Web.xml 中提及您的班级名称。我的是 RestApplication。像这样:

      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. 这是最后一步。您应该在 web.xml 中提及您的 Application 类(来自第 4 步):

      我的 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>
      

    从现在开始你只需要在任何你想要的属性后面提到@JsonSpringView注解。像这样:

    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;
    }
    

    【讨论】:

    • 这对我不起作用,过滤器只对第一个登录的用户应用一次。请你看看我的新问题:stackoverflow.com/questions/35558218/…。您有针对此问题的更新解决方案吗?
    • 如果有人能解决这个问题,请告诉我?我也面临同样的问题,即过滤器只对第一个登录的用户应用一次。
    • 这太棒了,有机会为当前的春季版本更新您的示例吗?
    • @BigDong 真的很抱歉,目前我自己的任务压力很大,所以不能真正更新答案:(
    • 那太糟糕了。这应该默认使用 spring-security 实现。
    【解决方案3】:

    虽然可以编写自定义 JSON 处理过滤器(例如,基于 JSON Pointers),但它会有点复杂。

    最简单的方法是创建您自己的 DTO 并仅映射用户有权获取的那些属性。

    【讨论】:

    • 如果您想将 10 个字段返回给具有 admin 角色的用户,而只返回 3 个字段给具有 user 角色的用户,则将无济于事。除非您有继承并检查端点内的角色。
    • @prettyvoid 我不明白评论。你能详细说明一下吗?我描述的解决方案由一个 DTO 类和一个使用少量 IF 语句的属性映射器组成。
    • 没关系对误导性评论感到抱歉,我认为您只是建议一个带有构造函数的 DTO,该构造函数采用具有所有字段的类。但是,当您说带有一些 if 语句的属性映射器时,您已经说得很清楚了。
    猜你喜欢
    • 1970-01-01
    • 2017-10-19
    • 2016-10-14
    • 2016-06-19
    • 1970-01-01
    • 2017-05-17
    • 2016-07-01
    • 2019-08-08
    • 2018-11-28
    相关资源
    最近更新 更多