Skip to content

Latest commit

 

History

History
407 lines (319 loc) · 12.8 KB

File metadata and controls

407 lines (319 loc) · 12.8 KB

权限系统接入指南

架构概览

                     ┌─────────────────────┐
                     │   OAuth 权限系统      │
                     │  (snapmaker-admin-   │
                     │   oauth)             │
                     │                     │
                     │  system_menu 表      │  ← 权限码集中管理
                     │  system_role_menu    │  ← 角色-菜单关联
                     │  /admin-api/system/  │
                     │    auth/get-         │
                     │    permission-info   │  ← 返回 { permissions, roles, menus }
                     └────────┬────────────┘
                              │
          ┌───────────────────┼───────────────────┐
          │                   │                   │
          ▼                   ▼                   ▼
   ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
   │  前端 SPA     │   │  Java 后端    │   │  Node 后端    │
   │  (Umi/React) │   │  (Spring)    │   │  (Express/   │
   │              │   │              │   │   Koa)       │
   │ 侧边栏 + 按钮 │   │ Controller   │   │ 中间件校验    │
   │ 显隐控制      │   │ @PreAuthorize│   │ auth.require │
   └──────────────┘   └──────────────┘   └──────────────┘

权限码统一在 OAuth 系统的 system_menu 表中管理。前端控制 UI 显隐,后端做最终安全保障。前后端的权限判断基于同一份 { permissions, roles } 数据。


一、前置条件:注册权限码

在 OAuth 系统的 system_menu 表中写入权限码,推荐通过 admin-ui 的菜单管理页面操作,也可直接 INSERT:

-- 目录
INSERT INTO system_menu(name, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show)
VALUES ('模型管理', 1, 0, 0, '/models', 'appstore', 0, true, true, true);

-- 页面菜单
INSERT INTO system_menu(name, type, sort, parent_id, path, component, status, visible, keep_alive, always_show)
VALUES ('全部模型', 2, 0, <上面拿到的id>, '/models', './models/index', 0, true, true, true);

-- 按钮权限
INSERT INTO system_menu(name, permission, type, sort, parent_id, status, visible, keep_alive, always_show)
VALUES
  ('模型查询',   'models:manage:query',   3, 1, <parent_id>, 0, false, false, false),
  ('模型创建',   'models:manage:create',   3, 2, <parent_id>, 0, false, false, false),
  ('模型删除',   'models:manage:delete',   3, 3, <parent_id>, 0, false, false, false);

然后给角色勾选这些菜单,该角色的用户重新登录后 permissions 数组里就会包含这些权限码。

权限码命名规范{模块}:{资源}:{操作}

操作 示例
查询 models:manage:query
新增 models:manage:create
修改 models:manage:update
删除 models:manage:delete
审核 models:manage:review
上下架 models:manage:offline

二、前端接入(UmiJS / React)

2.1 安装

{
  "dependencies": {
    "@snapmaker/admin-auth": "workspace:*"
  }
}

2.2 配置(.umirc.ts)

import { defineConfig } from '@umijs/max';
import { createPermissionRequest, getAccessToken, setTokens, removeTokens, attemptRefresh, getRefreshState } from '@snapmaker/admin-auth';
import routes from './config/routes';

export default defineConfig({
  routes,
  access: {},
  initialState: {},
  request: createPermissionRequest({
    getAccessToken, setTokens, removeTokens,
    attemptRefresh, getRefreshState,
    loginPagePath: '/login',
    apiPrefix: '/api',
  }),
  proxy: {
    '/admin-api': { target: 'https://pre.api.snapmaker.com', changeOrigin: true },
    '/api':       { target: 'http://localhost:3100', changeOrigin: true },
  },
});

2.3 初始化状态(src/app.tsx)

import { initAuthConfig, getAccessToken, getPermissionInfo, convertMenus, removeTokens } from '@snapmaker/admin-auth';
import type { CurrentUser } from '@/shared/user';

initAuthConfig({ baseUrl: '' }); // 空 = 走代理

let cachedMenuData: any[] = [];

