Skip to content

@Public 与白名单聚合机制

任何鉴权框架都要回答一个问题:哪些请求允许不带令牌直接进来?这条「放行清单」看似简单,却天然分裂在三个角色手里——框架内置的登录与文档端点、运维在 yml 里临时加的路径、以及各业务模块自己想暴露的接口。forge 没有把它们塞进同一份集中配置,而是用「@Public 注解 + AuthWhiteListProvider SPI 贡献 + Spider 并集聚合」三条来源汇成一份最终放行集。本篇解释为什么这样拆分,以及放行链路是如何串起来的。

接入与最小可用示例请看使用指南:白名单与免登录接口。涉及 SPI 扫描、包扫描与启动钩子的底层机制,可参考 前置知识

放行清单的三个来源

forge 把「允许不带令牌访问」拆成两种语义、三类来源,分别落在不同的角色与生命周期上。

来源形态谁来维护生效时机
框架内置ForgeWebSecurityConfig 硬编码列表框架作者拦截器注册时固定写入
运维配置forge.auth.white-list 列表运维 / 部署人员启动读取 ForgeAuthProperties
模块贡献AuthWhiteListProvider SPI 实现业务模块开发者Spider.getAllExtensions 扫描聚合
方法标注@Public 注解接口编写者拦截器运行期逐请求判定

前三类在拦截器注册阶段汇成静态的 excludePathPatterns,是「路径维度」的放行;第四类 @Public 则是运行期对命中的 Handler 做「方法维度」的判定。两者互补:前者适合一整片公共路径(如文档、健康检查),后者适合在一个本应鉴权的 Controller 里精确开一个口子。

框架内置基础白名单

ForgeWebSecurityConfig 把与「鉴权本身」和「接口文档」强相关的端点硬编码进基础白名单——这些路径不放行,系统根本无法登录或查看文档,没有任何配置化的必要。

基础白名单包含的路径
  • /v1/api/auth/login —— 登录端点,放行它才能换取令牌
  • /error —— 错误转发端点
  • /doc.html/swagger-ui/**/v3/api-docs/** —— 接口文档页面
  • /swagger-resources/**/webjars/** —— Swagger 静态资源

运维配置白名单

forge.auth.white-list 给部署侧留出一个不改代码的旋钮:临时把某条路径放开、灰度某个公共接口,都可以直接在 yml 里加。

yaml
forge:
  auth:
    white-list:
      - /v1/api/public/**
      - /v1/health

模块贡献白名单(SPI)

AuthWhiteListProvider 是一个 @SPI 契约,让每个业务模块声明「我自己有哪些路径要公开」:

java
@SPI
public interface AuthWhiteListProvider {
    List<String> contributeWhiteList();
}

这是本机制的核心。任意模块只要提供一个实现并通过 META-INF/services 注册,它贡献的路径就会被 Spider.getAllExtensions(AuthWhiteListProvider.class) 扫到并并入最终清单。若全工程没有任何实现,Spider 返回空集,拦截器照常工作——零实现零副作用。

@Public 方法级免登录

@Public 是类级或方法级注解,标注后对应接口免登录访问。它不参与路径清单的静态聚合,而是在拦截器里对命中的 Handler 直接判定。

java
@Operation(summary = "用户统计信息")
@Public
@GetMapping("/stats")
public Map<String, Object> getUserStats() {
    // 无需登录即可访问
}

聚合与放行链路

启动期:三类路径来源汇成 excludePathPatterns

ForgeWebSecurityConfig 在注册拦截器时,把内置列表、forge.auth.white-list 配置、以及 Spider 扫出的所有 AuthWhiteListProvider 贡献并集去重,作为 ForgeAuthInterceptorexcludePathPatterns;拦截范围则是 /v1/**/v2/**

运行期:单次请求的放行判定

请求进来后,ForgeAuthInterceptor 的判定顺序是:先看路径是否落在 excludePathPatterns(落在则压根进不了拦截器逻辑),再看命中的 Handler 是否带 @Public,最后才进入令牌校验。@Public 的检查先于取令牌,因此即便请求没带 Authorization 头也能放行。

Spider 并集聚合的角色

Spider.getAllExtensions 在这里扮演的不是「单选一个实现」的角色,而是「收集全部实现、取并集」。这与 AuthProvider 双实现选择 形成鲜明对照:AuthProvider 要求工程内恰好一个实现,多了少了都 fail-fast;而 AuthWhiteListProvider 是聚合贡献型扩展点,有几个就并几个,没有也不报错。

TIP

同一个扩展点基建(@SPI + Spider)支撑了两种截然不同的消费语义:互斥单选(AuthProvider)与聚合贡献(AuthWhiteListProvider)。理解这一点,就能理解 forge 为什么敢把放行清单交给各模块自治。

设计取舍

为什么用 SPI 聚合而非集中配置

集中配置(把所有公开路径写进一份全局 yml 或一个总清单类)是最直觉的做法,但它把「谁该公开哪些接口」这个本属于业务模块的知识,抽离到了模块之外。forge 选择让每个模块用 AuthWhiteListProvider 自带白名单,路径声明与接口定义同处一个模块、同生共死。

选项选择理由
全局集中配置一份白名单公开路径知识脱离业务模块,模块增删时易遗漏;改动需触碰全局文件,耦合所有模块
各模块 SPI 贡献 + 框架并集模块自治、就近声明;模块装配即生效、卸载即消失;新增模块零侵入框架
仅靠 @Public 注解注解只能逐方法标,无法表达 /swagger-ui/** 这类整片路径前缀

为什么保留三类来源而不统一成一种

三类来源对应三种稳定度不同的知识:内置端点几乎不变、运维路径需要不改代码即可调整、模块路径随业务演进。把它们强行合并成一种,要么牺牲框架端点的不可配置性(误删 login 直接锁死系统),要么逼运维去改代码,要么逼模块去碰全局配置。分层来源 + 启动期并集,让每一类知识都待在最合适的位置。

选项选择理由
全部塞进 forge.auth.white-list框架端点变得可被误删,运维清单膨胀且与业务强耦合
全部走 SPI运维无法不改代码临时放行,丧失部署期灵活性
内置 + 配置 + SPI 三层并集每类知识各居其位,互不污染,启动期统一汇聚

为什么 @Public 与路径白名单并存

路径白名单是「前缀粒度」的,适合一整片公共区域;@Public 是「方法粒度」的,适合在一个整体鉴权的 Controller 里精确开口。二者粒度互补:用路径前缀覆盖整片公共接口成本最低,用注解在受保护资源里开单点最精确、也最容易在代码评审中被看见。

选项选择理由
只有路径白名单在受保护 Controller 里开单个口子需写裸路径,易与实际映射脱节
只有 @Public无法表达 /webjars/** 这类整片前缀,要逐个方法标注
两者并存、@Public 先于取令牌判定粗放行用前缀、精开口用注解;注解与方法同在,评审时一眼可见

WARNING

@Public 标在一个本应鉴权的接口上等于显式开了一道免登录口子。它最大的便利也是最大的风险:容易随手加、随后忘。评审受保护模块时应把 @Public 当作敏感改动重点核对。

小结

forge 的放行机制围绕一个判断展开:放行清单不是单一所有者的资产,而是框架、运维、各业务模块共同贡献的并集。@SPI + Spider.getAllExtensions 的聚合语义让模块得以就近自治地声明公开路径,forge.auth.white-list 给运维留出不改代码的旋钮,@Public 则在方法粒度上补齐精确开口。三者在启动期与运行期分别汇聚,构成完整放行链路。具体配置与排查见 白名单与免登录接口