零信任架构下的Pig系统接口文档安全控制:Swagger3权限配置实战指南

前言:接口文档暴露的安全隐患

在微服务架构盛行的今天,接口文档(API Documentation)作为前后端协作的核心枢纽,其安全性往往被开发团队忽视。某金融科技公司曾因Swagger界面未做权限控制,导致黑客直接通过公开的接口文档调用核心交易接口,造成3700万资金损失。Pig系统作为基于Spring Cloud 2022、Spring Boot 3.1、OAuth2构建的企业级权限管理平台,在接口文档安全领域提供了多层次防护体系。本文将从实际代码出发,详解如何通过Swagger3配置与Spring Security权限注解,构建"文档可见范围=用户权限范围"的零信任安全模型。

一、Pig系统接口文档架构解析

1.1 微服务聚合文档实现原理

Pig系统采用网关层聚合各微服务API文档的架构,通过SpringDoc + Nacos服务发现机制动态生成统一接口文档门户。核心实现位于SpringDocConfiguration配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "springdoc.api-docs.enabled", matchIfMissing = true)
public class SpringDocConfiguration implements InitializingBean {

    private final SwaggerUiConfigProperties swaggerUiConfigProperties;
    private final DiscoveryClient discoveryClient;

    @Override
    public void afterPropertiesSet() {
        // 注册Nacos服务实例变更监听器
        NotifyCenter.registerSubscriber(new SwaggerDocRegister(swaggerUiConfigProperties, discoveryClient));
    }

    // 内部类:处理服务实例变更事件
    @RequiredArgsConstructor
    class SwaggerDocRegister extends Subscriber<InstancesChangeEvent> {
        @Override
        public void onEvent(InstancesChangeEvent event) {
            Set<SwaggerUrl> swaggerUrls = discoveryClient.getServices().stream()
                .flatMap(serviceId -> discoveryClient.getInstances(serviceId).stream())
                .filter(instance -> StringUtils.isNotBlank(instance.getMetadata().get("spring-doc")))
                .map(instance -> {
                    SwaggerUrl url = new SwaggerUrl();
                    url.setName(instance.getServiceId());
                    url.setUrl(String.format("/%s/v3/api-docs", instance.getMetadata().get("spring-doc")));
                    return url;
                })
                .collect(Collectors.toSet());
            
            swaggerUiConfigProperties.setUrls(swaggerUrls);
        }
    }
}

工作流程时序图

零信任架构下的Pig系统接口文档安全控制:Swagger3权限配置实战指南_Pig

1.2 文档访问路径控制

默认情况下,Pig系统将Swagger UI路径/swagger-ui.html和OpenAPI规范路径/v3/api-docs加入匿名访问白名单,定义在PermitAllUrlProperties类:

@ConfigurationProperties(prefix = "security.oauth2.ignore")
public class PermitAllUrlProperties implements InitializingBean {
    private static final String[] DEFAULT_IGNORE_URLS = new String[] {
        "/actuator/**", "/error", "/v3/api-docs", 
        "/swagger-ui.html", "/swagger-ui/**", "/webjars/**"
    };
    
    @Override
    public void afterPropertiesSet() {
        urls.addAll(Arrays.asList(DEFAULT_IGNORE_URLS));
        // 动态添加@Inner注解标记的接口路径
    }
}

二、接口文档安全控制的三层防护体系

2.1 第一层:基础认证拦截

Pig系统作为OAuth2资源服务器,通过PigResourceServerConfiguration配置类实现对所有接口的认证拦截:

@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class PigResourceServerConfiguration {
    
    @Bean
    SecurityFilterChain resourceServer(HttpSecurity http) throws Exception {
        PathPatternRequestMatcher[] permitMatchers = permitAllUrl.getUrls()
            .stream()
            .map(url -> PathPatternRequestMatcher.withDefaults().matcher(url))
            .toArray(PathPatternRequestMatcher[]::new);

        http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers(permitMatchers).permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(token -> token.introspector(customOpaqueTokenIntrospector))
                .authenticationEntryPoint(resourceAuthExceptionEntryPoint)
            )
            .csrf(csrf -> csrf.disable());
            
        return http.build();
    }
}

关键配置说明

  • 通过permitAllUrl.getUrls()获取白名单路径,默认包含文档相关路径
  • 所有非白名单请求必须通过OAuth2认证(Bearer Token)
  • 使用自定义customOpaqueTokenIntrospector验证令牌有效性

2.2 第二层:Swagger文档权限过滤

为实现不同角色查看不同接口文档的需求,需自定义OpenAPICustomizer对API文档进行权限过滤:

@Component
@RequiredArgsConstructor
public class SecurityOpenAPICustomizer implements OpenAPICustomizer {

    private final SecurityContextHolder securityContextHolder;
    private final PermissionService permissionService;

