Nacos Visibility Plugin 规范解析:面向 AI 注册资源的查询期可见性控制实现指南
【免费下载链接】nacosan easy-to-use dynamic service discovery, configuration and service management platform for building AI cloud native applications.项目地址: https://gitcode.com/GitHub_Trending/na/nacos
导读
本文基于 Nacos 仓库中的 visibility-plugin-spec.md 规范文档,结合 plugin/visibility 模块 SPI 源码与 nacos-default-auth-plugin 内建实现,系统讲解 Nacos 可见性(Visibility)插件体系的设计思想、资源模型、SPI 契约、查询下发(Query Advisory)机制、插件状态与配置方式,以及与鉴权(Auth)的协作边界。读者读完后,将能理解如何在 Nacos 中为 AI 注册域(如 Agent、MCP Server、Skill 等资源)实现"私有、公开、显式授权可见"三类可见性语义,并能正确集成或自研一个VisibilityServiceSPI 实现,避免列表查询中常见的 totalCount 错乱与私密资源泄露问题。
一、可见性插件要解决什么问题:与鉴权的职责边界
在 Nacos 插件体系中,可见性(Visibility)与鉴权(Auth)是两个相互独立、职责互补的插件类别:
- Auth 决定身份(identity)与权限(permission):回答"这个调用者是谁、能不能对目标资源执行目标动作";
- Visibility 决定可见性:回答"目标资源,或者某个范围查询(range query)结果中的资源,是否应当对当前身份可见"。
可见性插件是领域无关(domain-neutral)的。当前 Nacos 集成将其应用于 AI 注册资源域:用户创建的资源可以是"仅属主可见(private)"、"对读者公开(public)",或者"通过显式授权可见"。它补充了 Auth And Permission Spec,并遵循 Nacos Plugin Spec 中定义的通用生命周期规则;它可以与 auth plugin 协作,但不能取代鉴权插件。
规范特别强调了一个架构级约束:可见性必须在数据查询期(data-query time)施加。List 与 Search API 绝不能先对原始候选集分页、再在内存里过滤当前页,因为这会带来三类问题:
totalCount不准确:分页基于未过滤集合计算,返回的可见资源总数错误;- 出现空页:过滤掉大量不可见资源后,本应存在的页可能变成空白;
- 延迟不可预测:大集合上的内存过滤无法利用存储层索引。
这一点在 Query Advisory 一节会看到具体的落地方式。
二、资源模型:可见性感知资源必须提供哪些字段
一个"可见性感知(visibility-aware)"的资源必须遵循 Nacos 资源模型,并额外提供以下字段:
| 字段 | 含义 |
|---|---|
namespaceId | 拥有该资源的命名空间。 |
resourceType | 命名空间内的资源类别。 |
resourceName | 该类型内的稳定资源名。 |
scope | 可见性范围,当前取值为PUBLIC或PRIVATE。 |
owner | 拥有该资源的身份。 |
资源遵循 Nacos 资源层级:
NamespaceId -> resourceType -> resourceName在源码层面,这一模型由抽象基类 VisibilityResource.java 落地:namespaceId、resourceName、resourceType为抽象方法(由具体领域资源实现),scope与owner为实例字段,且默认值分别是PRIVATE与空字符串——即"未显式声明即为私有、无属主",从默认值层面就保证了私密资源的保守语义。取值常量集中在 VisibilityConstants.java:
SCOPE_PUBLIC = "PUBLIC"、SCOPE_PRIVATE = "PRIVATE";ACTION_READ = "r"、ACTION_WRITE = "w"。
三、Visibility SPI:五个方法构成的插件契约
可见性插件实现VisibilityServiceSPI 接口(VisibilityService.java),接口本身继承自PluginConfigSpec,从而纳入统一插件配置体系。其方法契约如下:
| 方法 | 要求 |
|---|---|
getVisibilityServiceName() | 返回稳定的插件名(如内建实现的nacos)。 |
init(properties) | 已废弃的遗留初始化回调,仅供未使用统一插件配置的实现使用,计划在 Nacos 4.0.0 移除。 |
resolveDefaultScopeForCreate(identity, apiType, resourceType) | 当创建资源未显式指定 scope 时,决定默认 scope;默认实现返回PRIVATE。 |
validateVisibility(identity, action, apiType, resource) | 对单个资源做可见性校验,返回ValidationResult。 |
adviseQuery(identity, action, apiType, queryContext) | 为范围查询返回查询谓词与显式授权资源列表(即QueryAdvisor)。 |
插件通过 SPI 机制被发现,并以visibility作为插件类型注册。服务名在启动时通过如下配置选择:
nacos.plugin.visibility.type=nacos该选择重启生效。它决定 AI 域请求的具体实现、以及统一插件管理中该实现的默认启用状态;它不是实现自己拥有的ConfigItemDefinition。
3.1 单资源校验与范围查询建议
validateVisibility返回 ValidationResult.java,提供allow()与deny(reason)两个静态工厂方法,deny可携带原因供上层日志或响应使用。
adviseQuery接收 VisibilityQueryContext.java(仅含namespaceId与resourceType两个最小上下文字段),返回 QueryAdvisor.java。QueryAdvisor的默认值为basePredicate = PUBLIC_AND_OWNER、authorizedPredicate = new AuthorizedResources()——也就是说,即使实现方忘记设置基础谓词,默认也会保守地只暴露"公开或属主"资源。
3.2 动作语义:读写采用与鉴权一致的词汇表
可见性沿用与鉴权相同的读写动作词汇:
| 动作 | 含义 |
|---|---|
r | 读取或列出可见资源。 |
w | 创建、更新、删除,或变更对可见性敏感的资源状态。 |
写可见性必须比读可见性更严格:公开读(public read)绝不意味着公开写(public write)。这一原则在内建实现中体现得非常直接——见下文第七节。
四、范围查询不得"全量加载后内存过滤"
当存储层可以施加可见性谓词时,范围查询禁止"加载全部资源、仅靠内存过滤"的朴素做法。为此,QueryAdvisor携带两类信息:
| 字段 | 用途 |
|---|---|
BaseVisibilityPredicate | 基础谓词,如:全部资源、仅公开、仅属主、公开加属主。 |
AuthorizedResources | 显式授权的资源名列表,应被纳入查询范围。 |
负责列出资源的 API 或存储适配器必须将两部分组合起来,且不得泄露私有资源。AuthorizedResources.java 是一个与存储无关的授权资源集合,包含resourceType与resources(资源名列表),便于领域适配器按类型批量翻译成查询条件。
五、查询下发(Query Advisory)机制:F AND (B OR G)组合规则
这是本规范最核心、也最容易实现出错的部分。默认领域集成会在 count 与分页查询执行前,将QueryAdvisor转换为仓库层QueryCondition。定义三个符号:
F:调用方已携带的业务过滤条件(例如请求中显式的scope或owner过滤);B:解析后的BaseVisibilityPredicate;G:name IN AuthorizedResources。
转换器必须产出:
final query = F AND (B OR G)5.1 基础谓词B的解析规则
B必须独立于G自行解析,结果只能是三种形态之一:恒满足(always satisfied)、恒不满足(never satisfied)、一组 OR 分支。解析规则如下表:
| 谓词 | B解析结果 |
|---|---|
ALL | 恒满足;不增加任何可见性条件。 |
PUBLIC | 当scope=PUBLIC时满足;当调用方业务过滤与公开范围冲突时恒不满足。 |
OWNER | 当owner=identity时满足;当身份缺失、或调用方业务过滤与"该身份作为属主"冲突时恒不满足。 |
PUBLIC_AND_OWNER | 当scope=PUBLIC OR owner=identity时满足;匿名调用者降级为PUBLIC解析;仅当调用方业务过滤同时把 scope 与 owner 固定为与两个分支都冲突的值时才恒不满足。 |
上述四种谓词正是 BaseVisibilityPredicate.java 中定义的枚举值:ALL、PUBLIC、OWNER、PUBLIC_AND_OWNER。
5.2 并集必须在简化成具体 QueryCondition 之前完成
只有在B解析完成之后,才能与G做并集:
B恒满足 →G变得无关紧要(B OR G依然恒满足);B恒不满足 →B OR G塌缩为仅剩G;B解析为一组 OR 分支 →G作为新增的一个 OR 分支并列加入。
这个并集动作必须发生在任何"简化为具体 QueryCondition 形态"(硬字段、OR 组、或alwaysEmpty)之前。如果先把B解析成具体条件(此时G尚不可知),很可能:
- 把本应是并集(union)的语义悄悄变成交集(intersection);
- 或者把整个查询标记为
alwaysEmpty,而事实上F AND G仍可能有匹配结果。
5.3 业务过滤必须在 QueryAdvisor 应用之前进入基础条件
调用方自带的业务过滤(owner、scope 等)必须先存在于基础QueryCondition中,再应用QueryAdvisor:它们既用于计算F,也用于在转换器决定"输出 OR 组还是回退到alwaysEmpty"之前,剪除B中已经满足或已经不可能的谓词分支。资源类型实现不得在转换后重置这些字段,以免覆盖插件的可见性约束。
5.4 显式授权资源G的填充与"永不丢弃"原则
如果AuthorizedResources非空,G按上述并集规则作为 OR 分支与B并列(当B单独恒不满足时则取而代之)——绝不因为B无法独立满足就丢弃G。默认可见性实现会从所选鉴权插件持有的、插件自有的显式授权记录中填充该列表。特别地:
- 存储的写授权隐含着读可见性(能写必然能读);
- 读授权只影响读/列表查询,不赋予写能力。
六、插件状态与配置:总开关、实现开关与统一配置三层关系
运行时可用性需要两层条件同时满足:家族级总开关 +visibility:{serviceName}的统一插件状态。
6.1 家族级总开关:最外层运行时门
nacos.plugin.visibility.enabled=true- 该开关是最外层运行时门(outer runtime gate)。当它为
false时,任何可见性实现都不得执行,无论其统一插件状态如何; - 核心插件管理器不会把该开关转换为实现状态;
- 开关为
false时,启动还会延迟可见性实现的发现; - 服务端配置刷新将其改为
true时,会触发一次性的:实现发现 → 持久化状态恢复 → 统一配置应用,之后可见性服务才可用; - 发现完成后再把开关改回
false,实例仍保持注册,但外层门会阻止其执行。
6.2 实现级初始状态与配置项
实现初始状态按如下优先级确定:
- 兼容性选择器
nacos.plugin.visibility.type; - 标准实现键
nacos.plugin.visibility.{serviceName}.enabled; - 持久化状态优先于以上两者,但永远不能覆盖家族级总开关。
实现级的运行时变更通过插件管理 API 完成。外部实现可以在如下命名空间下拥有自有属性:
nacos.plugin.visibility.{serviceName}.{itemKey}6.3 统一配置与遗留回调的互斥
VisibilityService继承PluginConfigSpec。内建的visibility:nacos实现没有私有配置、不声明任何 definitions、对外表现为configurable=false。相关约束:
- 针对旧 SPI 编译的遗留实现、以及不声明 definitions 的实现,会一次性通过
VisibilityService.init(Properties)收到服务级本地属性; - 使用非空遗留属性会输出迁移警告(不记录配置值本身,避免敏感信息入日志);
- 当实现上报
isConfigurable()=true时,可见性管理器不得调用遗留回调,核心插件管理器的统一applyConfig生命周期是它唯一的配置应用路径——此类实现声明自己的 definitions,获得统一的来源、元数据、掩码与更新语义。
6.4 禁用时的降级行为
如果所选插件被禁用或不可用:
- 当前 AI 域会跳过可见性过滤与单资源可见性校验;
- 创建资源时回退到
PRIVATEscope(沿用历史禁用行为)。
注意:这与鉴权是否启用无关,不能混淆。内建插件同样把"鉴权被禁用"视为"允许可见性"。
七、内建实现源码剖析:DefaultVisibilityService
规范中"默认 auth 插件实现提供当前内建可见性实现"这一点,在 nacos-default-auth-plugin 模块中得到确认,核心类为 DefaultVisibilityService.java。
7.1 单资源校验逻辑(validateVisibility)
校验顺序(从宽松到严格)清晰地体现了"公共读 ≠ 公共写"的原则:
isAuthDisabled(apiType):对应 API 范围的鉴权被禁用 →allow();isCurrentIdentityGlobalAdmin(identity):全局管理员 →allow();isPermitted(...):属主、或(读动作且scope=PUBLIC)、或显式资源权限命中 →allow();- 否则
deny("No visibility permission for resource: " + resourceName)。
其中isPermitted的判定顺序是:先判isOwner(属主对任何动作都可见可写),再判"读 + PUBLIC",最后才委托给鉴权插件做显式权限检查。写动作永远不享受PUBLIC带来的可见性——公开只对读生效。
7.2 范围查询建议逻辑(adviseQuery)
- 鉴权禁用或全局管理员 →
basePredicate = ALL(对全部资源可见); - 非读动作(写类列表)→
basePredicate = OWNER,并附带显式授权资源(仅属主 + 显式授权可写); - 读动作→ 匿名身份用
PUBLIC,否则PUBLIC_AND_OWNER,并附带显式授权资源。
匿名调用者降级为PUBLIC解析,与规范 5.1 节表格完全一致;而写查询默认只暴露OWNER,再次印证"写可见性严于读可见性"。
7.3 显式授权如何委托给鉴权插件
checkResourcePermission把可见性资源翻译为鉴权插件的Resource与Permission,然后调用AuthPluginService.validateAuthority(...)。资源标识符的构造为:
@@visibility/{namespaceId}/{resourceType}/{resourceName}其中namespaceId为空时使用默认命名空间。显式可见性权限资源使用领域自有的资源字符串与SignType.SPECIFIED。buildAuthorizedResources则通过 Spring 容器按需获取VisibilityGrantService的 Bean,查询当前身份在指定命名空间、资源类型、动作下被显式授权的资源名列表——该列表正是QueryAdvisor中G分支的数据来源。
八、与鉴权的关系:职责分离与授权管理 API
8.1 职责分离
可见性插件可以把显式权限检查委托给所选鉴权插件,从而保持关注点分离:可见性决定候选资源集合,鉴权仍是权限判定的唯一来源。默认资源标识符格式即上文所示的@@visibility/{namespaceId}/{resourceType}/{resourceName},配套 default-auth-plugin-spec.md 使用。
8.2 VisibilityResourceLocator 轻量查找桥
当插件自有的授权管理 API 需要校验资源存在性或属主元数据时,领域可以暴露轻量查找桥 VisibilityResourceLocator.java:
Optional<VisibilityResource> findResource(String namespaceId, String resourceType, String resourceName);这样鉴权/可见性插件就能解析namespaceId、resourceType、resourceName、owner、scope,而不必对领域持久化类建立编译期依赖——典型的分层解耦手段。
8.3 默认授权管理 API
默认内建授权管理 API 如下:
POST /v3/auth/visibility DELETE /v3/auth/visibility在源码中对应 VisibilityGrantControllerV3.java,其映射路径常量定义于 AuthConstants.java(VISIBILITY_PATH = "/v3/auth/visibility")。这些端点是插件自有的鉴权 API,必须使用ApiType.ADMIN_API,且grant/revoke操作都以ActionTypes.WRITE保护。默认实现不提供管理侧的授权列表查询端点。
九、API 集成要求:任何返回可见性感知资源的 API 必须遵守
规范对 API 层提出了明确的硬性要求,任何返回可见性感知资源的 API 都必须:
- 用
validateVisibility校验单资源的读/写操作; - 对列表或搜索操作,在返回数据前应用
adviseQuery; - 创建或更新资源时保留 owner 与 scope 元数据;
- 若领域暴露显式授权管理 API,在变更授权前校验资源存在性与管理权限;
- 避免通过 count、错误信息或部分列表响应泄露私有资源名;
- 当 API 需要隐藏资源存在性时,对被拒绝的单资源读返回not found;
- 对被拒绝的写返回access denied。
十、配置速查与实战要点
在 distribution/conf/application.properties 中可见相关配置的默认形态(第 430–434 行附近):
### 内建 visibility:nacos 实现默认启用 nacos.plugin.visibility.nacos.enabled=true ### 家族级总开关(默认注释,按需放开) #nacos.plugin.visibility.enabled=true ### 服务选择器(默认注释,按需放开) #nacos.plugin.visibility.type=nacos结合前文,实战中请记住四条要点:
- 改
nacos.plugin.visibility.type需重启生效,它同时决定 AI 域请求的实现与统一插件管理的默认启用状态; nacos.plugin.visibility.enabled是外层总闸,为false时任何实现都不得执行;改为true会触发一次性的发现与配置恢复;- 实现级开关形如
nacos.plugin.visibility.{serviceName}.enabled,持久化状态优先,但无法越过总闸; - 禁用可见性不等于禁用鉴权:AI 域在可见性禁用时跳过过滤、创建回退
PRIVATE,但鉴权插件仍然独立工作。
十一、从源码到测试:验证行为一致性的证据链
仓库中为 SPI 模型提供了配套单元测试,例如 VisibilityResourceTest.java、AuthorizedResourcesTest.java、ValidationResultTest.java 等;内建实现侧则有 DefaultVisibilityServiceTest.java 与 VisibilityGrantControllerV3Test.java,覆盖了校验分支与授权管理端点的行为。阅读这些测试是理解"匿名降级 PUBLIC""写查询仅 OWNER""auth 禁用即放行"等边界语义最直接的途径。
十二、小结
可见性插件是 Nacos AI 注册域实现"私有 / 公开 / 显式授权"资源语义的基石:它以VisibilityServiceSPI 统一了单资源校验(validateVisibility)与范围查询建议(adviseQuery),以F AND (B OR G)的组合规则保证列表查询既正确又高效,以"外层总开关 + 实现级状态 + 统一配置"的三层体系管理插件生命周期,并严格遵循"可见性定候选、鉴权定权限"的职责分离。无论是集成默认实现、还是基于 plugin/visibility 的 SPI 自研扩展,把握"查询期施力、先并集后简化、写严于读、禁用不等于放行鉴权"这四条主线,即可构建正确且安全的可见性控制层。
【免费下载链接】nacosan easy-to-use dynamic service discovery, configuration and service management platform for building AI cloud native applications.项目地址: https://gitcode.com/GitHub_Trending/na/nacos
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考