唯杰地图 logo唯杰地图
VJMAP
EN

鉴权与加密图(Authentication)

vjmap-service/references/authentication.md 9.7 KB163 行
原文

1. JWT Token

  • vjmap 后端用 标准 HS256 JWT 做鉴权
  • Claims 里包含 ID / Username / NickName / AuthorityId / BufferTime / nbf / exp / iss=vjmap
  • 过期:exp = now + expires_time(config.json expires_time,默认 3153600000 秒 ≈ 100 年)。BufferTime(config buffer_time,默认 86400)只是写进 claims,鉴权中间件从不检查——服务端没有自动刷新/换新 token 的机制,过期就得换新 token
  • 生产环境拿 token 的方式:通过业务登录接口;开发环境可直接从 vjmap 后台复制(本地服务在 <server>/data/auth/token/*.token)

1.1 Token 放哪里

两种位置,优先级从高到低(只有两级回退,没有 form):

  1. HTTP Header:token: eyJ...
  2. 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 SDK pwdToSecretKey 一致;空串 → 空串;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 === 403
  • cmd/... 路由: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 控制)