    @Override
    public void customise(OpenAPI openApi) {
        // 获取当前登录用户角色
        Authentication authentication = securityContextHolder.getContext().getAuthentication();
        if (authentication == null || !(authentication instanceof OAuth2AuthenticationToken)) {
            // 未认证用户只保留公开接口
            filterPublicApis(openApi);
            return;
        }
        
        // 根据用户权限过滤接口文档
        Set<String> userPermissions = permissionService.getUserPermissions(authentication.getName());
        filterApisByPermissions(openApi, userPermissions);
    }
    
    private void filterApisByPermissions(OpenAPI openApi, Set<String> permissions) {
        openApi.getPaths().values().removeIf(pathItem -> {
            // 检查PathItem是否有权限注解
            Optional<Operation> operation = getFirstOperation(pathItem);
            return operation.flatMap(op -> extractRequiredPermission(op))
                .map(perm -> !permissions.contains(perm))
                .orElse(false); // 无权限注解的接口默认保留
        });
    }
}

权限过滤流程图

2.3 第三层:方法级权限注解

Pig系统提供细粒度的方法级权限控制,通过@PreAuthorize注解和自定义权限评估器实现:

  1. 权限注解定义
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@pms.hasPermission('{value}'.split(','))")
public @interface HasPermission {
    String[] value();
}
  1. 控制器方法应用示例
@RestController
@RequestMapping("/sys/dict")
@Tag(name = "字典管理", description = "系统字典维护接口")
public class SysDictController {

    @PostMapping
    @Operation(summary = "新增字典", description = "创建新的系统字典")
    @PreAuthorize("@pms.hasPermission('sys_dict_add')")
    public R save(@Valid @RequestBody SysDict sysDict) {
        return R.ok(sysDictService.save(sysDict));
    }
    
    @DeleteMapping("/{id}")
    @Operation(summary = "删除字典", description = "根据ID删除系统字典")
    @PreAuthorize("@pms.hasPermission('sys_dict_del')")
    public R removeById(@PathVariable Long id) {
        return R.ok(sysDictService.removeById(id));
    }
}
  1. 权限评估器实现
@Component("pms")
public class PermissionService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 检查用户是否拥有指定权限
     */
    public boolean hasPermission(String... permissions) {
        if (ArrayUtils.isEmpty(permissions)) {
            return false;
        }
        
        // 获取当前用户权限集合
        String username = SecurityUtils.getUsername();
        Set<String> userPermissions = redisTemplate.opsForSet()
            .members(CacheConstants.USER_PERMISSIONS_KEY + username);
            
        if (CollectionUtils.isEmpty(userPermissions)) {
            return false;
        }
        
        // 检查是否拥有任一所需权限
        return Arrays.stream(permissions).anyMatch(userPermissions::contains);
    }
}

三、Swagger权限控制实战配置

3.1 环境准备与依赖配置

Pig系统接口文档安全控制依赖以下核心组件,确保pom.xml中已包含:

<!-- SpringDoc OpenAPI -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.1.0</version>
</dependency>

<!-- Spring Security OAuth2 Resource Server -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- Pig Common Security -->
<dependency>
    <groupId>com.pig4cloud.pig</groupId>
    <artifactId>pig-common-security</artifactId>
    <version>4.0.0</version>
</dependency>

3.2 安全配置最佳实践

步骤1:配置Swagger文档认证参数

application.yml中添加Swagger安全配置:

swagger:
  enabled: true
  title: Pig权限管理系统API文档
  description: 基于Spring Cloud Alibaba的企业级权限管理系统接口文档
  version: 4.0.0
  contact:
    name: pig4cloud
    url: https://pig4cloud.com
    email: wangiegie@gmail.com
  # 配置OAuth2认证参数
  security:
    enabled: true
    client-id: pig
    client-secret: secret
    auth-url: /oauth2/authorize
    token-url: /oauth2/token
    scope: server
步骤2:自定义Swagger安全过滤器

创建SwaggerSecurityConfiguration配置类,添加OAuth2认证支持:

@Configuration
public class SwaggerSecurityConfiguration {

