【问题标题】:How to trim Swagger docs based on current User Role in Java Spring?如何根据 Java Spring 中的当前用户角色修剪 Swagger 文档?
【发布时间】:2020-09-03 16:09:53
【问题描述】:

我正在使用 Spring Boot 开发应用程序,我正在使用 Swagger 自动生成 API 文档,并且我还使用 swagger-ui.html 与这些 API 进行交互。

我也启用了 Spring Security,并且我有不同角色的用户。不同的 REST API 可用于不同的角色。

问题:如何配置 Swagger 以尊重 Spring 的 @Secured 注释和 swagger-ui.html 显示的修剪操作,以便只有当前用户可用的操作可用?

即想象下面的控制器

@RestController
@Secured(ROLE_USER)
public void SomeRestController {
  @GetMapping
  @Secured(ROLE_USER_TOP_MANAGER)
  public String getInfoForTopManager() { /*...*/ }

  @GetMapping
  @Secured(ROLE_USER_MIDDLE_MANAGER)
  public String getInfoForMiddleManager() { /*...*/ }

  @GetMapping
  public String getInfoForAnyUser() { /*...*/ }
}

无论当前用户角色如何,Swagger 都会显示 getInfoForTopManagergetInfoForMiddleManager 这两个操作。如果当前经过身份验证的用户角色是 ROLE_USER_MIDDLE_MANAGER,我希望 Swagger 中只提供 getInfoForMiddleManagergetInfoForAnyUser 操作。

【问题讨论】:

    标签: java spring spring-boot swagger


    【解决方案1】:

    好的,我认为找到了解决该问题的好方法。解决方案由两部分组成:

    1. 通过 OperationBuilderPlugin 扩展控制器扫描逻辑以保留 Swagger 供应商扩展中的角色
    2. 覆盖 ServiceModelToSwagger2MapperImpl bean 以根据当前安全上下文过滤掉操作

    在您的项目中,这可能看起来有点不同(即您很可能没有 securityContextResolver 之类的东西),但我相信您将从以下代码中了解此解决方案的要点:

    第 1 部分:扩展控制器扫描逻辑以保留 Swagger 供应商扩展中的角色

    @Component
    @Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER + 1000)
    public class OperationBuilderPluginSecuredAware implements OperationBuilderPlugin {
        @Override
        public void apply(OperationContext context) {
            Set<String> roles = new HashSet<>();
            Secured controllerAnnotation = context.findControllerAnnotation(Secured.class).orNull();
            if (controllerAnnotation != null) {
                roles.addAll(List.of(controllerAnnotation.value()));
            }
    
            Secured methodAnnotation = context.findAnnotation(Secured.class).orNull();
            if (methodAnnotation != null) {
                roles.addAll(List.of(methodAnnotation.value()));
            }
    
            if (!roles.isEmpty()) {
                context.operationBuilder().extensions(List.of(new TrimToRoles(roles.toArray(new String[0]))));
            }
        }
    
        @Override
        public boolean supports(DocumentationType delimiter) {
            return SwaggerPluginSupport.pluginDoesApply(delimiter);
        }
    }
    

    第 2 部分:根据当前安全上下文过滤掉操作

    @Primary
    @Component
    public class ServiceModelToSwagger2MapperImplEx extends ServiceModelToSwagger2MapperImpl {
        @Autowired
        private SecurityContextResolver<User> securityContextResolver;
    
        @Override
        protected io.swagger.models.Operation mapOperation(Operation from) {
            if (from == null) {
                return null;
            }
            if (!isPermittedForCurrentUser(findTrimToRolesExtension(from.getVendorExtensions()))) {
                return null;
            }
            return super.mapOperation(from);
        }
    
        private boolean isPermittedForCurrentUser(TrimToRoles trimToRoles) {
            if (trimToRoles == null) {
                return true;
            }
            if (securityContextResolver.hasAnyRole(trimToRoles.getValue())) {
                return true;
            }
            return false;
        }
    
        private TrimToRoles findTrimToRolesExtension(@SuppressWarnings("rawtypes") List<VendorExtension> list) {
            if (CollectionUtils.isEmpty(list)) {
                return null;
            }
            return list.stream().filter(x -> x instanceof TrimToRoles).map(TrimToRoles.class::cast).findFirst()
                    .orElse(null);
        }
    
        @Override
        protected Map<String, Path> mapApiListings(Multimap<String, ApiListing> apiListings) {
            Map<String, Path> paths = super.mapApiListings(apiListings);
            return paths.entrySet().stream().filter(x -> !x.getValue().isEmpty())
                    .collect(Collectors.toMap(x -> x.getKey(), v -> v.getValue()));
        }
    
        @Override
        public Swagger mapDocumentation(Documentation from) {
            Swagger ret = super.mapDocumentation(from);
            Predicate<? super Tag> hasAtLeastOneOperation = tag -> ret.getPaths().values().stream()
                    .anyMatch(x -> x.getOperations().stream().anyMatch(y -> y.getTags().contains(tag.getName())));
            ret.setTags(ret.getTags().stream().filter(hasAtLeastOneOperation).collect(Collectors.toList()));
            return ret;
        }
    }
    

    附言这些 impl 效率不高,但考虑到它们的使用场景,我更喜欢简单的 impl

    【讨论】:

      猜你喜欢
      • 2022-10-23
      • 1970-01-01
      • 2017-12-07
      • 1970-01-01
      • 2011-07-06
      • 1970-01-01
      • 1970-01-01
      • 2013-02-07
      • 2014-01-17
      相关资源
      最近更新 更多