┌─────────────────────┐
│ 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 |
{
"dependencies": {
"@snapmaker/admin-auth": "workspace:*"
}
}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 },
},
});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'); },
});// 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 },
];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><dependency>
<groupId>com.snapmaker.admin</groupId>
<artifactId>snapmaker-admin-oauth-permission</artifactId>
</dependency>snapmaker:
permission:
skip-check: false # 本地调试可设 true
system-gateway-url: https://pre.api.snapmaker.com@Component
public class MyUserPrincipal implements UserPrincipal {
@Override
public Long extractUserId(HttpServletRequest request) {
// 从请求上下文获取当前用户 ID
return SecurityUtils.getLoginUserId(request);
}
}@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) { ... }
}请求 → 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
pnpm add "git+https://github.com/Snapmaker/snapmaker-admin.git#main&path=packages/admin-auth"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 分钟
});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 已挂载
});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 };
});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;
}
// 业务逻辑
}请求 → 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
以一个"删除许可证"接口为例:
| 步骤 | 做什么 | 哪里做 |
|---|---|---|
| 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=true、status=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 的重复请求几乎零开销。