export async function getInitialState() {
  const token = getAccessToken();
  if (!token) { history.push('/login'); return {}; }

  try {
    const info = await getPermissionInfo();
    cachedMenuData = convertMenus(info.menus ?? []);
    return {
      currentUser: {
        id: String(info.user.id),
        email: info.user.email,
        nickname: info.user.nickname,
        avatar: info.user.avatar,
        role_codes: info.roles,
        permissions: info.permissions,      // ← 权限码数组
      },
      menuData: cachedMenuData,             // ← 侧边栏菜单
    };
  } catch {
    removeTokens();
    history.push('/login');
    return {};
  }
}

export const layout = () => ({
  title: '管理平台',
  layout: 'mix',
  menu: { data: cachedMenuData },
  onPageChange: () => { if (!getAccessToken()) history.push('/login'); },
});

2.4 路由守卫(config/routes.ts + src/access.ts)

// src/access.ts
import { createAccess } from '@snapmaker/admin-auth';

export default function access(initialState: any) {
  const a = createAccess(initialState?.currentUser);
  return {
    canAccess: a.canAccess,
    isSuperAdmin: a.isSuperAdmin,
    hasPermission: a.hasPermission,
    hasAnyPermission: a.hasAnyPermission,
    // 菜单级路由
    hasMenu_models: a.hasAnyPermission('models:manage:query'),
  };
}
// config/routes.ts
export default [
  { path: '/models', icon: 'appstore', component: './models', access: 'hasMenu_models' },
  { path: '/login', component: './login', layout: false },
];

2.5 按钮权限控制

import { Authorized } from '@snapmaker/admin-auth';

<Authorized permission="models:manage:create">
  <Button type="primary">新增模型</Button>
</Authorized>

<Authorized permission="models:manage:delete">
  <Button danger>删除</Button>
</Authorized>

三、后端接入 — Java(Spring Boot)

3.1 引入依赖

<dependency>
    <groupId>com.snapmaker.admin</groupId>
    <artifactId>snapmaker-admin-oauth-permission</artifactId>
</dependency>

3.2 配置

snapmaker:
  permission:
    skip-check: false       # 本地调试可设 true
    system-gateway-url: https://pre.api.snapmaker.com

3.3 实现 UserPrincipal SPI

@Component
public class MyUserPrincipal implements UserPrincipal {
    @Override
    public Long extractUserId(HttpServletRequest request) {
        // 从请求上下文获取当前用户 ID
        return SecurityUtils.getLoginUserId(request);
    }
}

3.4 Controller 加注解

@RestController
@RequestMapping("/api/admin/model/manage")
public class ModelController {

    @GetMapping("/model/list")
    @PreAuthorize("@ss.hasPermission('models:manage:query')")
    public Result<Page<ModelVO>> list() { ... }

    @PostMapping("/model")
    @PreAuthorize("@ss.hasPermission('models:manage:create')")
    public Result<Long> create(@RequestBody ModelSaveVO vo) { ... }

    @DeleteMapping("/model/{id}")
    @PreAuthorize("@ss.hasPermission('models:manage:delete')")
    public Result<Void> delete(@PathVariable Long id) { ... }

    @PostMapping("/model/{id}/approve")
    @PreAuthorize("@ss.hasPermission('models:manage:review')")
    public Result<Void> approve(@PathVariable Long id) { ... }
}

3.5 校验流程

请求 → Token 过滤器 → 解析 JWT → SecurityContext
  → @PreAuthorize("@ss.hasPermission('models:manage:query')")
  → SecurityFrameworkService
    → Redis 缓存查找 (1分钟)
    → 未命中 → Feign RPC → /rpc-api/system/permission/has-any-permissions
    → 查用户角色 ∩ 权限对应的角色 → 有交集 = true
    → super_admin 角色直接返回 true
  → true: 放行 / false: 403

四、后端接入 — Node.js(Express / Koa)

4.1 安装

pnpm add "git+https://github.com/Snapmaker/snapmaker-admin.git#main&path=packages/admin-auth"

4.2 初始化

SDK 提供两个版本,功能完全一致:

// 方式 A:纯 JS require(Node 原生可用,不需要编译)
const { ServerAuthClient } = require('@snapmaker/admin-auth/src/server.cjs');

// 方式 B:TypeScript import(Umi / tsx / webpack 项目)
import { ServerAuthClient } from '@snapmaker/admin-auth';

