开源不易,如果觉得本项目对您的工作或者学习还是有帮助的话, 请帮忙在GitHub 点个⭐️Star, 更多实现细节及完整配置参数,详见 GitHub 仓库README和Wiki。
gj-pf4j 插件化安全架构全景:左侧 Provider 链式认证引擎负责"你是谁",右侧六位 Filter 插槽负责"在什么阶段做什么",中间生命周期引擎编排两者的注册与回收,三者协同贯穿插件的安全生命周期。
gj-pf4j 插件安全架构
基于 PF4J 内核,深度整合 Spring 生态。不依赖 Spring Boot,MVC/WebFlux 双栈自适应,轻量无侵入,对中小项目极友好;原生适配达梦、人大金仓、GaussDB 等国产数据库,插件代码完全标准 Spring 注解 -可插拔扩展点允许任意第三方组件以零框架改动的方式接入插件生命周期。开放插件自定义鉴权让插件通过 SPI 自由定义认证逻辑,Provider 链式委托配合可插拔策略分发请求,六位 Filter 插槽供宿主编排任意安全过滤器——受"安装 ≠ 启用"模型统一管控。
温馨提示:全文约6100 字左右,阅读时长约15分钟。如果你正在为插件设计鉴权方案,或者好奇一个插件框架如何把"鉴权"的事比较优雅地下放给插件——这篇值得你慢慢看
一、从一个真实的支付插件说起
假设你正在开发一个微信支付插件。它有四个接口:
POST /wxpay/order/create 创建订单 → 内部员工操作,走宿主鉴权
GET /wxpay/order/{id} 查询订单 → 同上,宿主 session 即可
POST /wxpay/order/refund 申请退款 → 内部员工操作,但需要额外验 JWT Token
POST /wxpay/order/callback 支付回调 → 微信服务器调用,验 API Key 签名
四个接口,三种认证方式:宿主 Session、JWT Token、API Key 签名。回调接口尤为特殊——它是微信服务器调过来的,没有宿主 Session,不走登录流程。它只认一个东西:你拿着微信发给你的 API Key,对请求体做了正确的签名。
在之前,插件完全做不到这一点。插件的 Controller 注册到宿主 DispatcherServlet,请求全部经过同一安全链——宿主用什么鉴权方式,插件就只能被动继承。回调接口既不走宿主鉴权,又不是完全公开(不能直接标 @AllowAnonymous),卡在中间无法处理。
更进一步——即使"创建订单"走宿主 Session 就够了,"申请退款"需要额外验 JWT。同一个插件内,不同接口需要不同的认证策略。这在之前也是做不到的。
答案只有一个:让插件能定义自己的认证方式,但授权仍由宿主统一裁定。
二、快速开始
宿主引入 gj-pf4j-security 模块即可启用全部鉴权能力,插件侧依赖相同,scope 改为 provided(运行时由宿主提供)。
三、设计边界:只做鉴权,不做授权
这是我们做的第一个关键决策。我们采取集中式策略引擎——从插件各自鉴权迁移到集中化鉴权,采用集中化授权框架做统一判定。授权集中化,不下放给插件——插件只负责"你是谁"的认证环节,授权决策完全由宿主接管。
插件负责 → “你是谁”(认证 Authentication)
宿主负责 → “你能干什么”(授权 Authorization)
回到支付插件的例子:插件只管验 API Key 签名、验 JWT Token,返回 Authentication 对象。至于这个通过签名验证的"微信支付回调"有没有权限调接口——由宿主的 AuthorizationFilter 统一裁决。插件不参与授权决策。
四、认证模型:链式委托与可插拔策略
委托式 Provider 链 · 认证编排引擎
认证链是"委托鉴权"的执行内核。下图完整展示了 Provider 链从 supports() 认领、authenticate() 执行,到策略裁决、四分支结果的完整流转。
4.1 多个认证源
支付插件的四个接口需要三种认证方式。同一个请求过来,它可能携带 API Key 签名(来自微信回调),也可能携带 JWT Token(来自内部员工的退款申请),也可能只有宿主 Session(来自内部员工的订单查询)。我们的方案是认证提供者链:多个 Provider 按优先级排成链,依次认领并认证同一个请求。以支付插件为例,注册两个 Provider:
POST /wxpay/order/callback 请求到达(微信服务器)
│
▼
Provider 1 — WxPayCallbackSignatureProvider (order=100)
supports() → 请求路径是否以 /callback 结尾?
├─ 否 → 跳过,继续下一个
└─ 是 → authenticate()
├─ 从 Header 取 Wechatpay-Signature
├─ 用 API Key 对请求体做 HMAC-SHA256 签名验证
├─ 签名匹配 → 注入 SecurityContext → 放行
└─ 签名不匹配 → PluginBadCredentialsException → 401
Provider 2 — WxPayJwtProvider (order=500)
supports() → 默认 true(兜底,认领剩余所有请求)
authenticate()
├─ 从 Header 取 Authorization: Bearer
├─ 无 token → 返回 null → 回退宿主鉴权
├─ 有 token → 校验 JWT → 注入 SecurityContext
└─ token 过期 → PluginBadCredentialsException → WARN 日志,继续
注意 WxPayCallbackSignatureProvider 的 supports() 只认 /callback 结尾的请求。创建订单、查询订单、退款都不匹配,直接跳过,交给下一个 Provider。这就是 supports() 的精细认领——只在需要时接管。而 WxPayJwtProvider 作为兜底,supports() 默认 true,认领所有剩余请求。对于普通订单查询(无 JWT),authenticate() 返回 null,框架回退到宿主鉴权。对于退款操作(有 JWT),返回 Authentication 注入上下文。
4.2 链的编排策略
Provider 链的"何时继续、何时终止、最终如何判定"不是硬编码的——我们借鉴 Apache Shiro 的 ModularRealmAuthenticator 设计,将其抽象为可插拔的 AuthenticationStrategy:
public 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: principal=mch_123456
[Plugin: payment] Auth chain: 1 success, 0 failed, 3ms total → principal=mch_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 → 401
6 种场景各配 INFO/WARN/ERROR/DEBUG 级别,统一 [Plugin: {id}] 前缀,每条日志带耗时。更进一步,框架发布认证事件——PluginAuthenticationSuccessEvent 和 PluginAuthenticationFailureEvent。宿主监听即可实现告警:
@Component
public class PaymentAuthAudit {
@EventListener
public 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() 方法:
@Component
public class WxPayJwtProvider extends AbstractPluginAuthenticationProvider {
@Override
public 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) 先执行
@Component
public 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 两层共存:框架自动冲突检测