OrgPermission 权限注解实现与权限控制说明
2026/3/11大约 5 分钟
OrgPermission 权限注解实现与权限控制说明
一、权限注解定义
位置:tgkw-adc/helper 包 → vendor/tgkw-adc/helper/src/Annotation/OrgPermission.php
核心字段:
| 字段 | 说明 |
|---|---|
parentAccessCode | 父菜单权限码,空表示顶级 |
accessCode | 权限唯一标识(如 corporate-admin.base-info:overview) |
module | 菜单层级(如 基本信息:企业概览) |
i18nName | 多语言名称 |
type | MENU / BUTTON |
sort | 排序 |
frontRouteAlias | 前端路由别名,前端路由匹配用 |
grantedByAccessCode | 派生权限来源:拥有列表中任一 accessCode 权限,即自动拥有本接口权限 |
syncToMenu (bool) | 是否将该注解同步为 menus 记录;为 false 时仅参与校验、不进菜单树 |
使用方式:支持类级 + 方法级,方法级覆盖类级;#[OrgPermission(...)] 标注在 Controller 类或方法上。
二、注解扫描与菜单同步
┌─────────────────────────────────────────────────────────────────┐
│ 微服务 │
│ OrgPermissionHelper::build() → MainWorkerStartListener │
└─────────────────────────────┬───────────────────────────────────┘
│ Nacos needAddMenuSrv 包含本服务
▼
┌─────────────────────────────────────────────────────────────────┐
│ adc-user │
│ UserService::addMenu (RPC) → MenuService::addMenu → menus │
└─────────────────────────────────────────────────────────────────┘OrgPermissionHelper::build()(vendor/tgkw-adc/helper/src/Helper/OrgPermissionHelper.php):
- 通过
AnnotationCollector::getClassesByAnnotation(OrgPermission::class)收集类级注解 - 通过
AnnotationCollector::getMethodsByAnnotation(OrgPermission::class)收集方法级注解 - 输出结构:
['micro' => APP_NAME, 'annotations' => [...], 'version' => time()] - 每项包含
action(格式:Controller完整类名@方法名)与annotation(注解对象)
同步自动:
各服务启动时 MainWorkerStartListener 调用 UserService::addMenu() 同步
MenuService::addMenu()(app/Service/MenuService.php):
- 先过滤:
syncToMenu === false的注解不会写入menus(只做运行时权限校验) - 按
parentAccessCode建立父子菜单树(先处理顶级,再逐层处理子项) - 将注解转为菜单:
access_code = micro + ':' + accessCode,front_route_alias = micro + '-' + frontRouteAlias action存为Controller@method(如App\Controller\V1\TenantSystem\BaseInfo\TenantController@overview)
三、OrgMiddleware 权限控制逻辑
位置:vendor/tgkw-adc/helper/src/Middleware/OrgMiddleware.php
请求进入 → 取 Token
├─ 无 → 401 需登录
└─ 有 → Redis 取 payload / 离线 JWT
→ 白名单? → 是 → 放行
→ 否 → 有租户? → 否 → 403 需加入/选择租户
→ 是 → 超级管理员? → 是 → 放行
→ 否 → 有 OrgPermission 注解?
├─ 否 → 放行
└─ 是 → RPC UserService::checkAccessPermission
├─ Enforcer::enforce 通过 → 放行
└─ 不通过 → 403 无权限主要步骤:
- 认证:从请求取 Org Token,Redis 或 JWT 解析出用户信息,写入
Context - 白名单:
logout、refreshToken、switchTenant等不校验权限 - 租户校验:必须有
current_tenant_id,否则 403 - 超级管理员:
is_current_tenant_main_admin === true直接放行 - 注解存在性:
AnnotationCollector::getClassMethodAnnotation取当前Controller@method的OrgPermission - 权限校验:
- 存在注解则调用
UserService::checkAccessPermission($params),$params结构为:['user:{$userId}', 'tenant:{$tenantId}', 'Controller@method', ['grantedByAccessCodes' => [...], 'micro' => APP_NAME]]
UserService会先校验当前action,再根据grantedByAccessCode到menus表解析出其他action并依次校验
- 存在注解则调用
- 无注解:不校验,直接放行(需注意安全)
四、示例:TenantController 与 EmployeeController 派生权限
// 类级:定义「基本信息」模块
#[OrgPermission(
module: '基本信息',
parentAccessCode: '',
accessCode: 'corporate-admin:base-info',
frontRouteAlias: 'corporate-admin.base-info',
)]
// 方法级:定义「企业概览」子菜单
#[OrgPermission(
module: '基本信息:企业概览',
parentAccessCode: 'corporate-admin:base-info',
accessCode: 'corporate-admin.base-info:overview',
frontRouteAlias: 'corporate-admin.base-info.overview',
)]
#[GetMapping(path: 'tenant/base-info/overview')]
public function overview() { ... }overview的action=App\Controller\V1\TenantSystem\BaseInfo\TenantController@overview- 权限校验时用该
action查 Casbin - 角色需被授予对应菜单(含此 action)才会通过
EmployeeController orgs(派生 + 不进菜单):
#[OrgPermission(
module: '通讯录:员工管理:组织选择',
i18nName: ['en' => 'Organization Select', 'zh_hk' => '組織選擇'],
sort: 1050,
parentAccessCode: 'corporate-admin:contacts:employee',
accessCode: 'corporate-admin:contacts:employee:orgs',
type: 'BUTTON',
frontRouteAlias: 'corporate-admin.contacts.employee.orgs',
syncToMenu: false,
grantedByAccessCode: [
'corporate-admin:contacts:employee:detail',
'corporate-admin:contacts:employee:add',
],
)]
#[GetMapping(path: 'contacts/employees/orgs')]
public function orgs() { ... }- orgs 接口 受控权:有注解 + OrgMiddleware + UserService 校验
- orgs 不出现在菜单 / 权限组配置界面:
syncToMenu: false - 只要角色拥有详情或新增按钮(即对应 accessCode)的权限,即可自动访问 orgs,而不需要单独配置 orgs
五、注意事项
- 派生权限优先:通过
grantedByAccessCode派生的权限只影响运行时校验,不会在菜单树中额外冗余节点(配合syncToMenu: false) - 无注解接口:未加
OrgPermission的接口在 OrgMiddleware 下会直接放行
六、新接口接入权限的开发 Checklist
以「普通页面 + 按钮」为例,新人可以按下面步骤接入:
- 设计 accessCode 与路由别名
- 约定:
micro:一级模块:二级模块:动作,例如:corporate-admin:contacts:employee:export - 与前端约定
frontRouteAlias,例如:corporate-admin.contacts.employee.export
- 约定:
- 在 Controller 上加
OrgPermission- 列表页/页面级接口:
type默认MENU - 按钮/操作:
type: 'BUTTON',parentAccessCode指向所属页面的 accessCode
- 列表页/页面级接口:
- (可选)设计派生权限
- 如果只是被其他权限「顺带」覆盖的接口(如 orgs 下拉、某些统计接口),使用:
syncToMenu: falsegrantedByAccessCode: ['existing:xxx:detail', 'existing:xxx:add']
- 如果只是被其他权限「顺带」覆盖的接口(如 orgs 下拉、某些统计接口),使用:
- 同步菜单
- 确认 Nacos
systemConfig.needAddMenuSrv中包含当前微服务名,重启服务自动同步;或在工具服务中执行相应* :sync-menu命令
- 确认 Nacos
- 在权限中心配置权限组/角色
- 通过 adc-user 后台给角色勾选对应菜单/按钮(无需勾选
syncToMenu: false的接口)
- 通过 adc-user 后台给角色勾选对应菜单/按钮(无需勾选
- 自测
- 使用只挂有目标角色的测试账号登录前端
- 验证:菜单显示是否正确、按钮是否可见、接口是否 200 / 被 403 拦截符合预期
七、grantedByAccessCode 使用示例(含跨微服务)
1. 同微服务(短格式)
// 当前服务 micro = 'crm'
#[OrgPermission(
module: 'CRM:线索:导出接口',
parentAccessCode: 'crm:lead:list',
accessCode: 'crm:lead:export-api',
type: 'BUTTON',
syncToMenu: false,
grantedByAccessCode: [
'crm:lead:export', // 拥有列表页导出按钮权限的用户,自动拥有本接口权限
],
)]- 运行时会自动补全为
crm:crm:lead:export这样的完整 accessCode,并从menus表解析对应action
2. 跨微服务(完整格式)
// 在 hr 服务中引用 user 服务的某个权限
#[OrgPermission(
module: '考勤:报表:员工维度报表',
parentAccessCode: 'hr:attendance:report',
accessCode: 'hr:attendance:employee-report',
type: 'BUTTON',
syncToMenu: false,
grantedByAccessCode: [
'user:corporate-admin:contacts:employee:detail',
],
)]UserService::checkAccessPermission中会识别首段user是已知微服务名,直接将其作为完整access_code查询- 适用于「某服务的权限由另一个中心服务统一承载」的场景
八、常见问题 / 排查思路
- 接口总是 403
- 检查是否真的加了
OrgPermission注解(类级/方法级) - 检查角色是否勾选了对应菜单/按钮,或是否在
grantedByAccessCode列表中 - 查看
menus表中是否存在对应access_code与action
- 检查是否真的加了
- 菜单不见了 / 变少了
- 检查代码是否给某些注解加了
syncToMenu: false - 确认是否刚调整了
accessCode导致版本同步删除了旧菜单
- 检查代码是否给某些注解加了
- 新增权限在前端看不到
- 是否执行过菜单同步(服务重启 +
needAddMenuSrv,或手动执行* :sync-menu命令) - 是否给测试账号绑定了包含该菜单/按钮的角色
- 是否执行过菜单同步(服务重启 +
贡献者
ou.阳