const auth = new ServerAuthClient({
  baseUrl: 'https://pre.api.snapmaker.com', // OAuth 系统地址
  cacheTTL: 60_000,                          // 权限缓存 1 分钟
});

4.3 Express 路由

const express = require('express');
const { ServerAuthClient } = require('@snapmaker/admin-auth/src/server.cjs');

const auth = new ServerAuthClient({ baseUrl: 'https://pre.api.snapmaker.com' });
const app = express();

// ── 每个路由声明需要的权限 ──
app.get('/api/admin/model/manage/model/list',
  auth.require('models:manage:query'),
  modelController.list
);

app.post('/api/admin/model/manage/model',
  auth.require('models:manage:create'),
  modelController.create
);

app.delete('/api/admin/model/manage/model/:id',
  auth.require('models:manage:delete'),
  modelController.delete
);

// ── 只验证登录态,不校验权限 ──
app.get('/api/me/profile', auth.authenticate(), (req, res) => {
  res.json({ user: req.user });  // req.user / req.permissions / req.roles 已挂载
});

4.4 Koa 路由

const Router = require('@koa/router');
const { ServerAuthClient } = require('@snapmaker/admin-auth/src/server.cjs');

const auth = new ServerAuthClient({ baseUrl: 'https://pre.api.snapmaker.com' });
const router = new Router();

router.get('/api/admin/model/manage/model/list',
  auth.koaRequire('models:manage:query'),
  modelController.list
);

router.post('/api/admin/model/manage/model',
  auth.koaRequire('models:manage:create'),
  modelController.create
);

// 只验证登录态
router.get('/api/me', auth.koaAuthenticate(), (ctx) => {
  ctx.body = { user: ctx.state.auth.user };
});

4.5 手动调用

async function handler(req, res) {
  const token = (req.headers.authorization || '').replace('Bearer ', '');
  const ok = await auth.hasPermission(token, 'models:manage:query');
  if (!ok) {
    res.statusCode = 403;
    res.end(JSON.stringify({ code: 403, msg: '无权限' }));
    return;
  }
  // 业务逻辑
}

4.6 校验流程

请求 → auth.require('models:manage:query')
  → 提取 Authorization: Bearer <token>
  → GET https://pre.api.snapmaker.com/admin-api/system/auth/get-permission-info
  → 返回 { permissions: ["models:manage:query", ...], roles: ["super_admin", ...] }
  → 缓存 (1分钟, 按 token 索引)
  → 检查 permissions.includes('models:manage:query')
    → super_admin 角色直接放行
  → true: next() / false: 403

五、新增接口完整 checklist

以一个"删除许可证"接口为例:

步骤 做什么 哪里做
1. 注册权限码 INSERT models:license:delete system_menu
2. 前端控制按钮 <Authorized permission="models:license:delete"> 页面组件
3. Java 后端校验 @PreAuthorize("@ss.hasPermission('models:license:delete')") Controller
4. Node 后端校验 auth.require('models:license:delete') 路由中间件
5. 角色分配 勾选该权限码给目标角色 admin-ui 角色管理

不需要改 SDK 代码。


六、常见问题

Q: 菜单/按钮不显示? A: 检查三处:① system_menu 表记录存在且 visible=truestatus=0;② 角色已勾选该菜单;③ 当前用户重新登录后 permissions 数组包含对应权限码。

Q: Token 过期? A: 前端 createPermissionRequest 的拦截器自动处理 401 → refresh → 重放。后端 Node 的 ServerAuthClient 不做自动刷新,过期的 token 调 OAuth 会直接返回 401。

Q: 超管为什么能看到所有菜单? A: 后端硬编码:super_admin 角色跳过所有权限校验。这不是 bug,超管需要在 admin-ui 里管理所有菜单和角色。

Q: 多个管理端共用一套权限? A: 是。共用 @snapmaker/admin-auth 包和同一个 OAuth 后端。每个端只写自己的 routes.ts 和对应的 hasMenu_xxx 定义。权限码在 system_menu 表里通过角色隔离控制谁能看到哪些菜单。

Q: 每次请求都调 OAuth 不会慢吗? A: 不会。Node SDK 有 1 分钟内存缓存(可调)。Java SDK 有 Redis 缓存 + Guava 本地缓存。同一个 token 的重复请求几乎零开销。