搜索 K
Appearance
Appearance
这一篇承接 登录与鉴权接入,从「谁能登录」走到「谁能做什么」。我们仍然用宠物商店后台做例子:登录进来的运营人员,并不是都能上下架宠物、调价、删除档案,需要按角色把动作分给不同的人。
先说清楚当前能力边界
forge-auth 目前已经落地的是:用户、角色、用户-角色关联三张表,以及超级管理员自举与免登录放行(@Public)。按钮级(动作级)授权尚在建设中,拦截器里留有明确标记:
// ForgeAuthInterceptor
// fixme ysy 当前仅登录认证,按钮级授权待补因此本篇分两部分读:前半部分(角色模型、超级管理员、@Public)是现在就能跑的;后半部分(@Action 动作声明、权限码、角色-权限-菜单)是约定中的协议形态,给出的是建模思路与命名约定,等实现合入后即可直接对齐,请勿当作已可运行的 API。
RBAC 的「R」就是角色。forge-auth 用三张标准表承载,与具体鉴权实现(Spring Security / Sa-Token)解耦:
| 模型 | 关键字段 | 说明 |
|---|---|---|
AuthUser | username、password、nickname、status、superAdmin | 用户;username 唯一索引,password 走 BCrypt |
AuthRole | code、name、remark、status | 角色;继承 CodeModel,由 code 保证唯一 |
AuthUserRole | userId、roleId | 用户-角色多对多关联;(userId, roleId) 唯一复合索引 |
宠物商店后台可以这样规划角色:
| 角色 code | 角色名称 | 期望能做的事 |
|---|---|---|
SUPER_ADMIN | 超级管理员 | 一切(框架内置) |
SHOP_MANAGER | 店长 | 宠物增删改、上下架、调价 |
SHOP_CLERK | 店员 | 只看宠物列表,不能上下架 |
角色编码就是稳定标识
AuthRole 继承 CodeModel,业务里请用 code(如 SHOP_MANAGER)做引用,不要用自增 id。code 唯一且语义稳定,跨环境迁移、数据导入都不会错位。
应用启动后,ForgeAuthMetaLoader 会监听 ApplicationReadyEvent,自动播种一个 admin/admin 账号,建好 SUPER_ADMIN 角色并绑定。这一步是幂等的——已存在就跳过,重启不会重复创建。
forge:
auth:
enabled: true
defaultAdminUsername: admin
defaultAdminPassword: admin # 生产务必改掉打开调试日志,能看到自举过程:
logging:
level:
cn.cvking.forge.auth: debug启动控制台代表性输出:
[auth] 已创建默认管理员账号: admin
[auth] 默认管理员初始化完成,耗时 38ms自举要写库,必须等 ORM、事务、数据源全部就绪。ApplicationReadyEvent 是应用完全 Ready 之后才发的,此时 Models.origin(AuthUser.class).save() 这类调用才安全。更深入的时序取舍见设计篇 鉴权模块设计 · 启动时序。
superAdmin = true 的账号会绕过后续所有权限检查。后台里给「老板」一个超管账号,给一线运营按角色细分,是推荐的划分方式。
@Public:把不需要登录的接口放出去 凡是不挂 @Public 的 /v1/**、/v2/** 接口,默认都要带令牌。宠物商店的「门店公开信息」「在售宠物展示」这类对游客开放的接口,用 @Public 标注即可免登录:
@RestController
@RequestMapping("/v1/api/pet")
public class PetPublicController {
@Operation(summary = "在售宠物列表(游客可见)")
@Public
@GetMapping("/on-sale")
public List<Pet> listOnSale() {
// 无需登录即可访问
return petService.listByStatus(PetStatus.ON_SALE);
}
}@Public 同时支持类级与方法级;拦截器在检查令牌之前就会先看这个注解,命中即放行,优先于配置白名单。白名单的三层聚合(硬编码 + forge.auth.white-list 配置 + SPI 贡献)详见 登录与鉴权接入 · 白名单。
带上调试日志,访问公开接口与受保护接口,行为差异很明显:
# 公开接口:直接通
curl http://localhost:8080/v1/api/pet/on-sale
# 受保护接口未带令牌:被拦
curl http://localhost:8080/v1/api/pet未带令牌访问受保护接口,返回错误码 40100(TOKEN_MISSING,未携带令牌,请先登录);令牌过期或非法则是 40101(TOKEN_INVALID)。完整错误码见 接入篇。
以下为协议设计,尚未在代码中实现
本节描述的是 forge-auth 动作级授权的目标形态,用于让你提前按统一约定组织 Controller。@Action、权限码扫描落库、角色-权限-菜单绑定等具体 API 合入后会与此对齐。当前版本请仍依赖角色 + 超管 + @Public 控制访问。
@Action 声明一个动作 后台的每个「能点的按钮 / 能调的接口」就是一个动作。约定在 Controller 方法上用 @Action 声明它的展示名,让动作能被自动发现、收集、落库:
@RestController
@RequestMapping("/v1/api/pet")
public class PetAdminController {
@Action(displayName = "上架宠物")
@PostMapping("/{id}/on-sale")
public void onSale(@PathVariable Long id) {
petService.updateStatus(id, PetStatus.ON_SALE);
}
@Action(displayName = "下架宠物")
@PostMapping("/{id}/off-shelf")
public void offShelf(@PathVariable Long id) {
petService.updateStatus(id, PetStatus.OFF_SHELF);
}
@Action(displayName = "调整售价")
@PostMapping("/{id}/price")
public void changePrice(@PathVariable Long id, @RequestBody BigDecimal price) {
petService.changePrice(id, price);
}
}每个动作需要一个全局唯一的标识,约定直接用「全限定类名 + # + 方法名」作为权限码。这样的好处是:天然唯一、不必人工编码、随重构自动变化、易于在日志里定位。
上面三个动作对应的权限码就是:
| 动作 | 权限码 |
|---|---|
| 上架宠物 | com.demo.petshop.controller.PetAdminController#onSale |
| 下架宠物 | com.demo.petshop.controller.PetAdminController#offShelf |
| 调整售价 | com.demo.petshop.controller.PetAdminController#changePrice |
为什么不让人手写权限码?
人写的权限码(如 pet:on-sale)容易撞名、容易拼错、改方法名后对不上。用「类名#方法名」交给框架扫描生成,唯一性由 JVM 类型系统兜底,开发者只需关心 @Action 的展示名。
完整的授权链路约定是这样的:
@Action)→ 产出权限码AuthRole)勾选若干菜单 / 权限码AuthUserRole 拿到角色,从而拥有这些权限码40300(FORBIDDEN)宠物商店的落地示意:
| 角色 | 拥有的动作权限码 |
|---|---|
SHOP_MANAGER | 上架、下架、调整售价(三个 PetAdminController#*) |
SHOP_CLERK | 仅宠物列表查询 |
超级管理员(superAdmin = true)不参与这套比对,直接全通。
动作和权限码不该靠人工维护,约定在应用启动时由框架完成「扫描 → 落库」:扫描所有挂了 @Action 的 Controller 方法,按「类名#方法名」生成权限码与展示名,写入权限表,供角色配置时勾选。这与 ForgeAuthMetaLoader 在 ApplicationReadyEvent 自举管理员是同一类启动期数据准备动作。包扫描与启动钩子的通用机制见 前置知识 · 包扫描与启动钩子。
想理解为什么把权限码定为「类名#方法名」、为什么扫描放在启动期、与角色模型如何解耦,请读设计篇 鉴权模块设计。
反例 1:手写权限码不唯一,两个动作互相顶替
现象:给「上架」和「批量上架」都手写了 pet:on-sale,配置角色时勾一个动作另一个也跟着生效。 原因:人工编码无法保证唯一,语义相近时极易撞名。 修复:不要手写权限码,遵循「全限定类名#方法名」由框架扫描生成;展示名靠 @Action(displayName = ...) 区分,标识靠类名#方法名兜底唯一。
反例 2:动作没被扫描,配角色时找不到这一项
现象:新写的「调整售价」接口,在角色配置界面里搜不到、勾不上。 原因:方法上漏标 @Action,或该 Controller 不在扫描包路径内,启动期没被收集落库。 修复:确认方法标了 @Action;确认 Controller 所在包在应用扫描范围内;重启观察启动日志中动作落库是否包含该项。包扫描范围见 前置知识。
反例 3:受保护接口忘标 @Public,游客页 401
现象:本应对游客开放的「在售宠物」接口,未登录访问报 40100(TOKEN_MISSING)。 原因:/v1/**、/v2/** 默认全部需要令牌,该接口既没加 @Public,也不在白名单里。 修复:给方法或类加 @Public,或在 forge.auth.white-list 配置其路径;二者命中其一即放行。
反例 4:默认管理员密码没改就上了生产
现象:生产环境用 admin/admin 仍能登录超管。 原因:defaultAdminPassword 沿用了默认值 admin。 修复:上线前通过 forge.auth.defaultAdminPassword 覆盖默认口令;JWT 方案还须用 forge.auth.jwt.secret 覆盖内置默认密钥(启动会打印 warn 提醒)。