'desc', ]; public function __construct(App $app) { parent::__construct($app); $this->model = new \app\admin\model\SystemMcpKey(); $this->assign('select_list_status', $this->model::SELECT_LIST_STATUS, true); // 行内修改仅允许 status;key/key_prefix/bind_admin_id 为敏感字段,一律排除 $this->allowModifyFields = [ 'status', ]; } /** * @NodeAnotation(title="列表") */ public function index() { if ($this->request->isAjax()) { if (input('selectFields')) { return $this->selectList(); } list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames(); $count = $this->model ->where($where) ->group($group) ->count(); $list = $this->model ->where($where) ->page($page, $limit) ->order($this->sort) ->group($group) ->select(); // 附加创建者用户名:不用 withJoin(system_admin 与主表有 id/status 等同名字段会歧义), // 一次 IN 查询建立 id => username 映射后回填,避免 N+1。 $adminIds = []; foreach ($list as $vo) { !empty($vo->bind_admin_id) && $adminIds[] = $vo->bind_admin_id; } $adminNames = empty($adminIds) ? [] : SystemAdmin::whereIn('id', array_unique($adminIds))->column('username', 'id'); foreach ($list as $vo) { $vo->bind_admin_username = $adminNames[$vo->bind_admin_id] ?? ''; } $data = [ 'code' => 0, 'msg' => '', 'count' => $count, 'data' => $list, ]; return json($data); } return $this->fetch(); } /** * @NodeAnotation(title="添加") */ public function add() { if ($this->request->isPost()) { $post = $this->request->post(); // 表单仅提交 title/status/remark;密钥三要素与统计字段一律服务端生成,忽略客户端传入 unset($post['id'], $post['key'], $post['key_prefix'], $post['bind_admin_id'], $post['use_num'], $post['last_use_time']); $rule = [ 'title|密钥名称' => 'require|max:50', ]; $this->validate($post, $rule); // 明文密钥仅此一次出现在内存与响应中:不落库、不写日志 $secretKey = 'sk-mcp-' . bin2hex(random_bytes(24)); $post['key'] = hash('sha256', $secretKey); $post['key_prefix'] = substr($secretKey, 0, 16); $post['bind_admin_id'] = $this->getAdminId(); try { $save = $this->model->save($post); } catch (\Exception $e) { $this->error('保存失败:' . $e->getMessage()); } if ($save) { // data 携带明文密钥,前端弹层展示一次后即丢弃 $this->success('保存成功', ['secret_key' => $secretKey]); } $this->error('保存失败'); } return $this->fetch(); } /** * @NodeAnotation(title="编辑") */ public function edit($id) { $row = $this->model->find($id); empty($row) && $this->error('数据不存在'); if ($this->request->isPost()) { $post = $this->request->post(); // 白名单:仅允许改 title/status/remark;key/key_prefix/bind_admin_id 及统计字段不可改 $post = array_intersect_key($post, array_flip(['title', 'status', 'remark'])); $rule = [ 'title|密钥名称' => 'require|max:50', ]; $this->validate($post, $rule); try { $save = $row->save($post); } catch (\Exception $e) { $this->error('保存失败:' . $e->getMessage()); } $save ? $this->success('保存成功') : $this->error('保存失败'); } $this->assign('row', $row); return $this->fetch(); } /** * 授权页:勾选创建者当前权限的子集给密钥. * @NodeAnotation(title="授权") */ public function authorize($id) { $row = $this->model->find($id); empty($row) && $this->error('数据不存在'); // 全量动作节点来自 getNodeTree 展开全部 type=2 节点 $nodeService = new NodeService(); $module_list = $nodeService->getNodeTree(); // 创建者当前节点集合:与 checkNode 完全一致的判定链(含超管直通/auth_on/动态黑名单) $authService = new AuthService($row->bind_admin_id); $allActionNodes = []; foreach ($module_list as $module) { foreach ($module['children'] as $controller) { foreach ($controller['children'] as $action) { $allActionNodes[] = $action['node']; } } } $creatorAllowedNodes = $authService->getAdminAllowedNodes($allActionNodes, $row->bind_admin_id); // 已选节点 = 库中记录 ∩ 创建者当前集合(防漂移:创建者权限被回收后旧勾选自动失效) $checkedNodeList = (new SystemMcpKeyNode()) ->where('key_id', $id) ->column('node'); $checkedNodeList = array_values(array_intersect($checkedNodeList, $creatorAllowedNodes)); // 树过滤:动作节点不在创建者集合内整级剔除;无动作节点的控制器、无控制器的模块不显示 foreach ($module_list as $module_key => $module) { foreach ($module['children'] as $controllerKey => $controller) { foreach ($controller['children'] as $actionKey => $action) { if (!in_array($action['node'], $creatorAllowedNodes)) { unset($module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]); continue; } $checked = in_array($action['node'], $checkedNodeList); $checked_string = $checked ? 'checked' : ''; $disabled_string = ''; if (!$action['auth']) { // 免鉴权节点创建者恒有权限,照 auth/authorize 惯例固定勾选并禁用 $checked_string = 'checked'; $disabled_string = 'disabled'; } $module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]['checked'] = $checked_string; $module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]['disabled'] = $disabled_string; } if (empty($module_list[$module_key]['children'][$controllerKey]['children'])) { unset($module_list[$module_key]['children'][$controllerKey]); } } if (empty($module_list[$module_key]['children'])) { unset($module_list[$module_key]); } } $this->assign('row', $row); $this->assign('checked_node_list', $checkedNodeList); $this->assign('module_list', array_values($module_list)); return $this->fetch(); } /** * 授权保存:先删后插,服务端强校验防越权. * @NodeAnotation(title="授权保存") */ public function saveAuthorize() { $this->checkPostRequest(); $id = $this->request->post('id'); $node = (array) $this->request->post('node', []); $row = $this->model->find($id); empty($row) && $this->error('数据不存在'); // 服务端强校验:每个提交节点必须 ∈ 创建者当前节点集合(重新计算,防权限已变) $nodeService = new NodeService(); $allActionNodes = []; foreach ($nodeService->getNodeTree() as $module) { foreach ($module['children'] as $controller) { foreach ($controller['children'] as $action) { $allActionNodes[] = $action['node']; } } } $authService = new AuthService($row->bind_admin_id); $creatorAllowedNodes = $authService->getAdminAllowedNodes($allActionNodes, $row->bind_admin_id); foreach ($node as $vo) { if (!in_array($authService->parseNodeStr($vo), $creatorAllowedNodes)) { $this->error('包含无权限节点'); } } try { $keyNode = new SystemMcpKeyNode(); $keyNode->where('key_id', $id)->delete(); if (!empty($node)) { $saveAll = []; foreach ($node as $vo) { $saveAll[] = [ 'key_id' => $id, 'node' => $authService->parseNodeStr($vo), ]; } $keyNode->saveAll($saveAll); } } catch (\Exception $e) { $this->error('保存失败'); } $this->success('保存成功'); } }