    @Bean
    public OpenAPI customOpenAPI(SwaggerProperties swaggerProperties) {
        // 配置文档基本信息
        OpenAPI openAPI = new OpenAPI()
            .info(new Info()
                .title(swaggerProperties.getTitle())
                .description(swaggerProperties.getDescription())
                .version(swaggerProperties.getVersion())
                .contact(new Contact()
                    .name(swaggerProperties.getContact().getName())
                    .url(swaggerProperties.getContact().getUrl())
                    .email(swaggerProperties.getContact().getEmail())));
        
        // 添加OAuth2认证支持
        if (swaggerProperties.getSecurity().isEnabled()) {
            String clientId = swaggerProperties.getSecurity().getClientId();
            String authUrl = swaggerProperties.getSecurity().getAuthUrl();
            String tokenUrl = swaggerProperties.getSecurity().getTokenUrl();
            
            SecurityScheme securityScheme = new SecurityScheme()
                .type(SecurityScheme.Type.OAUTH2)
                .flows(new OAuthFlows()
                    .password(new OAuthFlow()
                        .tokenUrl(tokenUrl)
                        .authorizationUrl(authUrl)
                        .scopes(new Scopes().addString(swaggerProperties.getSecurity().getScope(), "full access"))));
            
            openAPI.components(new Components().addSecuritySchemes("bearerAuth", securityScheme))
                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
        }
        
        return openAPI;
    }
}
步骤3:实现文档权限动态过滤

创建PermissionBasedOpenAPICustomizer,根据用户权限动态过滤接口文档:

@Component
@RequiredArgsConstructor
public class PermissionBasedOpenAPICustomizer implements OpenAPICustomizer {

    private final PermissionService permissionService;

    @Override
    public void customise(OpenAPI openApi) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return; // 未认证用户不做处理,由资源服务器拦截
        }

        // 超级管理员不过滤文档
        if (hasAdminRole(authentication)) {
            return;
        }

        // 获取当前用户权限集合
        Set<String> userPermissions = permissionService.getUserPermissions(authentication.getName());
        
        // 过滤Paths
        Map<String, PathItem> filteredPaths = openApi.getPaths().entrySet().stream()
            .filter(entry -> isPathAccessible(entry.getValue(), userPermissions))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
            
        openApi.setPaths(new Paths().addAll(filteredPaths));
    }
    
    private boolean isPathAccessible(PathItem pathItem, Set<String> userPermissions) {
        // 检查PathItem下所有Operation是否有权限访问
        return Stream.of(
            pathItem.getGet(), pathItem.getPost(), pathItem.getPut(), 
            pathItem.getDelete(), pathItem.getPatch(), pathItem.getHead(),
            pathItem.getOptions(), pathItem.getTrace()
        ).filter(Objects::nonNull)
        .allMatch(operation -> isOperationAccessible(operation, userPermissions));
    }
    
    private boolean isOperationAccessible(Operation operation, Set<String> userPermissions) {
        // 从Operation的extensions中获取权限信息
        Object permission = operation.getExtensions().get("x-permission");
        if (permission == null) {
            return true; // 无权限注解的接口默认可见
        }
        
        if (permission instanceof String) {
            return userPermissions.contains(permission);
        }
        
        if (permission instanceof List<?>) {
            List<?> permissionList = (List<?>) permission;
            return permissionList.stream()
                .filter(String.class::isInstance)
                .map(String.class::cast)
                .anyMatch(userPermissions::contains);
        }
        
        return false;
    }
    
    private boolean hasAdminRole(Authentication authentication) {
        return authentication.getAuthorities().stream()
            .anyMatch(a -> "ROLE_ADMIN".equals(a.getAuthority()));
    }
}

3.3 接口权限注解使用示例

在控制器方法上添加权限注解,控制接口文档可见性:

@RestController
@RequestMapping("/sys/user")
@Tag(name = "用户管理", description = "系统用户CRUD接口")
public class SysUserController {

    @GetMapping("/page")
    @Operation(summary = "用户分页查询", description = "获取系统用户列表,支持分页和条件查询")
    @Extension(name = "x-permission", value = @ExtensionProperty(value = "sys_user_view", parseValue = true))
    public R<IPage<SysUserVO>> getUserPage(Page<SysUser> page, SysUserQuery query) {
        return R.ok(sysUserService.getUserPage(page, query));
    }
    
    @PostMapping
    @Operation(summary = "新增用户", description = "创建新的系统用户,包含用户基本信息和角色分配")
    @PreAuthorize("@pms.hasPermission('sys_user_add')")
    @Extension(name = "x-permission", value = @ExtensionProperty(value = "sys_user_add", parseValue = true))
    public R<Void> save(@Valid @RequestBody SysUserSaveVO sysUser) {
        sysUserService.saveUser(sysUser);
        return R.ok();
    }
    
    @DeleteMapping("/{id}")
    @Operation(summary = "删除用户", description = "根据ID删除系统用户,级联删除用户角色关联")
    @PreAuthorize("@pms.hasPermission('sys_user_del')")
    @Extension(name = "x-permission", value = @ExtensionProperty(value = "sys_user_del", parseValue = true))
    public R<Void> removeById(@PathVariable Long id) {
        sysUserService.removeUserById(id);
        return R.ok();
    }
}

3.4 效果验证与测试

测试场景1:未认证用户访问文档
  1. 直接访问http://localhost:8080/swagger-ui.html
  2. 系统自动重定向到登录页面,要求进行OAuth2认证
