
最终裁决以及两层 API简单层/高级层如何覆盖全量排他、OR 鉴权、混合匿名与多策略共存全场景。随后揭示六位插槽 Filter 扩展点的安装≠启用三层控制模型MVC 与 WebFlux 双栈对等的适配器架构认证事件与分级日志构成的完整可观测性闭环以及 gj-pf4j-security 模块如何以声明式扩展接口实现零侵入集成。开源不易如果觉得本项目对您的工作或者学习还是有帮助的话 请帮忙在GitHub 点个⭐️Star, 更多实现细节及完整配置参数详见 GitHub 仓库README和Wiki。gj-pf4j 插件化安全架构全景左侧 Provider 链式认证引擎负责你是谁右侧六位 Filter 插槽负责在什么阶段做什么中间生命周期引擎编排两者的注册与回收三者协同贯穿插件的安全生命周期。gj-pf4j 插件安全架构基于 PF4J 内核深度整合 Spring 生态。不依赖 Spring BootMVC/WebFlux 双栈自适应轻量无侵入对中小项目极友好原生适配达梦、人大金仓、GaussDB 等国产数据库插件代码完全标准 Spring 注解 -可插拔扩展点允许任意第三方组件以零框架改动的方式接入插件生命周期。开放插件自定义鉴权让插件通过 SPI 自由定义认证逻辑Provider 链式委托配合可插拔策略分发请求六位 Filter 插槽供宿主编排任意安全过滤器——受安装 ≠ 启用模型统一管控。温馨提示全文约6100 字左右阅读时长约15分钟。如果你正在为插件设计鉴权方案或者好奇一个插件框架如何把鉴权的事比较优雅地下放给插件——这篇值得你慢慢看一、从一个真实的支付插件说起假设你正在开发一个微信支付插件。它有四个接口POST /wxpay/order/create 创建订单 → 内部员工操作走宿主鉴权GET /wxpay/order/{id} 查询订单 → 同上宿主 session 即可POST /wxpay/order/refund 申请退款 → 内部员工操作但需要额外验 JWT TokenPOST /wxpay/order/callback 支付回调 → 微信服务器调用验 API Key 签名四个接口三种认证方式宿主 Session、JWT Token、API Key 签名。回调接口尤为特殊——它是微信服务器调过来的没有宿主 Session不走登录流程。它只认一个东西你拿着微信发给你的 API Key对请求体做了正确的签名。在之前插件完全做不到这一点。插件的 Controller 注册到宿主 DispatcherServlet请求全部经过同一安全链——宿主用什么鉴权方式插件就只能被动继承。回调接口既不走宿主鉴权又不是完全公开不能直接标 AllowAnonymous卡在中间无法处理。更进一步——即使创建订单走宿主 Session 就够了申请退款需要额外验 JWT。同一个插件内不同接口需要不同的认证策略。这在之前也是做不到的。答案只有一个让插件能定义自己的认证方式但授权仍由宿主统一裁定。二、快速开始宿主引入 gj-pf4j-security 模块即可启用全部鉴权能力插件侧依赖相同scope 改为 provided运行时由宿主提供。io.github.wangpengxpy gj-pf4j-security {version} gj-pf4j 16 项核心能力三、设计边界只做鉴权不做授权这是我们做的第一个关键决策。我们采取集中式策略引擎——从插件各自鉴权迁移到集中化鉴权采用集中化授权框架做统一判定。授权集中化不下放给插件——插件只负责你是谁的认证环节授权决策完全由宿主接管。插件负责 → “你是谁”认证 Authentication宿主负责 → “你能干什么”授权 Authorization回到支付插件的例子插件只管验 API Key 签名、验 JWT Token返回 Authentication 对象。至于这个通过签名验证的微信支付回调有没有权限调接口——由宿主的 AuthorizationFilter 统一裁决。插件不参与授权决策。四、认证模型链式委托与可插拔策略委托式 Provider 链 · 认证编排引擎认证链是委托鉴权的执行内核。下图完整展示了 Provider 链从 supports() 认领、authenticate() 执行到策略裁决、四分支结果的完整流转。4.1 多个认证源支付插件的四个接口需要三种认证方式。同一个请求过来它可能携带 API Key 签名来自微信回调也可能携带 JWT Token来自内部员工的退款申请也可能只有宿主 Session来自内部员工的订单查询。我们的方案是认证提供者链多个 Provider 按优先级排成链依次认领并认证同一个请求。以支付插件为例注册两个 ProviderPOST /wxpay/order/callback 请求到达微信服务器│▼Provider 1 — WxPayCallbackSignatureProvider (order100)supports() → 请求路径是否以 /callback 结尾├─ 否 → 跳过继续下一个└─ 是 → authenticate()├─ 从 Header 取 Wechatpay-Signature├─ 用 API Key 对请求体做 HMAC-SHA256 签名验证├─ 签名匹配 → 注入 SecurityContext → 放行└─ 签名不匹配 → PluginBadCredentialsException → 401Provider 2 — WxPayJwtProvider (order500)supports() → 默认 true兜底认领剩余所有请求authenticate()├─ 从 Header 取 Authorization: Bearer├─ 无 token → 返回 null → 回退宿主鉴权├─ 有 token → 校验 JWT → 注入 SecurityContext└─ token 过期 → PluginBadCredentialsException → WARN 日志继续注意 WxPayCallbackSignatureProvider 的 supports() 只认 /callback 结尾的请求。创建订单、查询订单、退款都不匹配直接跳过交给下一个 Provider。这就是 supports() 的精细认领——只在需要时接管。而 WxPayJwtProvider 作为兜底supports() 默认 true认领所有剩余请求。对于普通订单查询无 JWTauthenticate() 返回 null框架回退到宿主鉴权。对于退款操作有 JWT返回 Authentication 注入上下文。4.2 链的编排策略Provider 链的何时继续、何时终止、最终如何判定不是硬编码的——我们借鉴 Apache Shiro 的 ModularRealmAuthenticator 设计将其抽象为可插拔的 AuthenticationStrategypublic interface AuthenticationStrategy {Decision onAttempt(ProviderAttempt attempt);Authentication onCompletion(List attempts);record ProviderAttempt( IPluginAuthenticationProvider provider, Authentication result, PluginAuthenticationException exception, long durationMs ) {}}框架内置两种策略策略 行为 支付插件中的表现AtLeastOneSuccessfulStrategy默认 所有支持的 Provider 都试任一成功即通过 回调请求WxPayCallbackSignatureProvider 成功 → 放行。退款请求WxPayJwtProvider 成功 → 放行。查询请求两个都无结果 → 回退宿主鉴权FirstSuccessfulStrategy 第一个成功就停 同上但第一个 Provider 成功后立即终止4.3 认证事件与日志可观测性不侵入 Provider每轮 Provider 调用都有完整日志。以支付插件一次回调为例[Plugin: payment] Auth SUCCESS via WxPayCallbackSignatureProvider in 3ms: principalmch_123456[Plugin: payment] Auth chain: 1 success, 0 failed, 3ms total → principalmch_123456退款操作——JWT 已过期[Plugin: payment] Bad credentials via WxPayJwtProvider in 5ms: token expired at 2026-07-05T12:00:00Z[Plugin: payment] Auth chain exhausted: 2 provider(s) all failed in 5ms → 4016 种场景各配 INFO/WARN/ERROR/DEBUG 级别统一 [Plugin: {id}] 前缀每条日志带耗时。更进一步框架发布认证事件——PluginAuthenticationSuccessEvent 和 PluginAuthenticationFailureEvent。宿主监听即可实现告警Componentpublic class PaymentAuthAudit {EventListenerpublic void onCallbackAuth(PluginAuthenticationSuccessEvent e) {if (“WxPayCallbackSignatureProvider”.equals(e.getProviderName())) {auditLog.info(“微信支付回调认证成功: mch{}, {}ms”,e.getAuthentication().getName(), e.getDurationMs());}}EventListener public void onAuthFailure(PluginAuthenticationFailureEvent e) { if (e.getDurationMs() 2000) { alerting.send(插件 [{}] 认证耗时异常: {}ms, e.getPluginId(), e.getDurationMs()); } }}Provider 代码里一行日志都不需要写。事件体系完全在 Provider 外部运作。五、两层 API简单与高级各司其职框架提供两层认证 API适配两种截然不同的开发场景。简单层面向大多数接口——写一个 authenticate() 方法框架自动处理路由分发。高级层面向特殊接口——supports() 精细控制认领范围完全自定义路由逻辑。两层可共存框架自动检测冲突。5.1 简单层只写认证逻辑路由框架自动处理对于大部分插件接口创建订单、查询订单、退款用 JWT 统一认证。开发者只写 authenticate() 方法Componentpublic class WxPayJwtProvider extends AbstractPluginAuthenticationProvider {Overridepublic Authentication authenticate(HttpServletRequest request)throws PluginAuthenticationException {String token request.getHeader(“Authorization”);if (token null || !token.startsWith(Bearer )) {return null; // 无 JWT → 不强制回退宿主鉴权}Claims claims JwtUtils.verify(token.substring(7));return new UsernamePasswordAuthenticationToken(claims.getSubject(), null, extractAuthorities(claims));}}框架在插件启动时自动采集该插件的所有 URL pattern——/wxpay/order/create、/wxpay/order/refund——注入到 supports() 中。路由分发完全自动开发者不需要写一行 if (uri.contains(…))。5.2 高级层精细控制回调接口自主认领回调接口比较特殊——它需要按路径匹配且认证方式完全不同验 API Key 签名而非 JWT。这适合走高级层Order(100) // 比简单层的 WxPayJwtProvider (默认 500) 先执行Componentpublic class WxPayCallbackSignatureProvider implements IPluginAuthenticationProvider {Override public boolean supports(HttpServletRequest request) { return request.getRequestURI().endsWith(/callback); } Override public Authentication authenticate(HttpServletRequest request) throws PluginAuthenticationException { String signature request.getHeader(Wechatpay-Signature); String timestamp request.getHeader(Wechatpay-Timestamp); String nonce request.getHeader(Wechatpay-Nonce); if (signature null || timestamp null || nonce null) { throw new PluginBadCredentialsException(缺少微信支付签名参数); } String body new String(request.getInputStream().readAllBytes()); String expectedSign HmacUtils.hmacSha256(apiKey, timestamp \n nonce \n body); if (!expectedSign.equals(signature)) { throw new PluginBadCredentialsException(微信支付签名验证失败); } return new UsernamePasswordAuthenticationToken(mch_ mchId, null, List.of()); }}关键设计Order(100) 让它在 WxPayJwtProvider默认 500之前执行。回调请求先被 supports() 匹配到认证成功后就结束了WxPayJwtProvider 不会参与。非回调请求不匹配 supports()跳过由后面的 Provider 处理。5.3 两层共存框架自动冲突检测支付插件恰好有两层共存——简单层的 WxPayJwtProvider 高级层的 WxPayCallbackSignatureProvider。框架在启动时自动检测发现 AbstractPluginAuthenticationProvider 子类: WxPayJwtProvider发现其他 IPluginAuthenticationProvider 实现: WxPayCallbackSignatureProvider检查 PluginAuthenticated 标注情况 → 已准确标注简单层注入 authenticatedPaths高级层原样注册 → OK如果开发者没有标注 PluginAuthenticated简单层会默认接管全部接口意外覆盖高级层的回调处理。框架检测到这个冲突后ERROR 日志并自动跳过简单层[Plugin: payment] Simple-layer provider (WxPayJwtProvider) found alongside1 advanced provider(s) without PluginAuthenticated annotation.Skipping simple-layer to prevent conflict.这是典型的错误时大声报错而非静默失败。六、一个注解会话优先与独占鉴权免鉴路由 · 双通道注册一个注解两种模式——PluginAuthenticated 声明式切换会话优先与独占鉴权。下图展示了它在 MVC 注解式路由和 WebFlux 函数式路由中的双通道等价实现我们回到支付插件的 ControllerRestControllerRequestMapping(“/wxpay/order”)public class WxPayOrderController {PluginAuthenticated // ← OR 鉴权 PostMapping(/refund) public Result refund(RequestBody RefundRequest req) { // 内部员工操作Session 用户直接放行外部系统走 JWT } GetMapping(/{id}) // ← 未标注走宿主 session public Result getOrder(PathVariable String id) { // 普通查询宿主鉴权即可 } PluginAuthenticated // ← OR 鉴权 PostMapping(/create) public Result createOrder(RequestBody OrderRequest req) { // 同退款Session 放行 / JWT } AllowAnonymous(reason 微信支付回调由微信服务器发起通过 API Key 签名验证) // ← 匿名 PostMapping(/callback) public Result callback(RequestBody String body) { // 不是真的匿名——WxPayCallbackSignatureProvider 在 Filter 层做了签名验证 }}四种接口三种行为接口 注解 认证流程GET /{id} 无 宿主 SessionPOST /create PluginAuthenticated Session 优先无 Session → JWTPOST /refund PluginAuthenticated Session 优先无 Session → JWTPOST /callback AllowAnonymous 跳过认证链 → WxPayCallbackSignatureProvider 在 Filter 层验签PluginAuthenticated 区分了两种模式模式 触发条件 Session 用户的处理全量排他Controller 无 PluginAuthenticated 忽略宿主 Session必过 authenticate()OR 鉴权Controller 有 PluginAuthenticated Session 优先放行无 Session 才走 authenticate()PluginAuthenticated 支持标注在类或方法上方法级优先于类级。同时标注 AllowAnonymous 则 AllowAnonymous 生效。WebFlux 函数式路由通过 .pluginAuthenticated() 链式调用声明与注解语义完全等价return GJRouterFunctions.route().POST(“/wxpay/order/refund”, handler::refund).pluginAuthenticated().POST(“/wxpay/order/callback”, handler::callback, “微信支付回调验签”).build();七、不止认证Filter 扩展点的六位插槽模型认证之外插件还可以在 Spring Security 标准链上的六个位置插入自定义 Filter——从请求预处理到响应后加工覆盖整条安全链。插件只需实现对应接口宿主通过三层配置控制是否启用。认证链与 Filter 扩展点互补认证负责你是谁Filter 负责在什么阶段做什么。7.1 真实需求回调请求需要做防重放微信支付回调还有一个额外需求防重放攻击。微信的签名参数中包含timestamp和nonce我们需要在认证之前就检查Timestamp 与服务器时间差不超过 5 分钟Nonce 在 5 分钟内没有被用过Redis 缓存检查这两个检查不是认证——它们不产生 Authentication 对象——但它们需要在认证之前执行。这就是 Filter 扩展点的用武之地Componentpublic class ReplayAttackFilter implements FirstFilterExtension {Overridepublic Filter getFilter() {return (req, res, chain) - {String path PluginHttpUtils.getPathWithinApplication((HttpServletRequest) req);if (!path.endsWith(“/wxpay/order/callback”)) {chain.doFilter(req, res);return;}String timestamp ((HttpServletRequest) req).getHeader(“Wechatpay-Timestamp”);String nonce ((HttpServletRequest) req).getHeader(“Wechatpay-Nonce”);if (Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(timestamp)) 300) {((HttpServletResponse) res).sendError(400, “timestamp expired”);return;}if (redis.setIfAbsent(“nonce:” nonce, “1”, Duration.ofMinutes(5))) {chain.doFilter(req, res);} else {((HttpServletResponse) res).sendError(400, “nonce reused”);}};}Overridepublic int getOrder() { return 0; }}它实现了 FirstFilterExtension 接口返回一个 Filter。框架在插件启动时扫描到它检查宿主是否开放了 FIRST 位置如果开放则动态注入安全链。7.2 六位插槽全景安全链上共 7 个位置其中 AUTHENTICATION位置 [4]由框架的 IPluginAuthenticationProvider 认证链占用插件不实现此位置的 Filter 扩展接口。其余 6 个位置——FIRST、SESSION_RESTORE、FORM_LOGIN、ANONYMOUS、PRE_AUTHORIZE、LAST——开放给插件实现。六位插槽全景支付插件只用到 FIRST 和 LAST 两个位置。其余位置空置——不影响安全链性能代理 Filter 检测到空列表后直接透传。再举一个 LAST 位置的例子——支付插件在每个响应中注入版本号Componentpublic class PaymentVersionHeaderFilter implements LastFilterExtension {Overridepublic Filter getFilter() {return (req, res, chain) - {chain.doFilter(req, res);((HttpServletResponse) res).setHeader(“X-Payment-Version”, “1.0.0”);};}}7.3 三层开关Filter 在安全链上执行不能无条件开放——防重放 Filter 如果写坏了可能把所有请求都拦截掉。因此我们设计了逐层收紧的控制模型三层控制漏斗 · 安装≠启用第一层全局开关 gj.security.filter.enabled true→ 防君子默认关闭管理员明确开启第二层位置开关 gj.security.filter.allowed-positions FIRST, LAST→ 防泛滥只开放你真正需要的位置第三层插件开关 gj.security.plugins.payment.filter.allowed-positions FIRST, LAST→ 防冒用只有受信插件的 Filter 才生效支付插件的配置示例gj:security:filter:enabled: trueallowed-positions: FIRST, LAST # 只开放两个位置plugins:gj.payment.module: # 仅对此插件开放filter:allowed-positions: FIRST, LAST插件实现了 Filter 接口 ≠ Filter 一定生效。三层全通过才注入默认全部关闭。7.4 为什么 Filter 需要三层开关而 Provider 不需要从威胁模型看Filter 和 Provider 的控制粒度截然不同——它们的破坏半径决定了是否需要逐层防御Filter 横向穿透一条SecurityFilterChain上挂载所有插件的 Filter宿主无法在运行时针对单个插件的 Filter 做启停控制。一个插件的防重放 Filter 写坏了可能把所有插件甚至宿主的请求都拦截。所以必须预埋三层漏斗——在注入之前由宿主逐层收紧。Provider 纵向隔离一个 Provider 只处理对应插件的请求不跨插件。即使 Provider 出 bug宿主只需disablePlugin(id)即可止损——这是宿主本来就有的插件管理能力无需通过开关收敛。安全上这叫破坏半径驱动控制——影响的请求范围越大需要的控制粒度越细。对比认证 SPIProvider 只是填充 SecurityContext破坏半径可控无需配置即可生效。7.5 动态插入代理 Filter 模式Spring Security 的 SecurityFilterChain 在构建后不可变但插件在运行时动态加载。我们通过预埋代理 Filter 解决——6 个 PluginCompositeFilter Bean 预先注册到宿主安全链内部持有 CopyOnWriteArrayList。插件启动时 Filter 插入列表停止时从列表移除。宿主安全链结构始终不变。每个插件 Filter 的调用包裹了 try-catch——沙箱隔离。通过 nextCalled 标记区分两种异常场景boolean[] nextCalled new boolean[1];wrapped (req, res) - {try {f.doFilter(req, res, (r, s) - {nextCalled[0] true;next.doFilter(r, s);});} catch (Exception e) {log.error(“Plugin filter error — skipping: {}”, f.getClass(), e);// 前置异常next 未调用→ 跳过故障 Filter继续后续链// 后置异常next 已调用→ 吞掉避免重复执行下游if (!nextCalled[0]) {next.doFilter(req, res);}}};同时发布 PluginFilterErrorEvent宿主可监听接入告警。7.6 异常体系异常 含义 日志级别PluginAuthChallenge OAuth2/SAML 等跳转控制流非错误 DEBUGPluginBadCredentialsException 凭证无效API Key、Token、签名等客户端错误 WARNPluginAuthServiceException 外部认证不可用 ERROR八、实践与思考回到支付插件的四个接口——创建订单、查询订单、退款、回调。之前全部走宿主鉴权回调完全没法处理。现在三种认证方式宿主 Session、JWT Token、API Key 签名在同一个插件内和平共存。对插件开发者大部分接口继承 AbstractPluginAuthenticationProvider写一个 authenticate() 方法框架自动处理路由分发。特殊接口如回调走高级层写 supports() 精细控制认领范围。需要防重放实现 FirstFilterExtension宿主配置一开即生效。对宿主运维Filter 扩展点默认关闭按位置、按插件逐层开启。每个 Provider 的每次调用都有日志带耗时、带场景级别、有事件EventListener 即可接入审计告警。认证链策略可替换——覆盖一个 Bean 即可切换编排模式。框架的安全边界不再由预置能力定义。任何人写一个 IPluginAuthenticationProvider 或实现一个 Filter 扩展接口就能以和内置能力完全相同的方式接入插件的安全生命周期。