鉴权与加密图(Authentication)
1. JWT Token
- vjmap 后端用 标准 HS256 JWT 做鉴权
- Claims 里包含
ID/Username/NickName/AuthorityId/BufferTime/nbf/exp/iss=vjmap - 过期:
exp = now + expires_time(config.jsonexpires_time,默认 3153600000 秒 ≈ 100 年)。BufferTime(configbuffer_time,默认 86400)只是写进 claims,鉴权中间件从不检查——服务端没有自动刷新/换新 token 的机制,过期就得换新 token - 生产环境拿 token 的方式:通过业务登录接口;开发环境可直接从 vjmap 后台复制(本地服务在
<server>/data/auth/token/*.token)
1.1 Token 放哪里
两种位置,优先级从高到低(只有两级回退,没有 form):
- HTTP Header:
token: eyJ... - Query:
?token=eyJ...
REST 接口不认 Authorization: Bearer ...——只有 MCP 路由在自己的中间件链里把 Bearer 转成 token 头(见 §7)。
SDK 实际上两种都带:URL 构造方法(map_url/cmd_url/service_url 等)把 token 拼进 query(便于 curl 排障),_request 同时自动附带 token header——反代剥 query 也能工作,无需任何开关。
1.2 Token 的 5 分钟内测
TOKEN="eyJhbG..."
curl -s "http://127.0.0.1:27660/version?token=$TOKEN"
curl -s "http://127.0.0.1:27660/api/v1/map/cmd/runstatus/_/v1?token=$TOKEN&detail=true"
1.3 常见错误
网关鉴权失败统一 FAIL=7 + HTTP 403({code:8} + HTTP 500 为服务端错误),msg 区分原因:
msg |
原因 |
|---|---|
Token is invalid |
没带 token(header 与 query 都为空),data:{reload:true} |
Couldn't handle this token: |
签名不匹配(换了 signing_key / 被篡改)等其它校验失败 |
That's not even a token |
不是 JWT 格式 |
Token expired |
已过期 |
Token not active yet |
nbf 未到 |
权限不足 |
token 合法,但角色被 casbin 策略拒绝(见 §1.4),data:{} |
HTTP 200 但业务字段异常 → token OK,但参数错误。
⚠️ 注记:系统根路径
/heart、/version是公共端点不过 JWT 鉴权——坏 token 请求它们也返回 200。验证鉴权行为要用/api/v1/...受保护端点(如/api/v1/map/cmd/runstatus/_/v1):坏 token →403 + {code:7},无 token →403;合法 token 放 query 单通道即可通过。
1.4 角色权限(casbin 默认策略)
角色取 claims 的 AuthorityId(内置用户 root1/admin1/guest1 → root/admin/guest)。策略文件 data/auth/policy.conf 首次启动生成,按请求 URI 做 regexMatch(子串正则):
| 角色 | 默认拒绝 |
|---|---|
| root | 无(全放行) |
| admin | /api/v1/service/workspace*(工作区 create/modify/delete/status,getWorkspace 不受影响)、/api/v1/map/cmd/slicelayer/*、对 sys_* 图的 updatemap / renamemap / deletemap / deletestyle、/*RegCode、/client_* |
| guest | 整个 /api/v1/*,以及 /*RegCode、/client_* |
被拒返回 HTTP 403 {code:7, msg:"权限不足"}。
2. 加密图(secretKey / accessKey)
2.1 设密:只在 v1 首次 openmap 时
- 上传接口
/map/uploads只取 multipart 里的文件,不读任何表单字段——上传时传secretKey无效 - 唯一入口:该 mapid 首次
openmap且版本为v1(mapinfos ini 尚不存在)时,请求参数里的secretKey若长度 ≥ 10 → 服务端原样存为该图 secretKey(不做 md5),同时生成随机salt与本图 accessKey =NormalizeMd5(salt + secretKey, 18, "ak")(18 位、ak开头)。不足 10 位静默不加密 - 之后改密 / 取消密码 / 取 accessKey 走
cmd/permission(§2.5) - 明文密码建议先转成 16 位 key 再提交——
computeSecretKey(pwd)/compute_secret_key(pwd)(连续 4 次 md5 取前 16,与官方 JS SDKpwdToSecretKey一致;空串 → 空串;18 位ak开头原样返回)。服务端不做这一步,官方前端弹框输入密码后也是先pwdToSecretKey再发
2.2 权限判定
| 请求里出现 | 匹配 | 得到权限 |
|---|---|---|
accessKey 列表含 config map_super_key |
实例所有图 | admin |
secretKey 列表含该图 secretKey |
该图 | admin |
accessKey 列表含该图 accessKey |
该图 | 仅浏览 |
- 未设密的图对任何请求都是 admin
- 删除、重命名、
cmd/permission等需要图级 admin,只拿 accessKey 不够
2.3 传递方式
- Header:
secretKey: k1_;_k2、accessKey: ak1_;_ak2(多个用_;_连接,后端按它切分);也可放 body/query 的同名驼峰键(后端无secret_key消费点) - ⚠️ header 会覆盖 body/query 里的同名键。首次 openmap 设密时若 header 里带的是别的 key 串,存进去的就是整串 header 值
- SDK 缓存:
_secretKeys/_secret_keys按形态拆头——18 位ak开头 →accessKey头,其余 →secretKey头;_accessKeys/_access_keys里的值始终走accessKey头(super key 放这里)。openMap/open_map显式传了secretKey/accessKey时,本次请求对应的头只带该值
svc._secret_keys = ["<16chars>", "ak................"] # 前者 → secretKey 头,后者 → accessKey 头
svc._access_keys = ["<map_super_key>"] # 始终 → accessKey 头
svc._secretKeys = ['16chars...', 'ak................'];
svc._accessKeys = ['<map_super_key>'];
2.4 加密图典型流程
key = compute_secret_key("my-password") # 16 位;服务端原样存/比对,<10 位不加密
up = svc.upload_map("./finance.dwg")
svc.open_map(mapid="finance_map", version="v1",
fileid=up["fileid"], secretKey=key) # v1 首次打开即设密
svc._secret_keys.append(key) # 之后的请求自动带 secretKey 头
meta = svc.metadata("finance_map", "v1")
2.5 改密 / 取 accessKey:cmd/permission/{mapid}/_
body {opName, key, newKey?}。前提:本次请求已对该图有 admin 权限(带对 secretKey,或把 super key 作 accessKey),否则回 {status:false, error:true, reason:"no admin permisson, please use password or superKey"};且 key 必须等于该图 secretKey 或 super key,否则 reason:"old password is not right"。
opName |
作用 |
|---|---|
GetAccessKey |
返回 {status:true, accessKey} |
ResetAccessKey |
重新生成 salt 与 accessKey 并返回(旧 accessKey 失效) |
ModifyPassword |
newKey 原样存为新 secretKey 并重生 accessKey(须 ≥ 10 位,否则 new password is not valid);newKey 为空 → 取消密码保护;原先无密码的图则是新设密 |
SDK 未封装,用 _request 裸调:
r = svc._request("POST", svc.cmd_url("permission", "finance_map", "_"),
json_body={"opName": "GetAccessKey", "key": key})
2.6 加密图的错误形态
openmap缺 key / key 错:HTTP 200 +{status:403, isPasswordError, error}(isPasswordError= 请求里带了 secretKey 或 accessKey)——SDK 不抛异常,须自查res.status === 403cmd/...路由:HTTP 403 +{error:"no permission to access this map, request must include secretKey or accessKey"}→ SDK 抛VjmapError(status=403)。thumbnail/wms以及 mapid 为_/_null的 cmd 不做此校验
3. Super Key(运维视角)
.env 里的 VJMAP_MAP_SUPER_KEY(测试/示例约定,对应服务端 config.json 的 map_super_key)是一把超级密钥:
- 只在请求的
accessKey列表里匹配——当secretKey/ password 提交无效。SDK 放svc._accessKeys/svc._access_keys - 对实例所有加密图都是 admin 权限
cmd/permission的key可以填 super key,但前提是请求已带 super key 作 accessKey 拿到了 admin 权限- 二次开发一般不需要它;只有测试框架需要跑加密场景时才用
4. CSRF / 同源
- 后端不强制 CSRF token;依赖 JWT 签名
- 浏览器侧调用请注意 CORS:Go 网关与图形服务都默认
Access-Control-Allow-Origin: *;如果在受限部署里碰到 CORS,需要运维调整这两处中间件/构建配置
5. 速率限制
- 默认无全局速率限制
- 单个 cmd 的并发由
runstatus.maxParallelProcessGeomNum(几何渲染)控制,典型值 2-4 - 超并发请求会被排队,不会报错但会"看起来卡住"
6. Token 重置路由(运维,不是刷新)
没有"过期前换新 token"的接口。config system.update_token_route(长度 ≥ 2 才注册)会挂一个 GET /<该值>(同样过 JWT + casbin):
- 用 query
signing_key/expires_time/buffer_time改写 config 中的 JWT 配置——signing_key不传时回落为内置默认 key - 随后为内置用户重签全部 token(写
data/auth/token/*.token) - signing_key 一变(包括不传而当前配置是自定义 key 的情况),所有已发出的旧 token 立即失效
这是运维重置端点,二次开发不要调用。
7. MCP 路由
- 默认关闭:config
mcp.enable=true才注册;路径mcp.route(缺省/mcp),streamable HTTP - 注册在全局 JWT 之前,自带"Bearer 适配 → JWT → casbin"链:token 可放
token头、Authorization: Bearer <jwt>(仅在没有token头时转成token头)或?token=;mcp.disable_auth=true可关闭鉴权(仅限可信内网) - URL 参数:
?tools=分类或工具名(白名单)、?exclude=(黑名单)、?workspace=名称(本连接默认工作区) GET {route}/tools:列出当前暴露的工具(name/category/write/description)与enableWrite(写类工具受mcp.enable_write控制)