用户权限集成
大约 8 分钟
用户权限集成
VJCAD 作为二次开发平台,本身不内置用户系统。通过用户信息传递 + 后台权限回调机制,二次开发者可以将自己业务系统中的用户身份和权限控制无缝集成到 VJCAD 的协同编辑流程中。
在线示例
| 示例 | 描述 | 链接 |
|---|---|---|
| 用户权限回调 | 演示业务系统登录、用户身份传递和权限校验回调 | 在线演示{target="_blank"} |
整体架构
工作原理
- 前端传递用户信息:二次开发者在创建
MainView时通过userInfo配置传入业务系统的用户信息(userId、userName、sessionId) - 请求自动携带:所有 VJCAD 操作请求会自动在请求体中携带这些用户信息
- 后端权限回调:VJCAD 后端(odasvr)在执行每个操作前,向配置的权限回调 URL 发起 HTTP 请求,将用户信息和操作信息发送给业务系统
- 业务系统决策:业务系统根据自己的权限逻辑返回允许或拒绝
- 结果反馈:如果被拒绝,前端会触发
onAuthError回调,二次开发者可以做相应处理(如跳转登录页、提示无权限等)
前端集成
基本用法
const { MainView, initCadContainer } = vjcad;
const cadView = new MainView({
serviceUrl: "http://127.0.0.1:27660/api/v1",
accessToken: "your-access-token",
// 传入业务系统的用户信息
userInfo: {
userId: "user_12345", // 必填,用户唯一标识
userName: "张三", // 选填,显示名称(为空时显示 userId)
sessionId: "sess_abc123", // 选填,业务系统会话ID
},
// 权限错误回调
onAuthError: (error) => {
// error.errorCode: "session_expired" | "no_permission" | 自定义错误码
// error.message: 人类可读的错误信息
// error.operation: 触发错误的操作名称
if (error.errorCode === 'session_expired') {
window.location.href = '/login'; // 跳转登录页
} else {
alert('权限不足: ' + error.message);
}
},
});
initCadContainer("cad-app", cadView);完整集成示例(含登录流程)
const { MainView, initCadContainer } = vjcad;
// ========== 第一步:调用业务系统登录接口 ==========
async function login(userId, password) {
const resp = await fetch('http://your-auth-server/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, password })
});
return await resp.json();
// 返回格式: { success: true, sessionId: "sess_xxx", userName: "张三" }
}
// ========== 第二步:登录成功后创建 MainView ==========
const loginResult = await login('test_user', '123456');
if (loginResult.success) {
const cadView = new MainView({
serviceUrl: "http://127.0.0.1:27660/api/v1",
accessToken: "your-access-token",
userInfo: {
userId: loginResult.userId,
userName: loginResult.userName,
sessionId: loginResult.sessionId,
},
onAuthError: (error) => {
if (error.errorCode === 'session_expired') {
alert('会话已过期,请重新登录');
} else {
alert(error.message);
}
},
});
initCadContainer("cad-app", cadView);
}运行时更新用户信息
当会话续期或用户切换时,可以通过 updateUserInfo 方法动态更新:
// 会话续期后更新 sessionId
cadView.updateUserInfo({
userId: "user_12345",
userName: "张三",
sessionId: "new_sess_def456", // 新的 sessionId
});不传 userInfo 的行为
如果不配置 userInfo,VJCAD 的行为与之前完全一致:
- 使用浏览器指纹作为默认作者标识
- 不触发后台权限回调
- 保持向后兼容
后端配置
config.json 配置
在 vjmap 服务后台 的 config.json 中配置权限回调:
{
"map": {
"auth_callback": {
"url": "http://127.0.0.1:3200/api/auth/check",
"method": "POST",
"timeout": 5000,
"fail_policy": "deny"
}
}
}| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url | string | 空 | 权限回调接口地址。为空或不配置则跳过权限回调 |
method | string | "POST" | HTTP 请求方式,支持 POST 和 GET |
timeout | number | 5000 | 超时时间(毫秒) |
fail_policy | string | "deny" | 回调失败(超时/网络错误)时的策略:"deny" 拒绝操作,"allow" 放行 |
提示
如果不配置 auth_callback,VJCAD 后端不会进行任何权限回调,所有操作仅受 SDK 自身的 secretKey/accessKey 控制。
回调触发时机
每次 VJCAD 操作都会触发权限回调(不使用缓存),业务系统可以在回调中进行:
- 权限验证
- 操作日志记录
- 配额统计
- 工作流触发
- 其他自定义逻辑
回调覆盖的操作
| 操作 | operation 值 | 说明 |
|---|---|---|
| 列出图纸 | listVJCADDraws | 获取图纸列表 |
| 获取图纸数据 | getVJCADData | 打开/读取图纸 |
| 保存修改 | saveVJCADPatch | 保存编辑内容 |
| 删除图纸 | deleteVJCADDraw | 删除整个图纸或指定版本 |
| 创建分支 | createVJCADBranch | 创建新分支 |
| 删除分支 | deleteVJCADBranch | 删除分支 |
| 合并分支 | mergeVJCADBranch | 合并分支 |
权限回调接口规范
请求格式(POST)
{
"userId": "user_12345",
"sessionId": "sess_abc123",
"userName": "张三",
"operation": "saveVJCADPatch",
"resource": {
"type": "imports",
"mapId": "drawing-001",
"version": "v1",
"designPath": "",
"branch": "main"
}
}| 字段 | 说明 |
|---|---|
userId | 用户唯一标识 |
sessionId | 业务系统会话ID |
userName | 用户显示名称 |
operation | 操作名称(见上表) |
resource.type | 图纸类型:"imports"(导入图纸)或 "designs"(设计图纸) |
resource.mapId | 地图ID(imports 类型) |
resource.version | 版本号 |
resource.designPath | 设计图路径(designs 类型) |
resource.branch | 分支名称 |
响应格式
{
"allowed": true,
"reason": "no_permission",
"message": "只有管理员才能删除图纸"
}| 字段 | 类型 | 说明 |
|---|---|---|
allowed | boolean | true 允许操作,false 拒绝操作 |
reason | string | 拒绝原因码(前端通过 errorCode 接收)。建议值:"session_expired"、"no_permission"、自定义错误码 |
message | string | 人类可读的错误信息(前端通过 error.message 接收) |
后端权限服务示例(Node.js)
以下是一个完整的 Node.js 权限服务示例,包含登录、登出和权限校验接口:
/**
* VJCAD 权限回调服务示例(Node.js + Express)
*
* 安装依赖: npm install express
* 启动服务: node auth-server.js
*
* 然后在 odasvr 的 config.json 中配置:
* {
* "map": {
* "auth_callback": {
* "url": "http://127.0.0.1:3200/api/auth/check",
* "method": "POST",
* "timeout": 5000,
* "fail_policy": "deny"
* }
* }
* }
*/
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = 3200;
app.use(express.json());
// CORS 支持(前端可能在不同端口)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});
// ==================== 用户数据(实际项目中连接数据库) ====================
const users = {
'admin': { password: '123456', userName: '管理员', role: 'admin' },
'editor_user': { password: '123456', userName: '编辑者', role: 'editor' },
'readonly_user': { password: '123456', userName: '只读用户', role: 'viewer' },
};
// ==================== 会话管理 ====================
const sessions = new Map();
const SESSION_TTL = 30 * 60 * 1000; // 30 分钟
// POST /api/login - 登录
app.post('/api/login', (req, res) => {
const { userId, password } = req.body;
const user = users[userId];
if (!user || user.password !== password) {
return res.json({ success: false, message: '用户名或密码错误' });
}
const sessionId = 'sess_' + crypto.randomBytes(16).toString('hex');
sessions.set(sessionId, {
userId, userName: user.userName, role: user.role,
expiresAt: Date.now() + SESSION_TTL
});
res.json({ success: true, userId, userName: user.userName, sessionId, role: user.role });
});
// POST /api/logout - 登出
app.post('/api/logout', (req, res) => {
sessions.delete(req.body.sessionId);
res.json({ success: true });
});
// ==================== 权限校验接口(由 VJCAD 后端自动调用) ====================
app.post('/api/auth/check', (req, res) => {
const { userId, sessionId, operation, resource } = req.body;
// 1. 验证会话
const session = sessions.get(sessionId);
if (sessionId && (!session || Date.now() > session.expiresAt)) {
if (session) sessions.delete(sessionId);
return res.json({
allowed: false,
reason: 'session_expired',
message: '会话已过期,请重新登录'
});
}
// 2. 根据角色判断权限
const role = session?.role || 'guest';
// 封禁用户
if (role === 'blocked') {
return res.json({
allowed: false, reason: 'no_permission', message: '该用户已被禁止操作'
});
}
// 只读用户不能执行写操作
if (role === 'viewer' && !['listVJCADDraws', 'getVJCADData'].includes(operation)) {
return res.json({
allowed: false, reason: 'no_permission', message: '只读用户不能执行写操作'
});
}
// 非管理员不能删除
if (['deleteVJCADDraw', 'deleteVJCADBranch'].includes(operation) && role !== 'admin') {
return res.json({
allowed: false, reason: 'no_permission', message: '只有管理员才能执行删除操作'
});
}
// 3. 通过 — 这里还可以做日志记录、配额统计等
console.log(`[AUTH] ${userId} ${operation} -> ALLOW`);
res.json({ allowed: true });
});
// 定时清理过期会话
setInterval(() => {
const now = Date.now();
for (const [id, s] of sessions) {
if (now > s.expiresAt) sessions.delete(id);
}
}, 60000);
app.listen(PORT, () => {
console.log(`权限服务运行在 http://127.0.0.1:${PORT}`);
});权限检查流程
VJCAD 后端的权限检查是双层串行的:
- SDK 自身权限(secretKey/accessKey) — 地图级别的访问控制
- 业务权限回调(auth_callback) — 用户级别的操作权限
向后兼容
- 不配置
auth_callback→ 不进行业务权限回调 - 不传
userInfo→ 不进行业务权限回调,使用浏览器指纹作为作者 - 两种情况都完全兼容现有行为
协同编辑中的用户标识
配置了 userInfo 后,协同编辑中的以下位置会使用真实用户名(而非机器指纹):
- 保存 Patch 的作者字段:
author字段使用userName(为空时使用userId) - 创建分支的作者字段
- 合并分支的作者字段
- 冲突信息中的作者来源
常见问题
Q: 不启动权限服务,VJCAD 能正常运行吗?
可以。如果 config.json 中没有配置 auth_callback,或 auth_callback.url 为空,所有操作正常执行,不会进行权限回调。
Q: 权限服务挂了会怎样?
取决于 fail_policy 配置:
"deny"(默认):所有带 userId 的操作会被拒绝,返回 "service unavailable""allow":权限服务不可用时放行所有操作
Q: 每次操作都回调,性能有影响吗?
权限回调是同步的,会增加每次操作的延迟(通常 1-10ms 内网环境)。建议:
- 权限服务部署在与 VJCAD 后端同一内网
- 设置合理的
timeout(建议 3000-5000ms) - 权限服务自身的响应尽量快速
Q: 可以只对写操作做权限回调吗?
权限回调会对所有 7 种操作触发。业务系统可以在回调中对读操作直接返回 { "allowed": true } 来快速放行。