搜索 K
Appearance
Appearance
forge 的鉴权能力被切成两层:核心层 forge-auth 只定义契约与流程骨架,签发与校验令牌的真正逻辑通过 SPI 下沉到两个独立实现包 —— forge-auth-spring-security(无状态 JWT)与 forge-auth-sa-token(Sa-Token)。本文解释这套分层「为什么这么设计」:为何强制唯一实现、登录链路如何在两种实现之间保持一致、以及为什么默认管理员元数据落库要等到 ApplicationReadyEvent 而非更早的容器刷新阶段。
接入步骤与配置项请看使用指南:鉴权快速接入。
宠物商店上线初期跑在单机、用最简单的无状态 JWT 即可;等到要做多端登出、单点踢人这类有状态能力时,团队希望切到 Sa-Token,而不想动业务代码里的 @Public、登录端点和拦截器。要做到「换实现不换上层」,就必须让上层只依赖一个稳定契约。
这个契约是 AuthProvider:
@SPI
public interface AuthProvider {
String issueToken(LoginUser user); // 令牌签发
LoginUser verifyToken(String token); // 令牌校验与解析
default void invalidate(String token) {} // 令牌失效(可选)
}上层的 ForgeDefaultAuthManager 只认这三个方法,至于令牌是一段 HS256 签名的 JWT 还是 Sa-Token 的会话票据,它一概不关心。实现包通过 @SPI.Service 声明自己的名字,再由 META-INF/services 注册,启动时被发现并选中。
TIP
关于 @SPI + Spider.getAllExtensions 这套扩展点基建的通用机制,见 SPI 扩展点。这里只讲鉴权场景下的选择策略。
ForgeAuthProvider 在启动时聚合所有实现,并强制「有且仅有一个」:
public static AuthProvider get() {
List<AuthProvider> all = Spider.getAllExtensions(AuthProvider.class);
if (all.isEmpty())
throw new IllegalStateException("未检测到任何鉴权实现");
if (all.size() > 1)
throw new IllegalStateException("检测到多个鉴权实现同时存在");
AuthProvider chosen = all.get(0);
log.info("[auth] 使用鉴权实现: {}", chosen.name());
return chosen;
}这是一处刻意的 fail-fast:如果项目同时引入了 forge-auth-spring-security 与 forge-auth-sa-token,两种实现签发的令牌格式互不兼容,运行期若随机命中一个,会出现「能登录但校验失败」的诡异现象。与其让问题在生产环境以偶发形式暴露,不如在启动期直接拒绝。选中后日志会打印:
[auth] 使用鉴权实现: spring-security或
[auth] 使用鉴权实现: sa-token两个实现都标注 @SPI.Service 与 @Order(10),差别在于令牌的承载方式。
| 维度 | SpringSecurityAuthProvider | SaTokenAuthProvider |
|---|---|---|
| SPI 名 | spring-security | sa-token |
| 签发 | ForgeAuthRuntime.codec().sign(user) | StpUtil.login(userId) 后取 StpUtil.getTokenValue() |
| 校验 | ForgeAuthRuntime.codec().parse(token) | StpUtil.getLoginIdByToken(token) 后 ForgeLoginUsers.load(userId) |
| 失效 | 空实现(纯无状态,无法主动失效) | StpUtil.logoutByTokenValue(token) |
| 用户信息来源 | JWT 自定义声明 | 回源数据库重建 LoginUser |
| 主要依赖 | jjwt 0.12.6 + spring-boot-starter-security | sa-token-spring-boot3-starter 1.39.0 + sa-token-jwt |
值得注意的是两者对「校验时如何拿到用户」的不同选择:Spring Security 方案把 username、nickname、superAdmin 写进 JWT 声明里,校验即可还原,零数据库往返;Sa-Token 方案只在令牌里放 userId,校验时用 ForgeLoginUsers.load(userId) 回源数据库重建 LoginUser,代价是一次查询,换来的是不把用户属性固化进令牌、改昵称/权限可即时生效。
无论选中哪个实现,上层 ForgeDefaultAuthManager 的流程是一致的。下面以 Spring Security 实现为例画出登录与一次受保护请求的时序。
密码错误会抛 LOGIN_FAILED(40110),账号被禁用抛 USER_DISABLED(40111),二者都在签发令牌之前拦下。
未携带令牌返回 TOKEN_MISSING(40100),令牌非法或过期返回 TOKEN_INVALID(40101)。Sa-Token 实现的链路结构相同,只是 parse 一步换成「解析 userId + 回源 DB」。
Spring Security 实现没有把签名密钥散落在 Provider 里,而是抽出 ForgeJwtTokenCodec 专管编解码,再由 ForgeAuthRuntime 这个 volatile 静态 holder 持有它。原因有三:
ForgeJwtTokenCodec 在构造时用 Keys.hmacShaKeyFor 校验密钥长度,HS256 要求 ≥ 32 字节,长度不足直接 fail-fast,避免运行期才暴露弱密钥。SecurityJwtConfiguration 在启动期把 ForgeAuthProperties 喂给 ForgeAuthRuntime.configure(...),codec() 取用时做 lazy 初始化检查,未配置即抛异常,而不是悄悄用半成品。[auth] 正在使用内置默认 JWT 密钥,生产环境务必通过 forge.auth.jwt.secret 覆盖!parse 阶段对 JwtException、IllegalArgumentException 统一兜底,转换成 TOKEN_INVALID,不让底层异常类型泄漏到上层。
ForgeAuthMetaLoader 负责在启动时播种默认管理员:创建 admin/admin 账号、SUPER_ADMIN 角色,并建立两者的绑定关系。它的监听点是 ApplicationReadyEvent,而不是更早的 ContextRefreshedEvent 或 @PostConstruct:
@EventListener(ApplicationReadyEvent.class)
public void onReady(ApplicationReadyEvent event) {
long start = System.currentTimeMillis();
seedDefaultAdmin();
log.info("[auth] 默认管理员初始化完成,耗时 {}ms", System.currentTimeMillis() - start);
}核心理由是时序:播种动作要写 AuthUser、AuthRole、AuthUserRole 三张表,而这些表是由元数据驱动在启动期建出来的。若在容器刚刷新(ContextRefreshedEvent)时就执行,建表 DDL 可能尚未跑完,落库会因表不存在而失败(例如向尚未建出的角色/权限相关表写数据时直接报「表不存在」)。ApplicationReadyEvent 在整个应用 Ready 之后才发出,此时 ORM(MyBatis-Plus)已装配、建表已完成、事务管理就绪,可以安全地通过 Models.origin(AuthUser.class).save() 等方式落库。
该流程同时是幂等的:每一步都先查后写,已存在则跳过,因此重启不会重复播种。代码还有一个细节 —— 创建后会重新查询取主键,而不依赖自增主键回填,以兼容分布式主键场景。创建成功时打印:
[auth] 已创建默认管理员账号: admin
[auth] 默认管理员初始化完成,耗时 XXms| 决策点 | 选项 | 选择 | 理由 |
|---|---|---|---|
| 实现数量 | 允许多实现共存 / 强制唯一 | 强制唯一,fail-fast | 不同实现令牌互不兼容,多实现会导致随机命中、偶发校验失败;启动期拒绝优于生产期暴露 |
| 令牌承载用户信息 | 全写进令牌 / 只放 userId 回源 | 两实现各取一种 | Spring 方案零 DB 往返但属性被固化;Sa-Token 方案多一次查询但改昵称/权限即时生效 |
| 主动失效令牌 | 统一要求 / 由实现决定 | 接口提供默认空实现 | 纯无状态 JWT 无法主动失效,强行要求会迫使其引入黑名单;交给有状态实现自行支持 |
| JWT 密钥管理位置 | 散落在 Provider / 抽独立 codec | 下沉到 ForgeJwtTokenCodec + ForgeAuthRuntime | 密钥长度校验、TTL、默认值告警集中一处,配置注入与读写解耦 |
| 默认管理员落库时机 | @PostConstruct / ContextRefreshedEvent / ApplicationReadyEvent | ApplicationReadyEvent | 晚于建表 DDL 与 ORM 装配,避免向尚未建出的表写数据而报表不存在 |
| 播种幂等 | 直接插入 / 先查后写 | 先查后写并重查取主键 | 支持重启不重复,且不依赖自增主键回填,兼容分布式主键 |
WARNING
强制唯一实现意味着不能为了过渡而临时双引。若需从 Spring Security 切换到 Sa-Token,应移除旧实现包后再引入新包,并要求所有用户重新登录 —— 旧令牌在新实现下无法解析。
签发链路的可复制代码、application.yml 配置键与启动日志验证,见使用指南 鉴权快速接入。