后端 OrgPermission 权限注解实现与权限控制说明
后端 OrgPermission 权限注解实现与权限控制说明
目录
强制约定
菜单/按钮权限用完整
OrgPermission注解同步进menus;仅作接口鉴权、不进菜单的接口,使用syncToMenu: false+grantedByAccessCode成对配置。
类级 + 方法级均可标注;方法级覆盖类级语义(方法上单独声明自己的注解)。
需要出现在菜单树 / 权限配置界面的节点:
syncToMenu保持默认true,并正确配置accessCode、parentAccessCode、module、frontRouteAlias等。仅运行时鉴权、不进菜单:必须成对使用
syncToMenu: false与grantedByAccessCode(见下一节);列配置/下拉等附属接口可挂父级,需精确到按钮时挂叶子(见下一节)。无
OrgPermission注解的接口在OrgMiddleware下不校验权限、直接放行(需谨慎)。
注解定义:基础包 TgkwAdc\Annotation\OrgPermission。
菜单同步与鉴权实现:以 user 服务(MenuService::addMenu、UserService::checkAccessPermission)为准。
注解字段
| 字段 | 说明 |
|---|---|
parentAccessCode | 父菜单权限码(注解内短码,不含当前微服务前缀);空表示顶级 |
accessCode | 权限唯一标识(短码);入库后为 {micro}:{accessCode} |
module | 菜单层级文案(如 权限设置:角色管理:列表),最后一段作菜单名 |
i18nName | 多语言名称 |
type | MENU / BUTTON |
sort | 排序 |
frontRouteAlias | 前端路由别名;入库后为 {micro}.{frontRouteAlias} |
url / urlType / redirect / keepAlive | 前端路由相关 |
status / isEnable / showMobile | 显示、启用、移动端 |
needAuth | 是否需要权限校验;0 时中间件直接放行 |
method / app / micro / appId | 请求方法、微前端/微服务、应用 ID 等 |
syncToMenu | 是否同步到 menus;默认 true;false 时不进菜单树 |
grantedByAccessCode | 派生权限来源(短码数组):拥有其中任一 accessCode 对应菜单权限,即视为拥有本接口权限 |
使用方式:#[OrgPermission(...)] 标注在 Controller 类或方法上。
派生权限成对配置(重要)
grantedByAccessCode与syncToMenu: false必须成对使用。
配置这一对时,本质上只需这两项;module/accessCode/parentAccessCode/frontRouteAlias/type/sort/i18nName等无需配置,配置了也不生效(不会写入menus,运行时鉴权也不依赖这些字段)。
适用场景:列配置、下拉组织、附属查询等「被某个已有菜单/按钮权限顺带覆盖」的接口。
| 配置 | 作用 |
|---|---|
syncToMenu: false | MenuService::addMenu 过滤掉该注解,不写入 menus,权限树不可单独勾选 |
grantedByAccessCode: [...] | 运行时:当前 action 无直接权限时,按列表中的短码解析已有菜单的 action,任一 Enforcer::enforce 通过即放行 |
推荐写法(与 user 服务一致):
#[OrgPermission(
syncToMenu: false,
grantedByAccessCode: [
'corporate-admin:permission-setting:role:list',
],
)]
#[GetMapping(path: 'roles/columns')]
public function columns() { ... }或更短:
#[OrgPermission(syncToMenu: false, grantedByAccessCode: ['corporate-admin.contacts.organization'])]
#[GetMapping(path: 'contacts/organizations/columns')]
public function columns() { ... }grantedByAccessCode 填注解里的短码(与被依赖菜单的 accessCode 一致,不要再手写当前服务的 micro: 前缀)。校验时 user 服务会自动拼成 {当前请求服务micro}:{短码} 去查 menus.access_code。
挂「有子集」权限码:适用场景与风险(不禁止)
grantedByAccessCode可以配置「仍有子权限」的节点(如一级/二级目录、带按钮子集的页面权限)。
列配置、下拉列表等「需做权限控制、但非单点精细鉴权」的附属接口,可以挂父级菜单;写操作或需精确到按钮的接口,仍优先挂叶子。
当前权限体系约定:对带子集的权限节点,只要用户拥有其任意一个子集权限,即视为自动拥有该父级权限。
适合挂父级(附属读接口,只需「进过该模块任一权限」即可):
- 列配置(
columns)、筛选元数据 - 下拉 / 选项列表(组织、字典、枚举等)
- 其它被页面顺带调用、不单独授权的查询
// ✅ 适用:列配置 / 下拉等附属接口,挂页面父级即可
// 拥有该页面下任意按钮权限的用户都能访问
#[OrgPermission(
syncToMenu: false,
grantedByAccessCode: ['corporate-admin:permission-setting:role'],
)]
#[GetMapping(path: 'roles/columns')]
public function columns() { ... }风险:挂父级后,凡拥有该父级下任意子权限的用户都会放行——权限面大于「仅列表可访问」。对列配置、下拉这类场景通常可接受;若接口本身敏感或需与某一按钮严格对齐,则不要挂父级。
需精确控制时:挂叶子权限(通常是具体 BUTTON),多入口时显式列出多个短码:
// ✅ 精确:仅拥有列出的按钮权限才可访问
grantedByAccessCode: [
'corporate-admin:permission-setting:role:list',
'corporate-admin:permission-setting:role:add-employees',
]注解扫描与菜单同步
各微服务启动
→ OrgPermissionHelper::build()
→ MainWorkerStartListener
→(Nacos needAddMenuSrv 含本服务)
→ RPC UserService::addMenu
→ MenuService::addMenu → menus 表OrgPermissionHelper::build()(基础包):
- 收集类级、方法级
OrgPermission - 输出:
['micro' => APP_NAME, 'annotations' => [...], 'version' => time()] - 每项含
action:Controller完整类名@方法名(类级为Class@)
MenuService::addMenu()(user 服务):
- 服务级同步锁 + 版本校验(过期 version 跳过;空快照保护等)
- 过滤
syncToMenu === false的注解(只参与运行时校验,不落库) - 先写
parentAccessCode为空的顶级,再按父节点已存在逐层写子菜单 - 字段转换要点:
access_code={micro}:{accessCode}action={micro}:{Controller@method}(避免跨服务冲突)front_route_alias={micro}.{frontRouteAlias}
- 本服务旧 version 菜单二次确认后删除,并清理对应 Casbin 策略
OrgMiddleware 权限控制
位置:基础包 TgkwAdc\Middleware\OrgMiddleware
请求进入 → 取 Org Token
├─ 无 / Redis 无效 → 401
└─ 有 → 写入 Context
→ 白名单? → 放行
→ 无租户 / 未选租户 → 403
→ 当前租户主管理员? → 放行
→ 取方法上 OrgPermission
├─ 无注解 → 放行
├─ needAuth === 0 → 放行
└─ 有注解 → RPC checkAccessPermission
├─ 通过 → 放行
└─ 否 → 403要点:
- 鉴权
act为:{APP_NAME}:{Controller}@{method} - 若存在
grantedByAccessCode,一并传入 options:grantedByAccessCodes+micro - user 服务内:先对当前
actenforce;失败再按派生 accessCode 查菜单action依次 enforce
adc-user 校验逻辑
UserService::checkAccessPermission:
param = [sub, obj, act, options]
sub = user:{userId}
obj = tenant:{tenantId}
act = {micro}:{Controller@method}
若 options.grantedByAccessCodes 非空:
短码 → 拼 {micro}:{短码} → 查 menus.access_code → 得到候选 action 列表
hasAccess = Enforcer::enforce(sub, obj, act)
若失败且有候选 action:任一 enforce 成功则 hasAccess = true
返回 ['hasAccess' => bool]因此派生权限依赖:被引用的 accessCode 必须已作为正常菜单同步进 menus,且角色已授予对应菜单/按钮。
样例代码
1. 普通菜单 + 按钮(需同步)
#[OrgPermission(
module: '权限设置:角色管理',
i18nName: ['en' => 'Role Management', 'zh_hk' => '角色管理'],
sort: 1100,
parentAccessCode: 'corporate-admin:permission-setting',
accessCode: 'corporate-admin:permission-setting:role',
redirect: 'corporate-admin.permission-setting.role.list',
frontRouteAlias: 'corporate-admin.permission-setting.role.list',
)]
#[Controller('v1')]
class RoleController
{
#[OrgPermission(
module: '权限设置:角色管理:列表',
type: 'BUTTON',
sort: 1100,
parentAccessCode: 'corporate-admin:permission-setting:role',
accessCode: 'corporate-admin:permission-setting:role:list',
)]
#[GetMapping(path: 'roles')]
public function index() { ... }
}2. 派生接口(仅成对两项)
// 拥有「角色列表」按钮权限即可访问 columns;不进菜单树
#[OrgPermission(
syncToMenu: false,
grantedByAccessCode: [
'corporate-admin:permission-setting:role:list',
],
)]
#[GetMapping(path: 'roles/columns')]
public function columns() { ... }// 组织相关附属接口挂在组织管理权限下
#[OrgPermission(syncToMenu: false, grantedByAccessCode: ['corporate-admin.contacts.organization'])]
#[GetMapping(path: 'contacts/organizations/columns')]
public function columns() { ... }新接口接入 Checklist
- 是否需要单独出现在权限树?
- 是 → 完整配置
accessCode/parentAccessCode/module等,syncToMenu默认 true - 否 → 只配
syncToMenu: false+grantedByAccessCode,不要堆其他无效字段
- 是 → 完整配置
- 设计短码:与前端约定
accessCode、frontRouteAlias(菜单节点);派生接口:列配置/下拉等可挂父级,需精确控制时填叶子accessCode - 确认 Nacos
systemConfig.needAddMenuSrv含当前微服务;重启或执行菜单同步 - 权限中心:给角色勾选真实菜单/按钮(无需勾选
syncToMenu: false的接口) - 自测:菜单是否显示、按钮是否可见、派生接口 200 / 403 是否符合预期
FAQ
为什么派生场景配置了 accessCode 也不生效?
syncToMenu: false 时该注解不会写入 menus。运行时中间件用的是 Controller@method + grantedByAccessCode,不会用你多写的那些菜单字段。
grantedByAccessCode 可以填父级菜单吗?
可以。列配置、下拉列表等需鉴权但非单点精细控制的附属接口,挂父级菜单很合适(拥有该模块下任意子权限即可访问)。带子集时用户只要有任意子集即视为拥有该父级,权限面会放大;写操作或需对齐某一按钮时,仍应显式列叶子短码。
grantedByAccessCode 要不要带 user: 前缀?
填短码即可(与被依赖注解的 accessCode 一致)。user 服务校验时会用当前请求服务的 micro 拼接。跨服务引用其它微服务菜单时,需保证拼出的 {micro}:{短码} 在 menus 中真实存在;常规做法是挂靠本服务已同步的菜单短码。
接口总是 403?
- 是否加了
OrgPermission - 角色是否拥有对应菜单/按钮,或是否命中
grantedByAccessCode menus中是否有对应access_code/action(派生源菜单必须存在)- 当前用户是否主管理员(主管理员会直接放行)
菜单少了 / 不见了?
- 是否误加
syncToMenu: false - 是否改了
accessCode导致旧 version 菜单被清理 - 是否完成菜单同步(服务在
needAddMenuSrv内并已重启)
needAuth: 0 是什么?
有注解但不做权限校验(公共接口)。与「无注解直接放行」不同:仍可同步菜单(若 syncToMenu: true),只是运行时跳过 enforce。