零信任架构下的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);
}
}
}工作流程时序图:
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注解和自定义权限评估器实现:
- 权限注解定义:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@pms.hasPermission('{value}'.split(','))")
public @interface HasPermission {
String[] value();
}- 控制器方法应用示例:
@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));
}
}- 权限评估器实现:
@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:未认证用户访问文档
- 直接访问
http://localhost:8080/swagger-ui.html - 系统自动重定向到登录页面,要求进行OAuth2认证
测试场景2:普通用户访问文档
- 使用普通用户账号登录系统
- 访问接口文档,验证只能看到有权限的接口(如
sys_user_view权限只能看到查询接口)
测试场景3:管理员用户访问文档
- 使用管理员账号登录系统
- 验证可看到所有接口文档,无权限过滤
四、常见问题与解决方案
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构建的接口文档安全控制体系,实现了从"全开放"到"精细化权限控制"的安全升级。核心价值体现在:
- 多层次防护:资源服务器认证 → 文档权限过滤 → 方法级权限校验的三层防护
- 动态适应性:基于Nacos服务发现的动态文档聚合,适应微服务架构
- 细粒度控制:支持基于权限/角色的接口文档过滤,满足最小权限原则
未来可进一步增强的方向:
- 实现接口文档的IP白名单控制
- 添加接口调用频次限制
- 支持文档操作审计与追溯
- 开发文档权限管理UI界面
通过本文介绍的配置方案,开发团队可快速为Pig系统添加专业的接口文档安全控制,有效防范因接口文档泄露导致的安全风险,同时保持良好的开发体验。
相关资源:
系列文章预告:
- 《Pig系统OAuth2认证流程深度解析》
- 《基于Pig的微服务权限设计最佳实践》
- 《Pig系统接口安全测试指南》
欢迎点赞收藏,持续关注Pig生态的更多技术实践!
