测试场景2:普通用户访问文档
  1. 使用普通用户账号登录系统
  2. 访问接口文档,验证只能看到有权限的接口(如sys_user_view权限只能看到查询接口)
测试场景3:管理员用户访问文档
  1. 使用管理员账号登录系统
  2. 验证可看到所有接口文档,无权限过滤

四、常见问题与解决方案

4.1 文档白名单与权限控制冲突

问题描述:配置了权限控制但Swagger文档仍可匿名访问。

解决方案:检查PermitAllUrlProperties是否误将文档路径加入白名单:

// 错误配置:文档路径被加入匿名访问白名单
private static final String[] DEFAULT_IGNORE_URLS = new String[] {
    "/actuator/**", "/error", "/v3/api-docs", 
    "/swagger-ui.html", "/swagger-ui/**"  // 这会导致匿名访问
};

// 正确配置:移除文档路径的匿名访问权限
private static final String[] DEFAULT_IGNORE_URLS = new String[] {
    "/actuator/**", "/error"
};

4.2 权限注解不生效

问题描述:添加了@PreAuthorize注解但接口文档未过滤。

解决方案:检查是否启用了方法级安全注解:

// 确保配置类上添加了@EnableMethodSecurity
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用@PreAuthorize等注解
public class SecurityConfig {
    // ...
}

4.3 微服务文档聚合失败

问题描述:网关无法聚合其他服务的Swagger文档。

解决方案:检查微服务是否配置了spring-doc元数据:

# 在每个微服务的application.yml中添加
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          spring-doc: ${spring.application.name} # 用于文档聚合的标识

五、高级特性与扩展方案

5.1 基于RBAC的动态权限文档

实现根据用户角色动态展示不同接口文档的高级功能,可扩展PermissionBasedOpenAPICustomizer

// 扩展:支持基于角色的文档过滤
private boolean isOperationAccessible(Operation operation, Authentication authentication) {
    // 获取接口所需角色
    Object roles = operation.getExtensions().get("x-roles");
    if (roles == null) {
        return true;
    }
    
    Set<String> requiredRoles = new HashSet<>();
    if (roles instanceof String) {
        requiredRoles.add((String) roles);
    } else if (roles instanceof List<?>) {
        requiredRoles.addAll(((List<?>) roles).stream()
            .filter(String.class::isInstance)
            .map(String.class::cast)
            .collect(Collectors.toSet()));
    }
    
    // 检查用户是否拥有所需角色
    return authentication.getAuthorities().stream()
        .map(GrantedAuthority::getAuthority)
        .anyMatch(authority -> requiredRoles.contains(authority.replace("ROLE_", "")));
}

5.2 接口文档访问审计日志

为满足合规要求,可添加接口文档访问审计功能:

@Component
@Aspect
@RequiredArgsConstructor
public class SwaggerAccessAuditAspect {

    private final SysLogService sysLogService;

    @Around("execution(* org.springdoc.webmvc.ui.SwaggerWelcomeWebMvc.*(..)) || " +
           "execution(* org.springdoc.webmvc.api.OpenApiWebMvcResource.getOpenApi(..))")
    public Object auditSwaggerAccess(ProceedingJoinPoint joinPoint) throws Throwable {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            // 记录文档访问日志
            SysLog sysLog = new SysLog();
            sysLog.setUsername(authentication.getName());
            sysLog.setOperation("访问接口文档");
            sysLog.setMethod(joinPoint.getSignature().getName());
            sysLog.setParams("Swagger UI访问");
            sysLog.setCreateTime(new Date());
            sysLogService.save(sysLog);
        }
        
        return joinPoint.proceed();
    }
}

六、总结与展望

Pig系统通过SpringDoc + Spring Security + OAuth2构建的接口文档安全控制体系,实现了从"全开放"到"精细化权限控制"的安全升级。核心价值体现在:

  1. 多层次防护:资源服务器认证 → 文档权限过滤 → 方法级权限校验的三层防护
  2. 动态适应性:基于Nacos服务发现的动态文档聚合,适应微服务架构
  3. 细粒度控制:支持基于权限/角色的接口文档过滤,满足最小权限原则

未来可进一步增强的方向:

  • 实现接口文档的IP白名单控制
  • 添加接口调用频次限制
  • 支持文档操作审计与追溯
  • 开发文档权限管理UI界面

通过本文介绍的配置方案,开发团队可快速为Pig系统添加专业的接口文档安全控制,有效防范因接口文档泄露导致的安全风险,同时保持良好的开发体验。


相关资源

系列文章预告

  • 《Pig系统OAuth2认证流程深度解析》
  • 《基于Pig的微服务权限设计最佳实践》
  • 《Pig系统接口安全测试指南》

欢迎点赞收藏,持续关注Pig生态的更多技术实践!