第22章 - 服务端、自托管与部署运维

GeoLibre 的核心是一个纯客户端的 GIS:地图渲染、图层管理、DuckDB 空间分析、Whitebox 工具链全部在浏览器或桌面 WebView 中运行,默认不需要任何服务器。但当团队要把工程集中托管、要给成员分配账号与权限、要多人同时编辑同一张图,或者要把敏感数据锁在自己的内网时,就需要引入服务端组件。

本章讨论三类服务端组件的职责边界、参考实现的 HTTP 契约、Docker 部署方法、自托管架构选择,以及把 GeoLibre「钉死」成只读展示终端的部署策略。本章所有路径、变量、端点都来自仓库中的 docs/server-api.md、docs/self-hosting.md、docs/deployment-policy.md、docs/deployment-capabilities.md、docs/ui-profiles.md、docker-compose.yml 与 backend/geolibre_server_api/。

注意:本章不会把「客户端隐藏」说成安全边界。GeoLibre 的能力开关(capabilities)、UI Profile、菜单隐藏都只是界面收敛;真正的访问控制只能靠反向代理、账号体系与容器路由策略。

22.1 三类服务端组件与职责边界

仓库里叫「server」的东西有三个,它们解决的问题完全不同,混用是部署事故的常见起点。

组件 代码位置 默认监听 使用者 是否必需
本地处理 sidecar backend/geolibre_server 127.0.0.1:8765 本机单个用户 否,桌面端按需拉起
项目与身份 API backend/geolibre_server_api 容器内 0.0.0.0:8000 多用户、跨机器 否,要托管工程时部署
协作中继 workers/collab-node(自托管)或 workers/collab(Cloudflare) 8787(Node 版) 多人实时编辑 否,要实时候选中继时部署

22.1.1 本地处理 sidecar(127.0.0.1:8765)

sidecar 是 GeoLibre 的「重型算子外包进程」:FastAPI + uvicorn,只在需要 rasterio、GeoPandas、SedonaDB、Whitebox 或转换运行时的时候才被用到。

  • 桌面端按需启动,绑定回环地址 127.0.0.1:8765;启动时通过 GEOLIBRE_SIDECAR_TOKEN 注入一个令牌,Tauri 侧用 X-GeoLibre-Token 请求头携带(见 docker/nginx.conf 与 apps/geolibre-desktop/src-tauri/src/lib.rs)。
  • 浏览器构建不直连 8765,而是走同源的 /sidecar 反向代理,因此没有 CORS 问题。Docker 镜像内置了 sidecar,并由 entrypoint 生成随机令牌,nginx 在转发时注入。
  • sidecar 的读写路径被 GEOLIBRE_CONVERSION_ROOTS 限制(镜像默认 /data)。桌面端自己的 sidecar 默认不限制,因为它服务的是用户自己的文件系统。

主要端点:

方法 路径 用途
GET /health 健康检查
POST /run 运行算法(占位实现,返回 501)
GET /conversion/status 转换运行时是否就绪
POST /conversion/vector-to-geoparquet 矢量 → 按 Hilbert 排序的 GeoParquet
POST /conversion/vector-to-flatgeobuf 矢量 → 按 Hilbert 排序的 FlatGeobuf
POST /conversion/csv-to-geoparquet 经纬度 CSV → GeoParquet
POST /conversion/vector-to-pmtiles 矢量 → PMTiles
POST /conversion/raster-to-cog 栅格 → COG
GET /conversion/jobs/{id} 转换任务状态
GET /sql/status、POST /sql/run SedonaDB 空间 SQL
GET /ml/status、POST /ml/segment/* SAM 3 分割代理(转发到独立的 samgeo-api)

提示:浏览器构建不需要 sidecar 就能跑 Whitebox 工具箱与 GeoParquet/CSV 转换——它们分别跑在 WebAssembly 与 DuckDB-WASM 上。sidecar 补的是栅格工具全集、GeoPandas 引擎与 Sedona 引擎。

22.1.2 项目与身份 API(多用户后端)

这是本章的主角:backend/geolibre_server_api,FastAPI + SQLAlchemy,数据库可以是 PostgreSQL(推荐)或 SQLite,对象存储可以是本地目录或 S3 兼容存储。它实现了 docs/server-api.md 定义的 v1 契约,为 Project Gallery 与 Project → Share 提供账号、组织、组、工程版本、分享链接与传输。

它的关键特征是「公共契约」而不是「GeoLibre 私有后端」:任何实现只要满足同一份 HTTP 契约,客户端都能对接。文档原文把参考实现定位为「correctness baseline, not a hardened deployment」——正确性基线,不是加固过的生产部署。

22.1.3 协作中继(WebSocket)

实时协作是实验性能力,默认关闭,只有配置了 VITE_GEOLIBRE_COLLAB_URL(或部署策略的 sharing.collabUrl)才会出现。仓库提供两种中继:

形态 位置 存储 端点
Cloudflare Durable Object workers/collab Durable Storage POST /sessions、GET /sessions/:id/ws、GET /health
单进程 Node 版 workers/collab-node SQLite POST /sessions、GET /sessions/:id/ws、GET /health

Node 版的配置项:PORT(默认 8787)、HOST(默认 0.0.0.0)、COLLAB_DB_PATH(默认 ./data/collab.sqlite)、COLLAB_MAX_SNAPSHOT_BYTES(默认 1000000,与托管 Worker 一致)、COLLAB_IDLE_TTL_MS(最后一名参与者断开后保留会话多久,默认两小时)。

中继传的是整份工程快照(last-write-wins)、光标与视口 presence、聊天与评论变更,不做 CRDT 级合并。这意味着:中继是信任边界内的小圈子工具,不要把它当成公网多人协作服务直接暴露。

22.1.4 什么时候真的需要服务端

需求 需要的组件 说明
本机分析、离线出图 都不需要 数据可以完全不出机器
浏览器里导出 PMTiles / 跑栅格工具 sidecar(可选) 浏览器可替代大部分;桌面端自带
团队共享工程、版本、分享链接 项目与身份 API 组织/组/可见性都在这一层
多人同时改同一张图 协作中继 配合项目 API 做持久化更完整
私有数据目录与按数据集授权 GeoLens + 同源部署 见 22.10 节
只读大屏 / 展厅 不需要后端 用部署策略收紧能力即可,见 22.11 节

一个典型的私有化拓扑是:反代(TLS + SSO)在同一域名下分发 /(geolibre-web:80)、/api 与 /oauth(geolibre-server:8000)、中继(geolibre-collab:8787)与目录/瓦片(GeoLens:8080 → PostGIS + 对象存储)。

22.2 HTTP 契约:约定、限额、缓存与 CORS

22.2.1 基础约定

项目 约定
Base URL 容器运行期用 GEOLIBRE_SHARE_URL,构建期用 VITE_GEOLIBRE_SHARE_URL
视图地址 GEOLIBRE_VIEWER_URL,用于拼出 viewerUrl
请求/响应体 application/json,键名 camelCase
时间 UTC ISO 8601 字符串
认证 Authorization: Bearer <token>,个人 API 令牌或 OAuth 访问令牌
未知字段 消费方必须忽略,服务端可以在 v1 内新增可选字段

22.2.2 状态码语义

状态码 含义 备注
401 令牌缺失、无效或过期 响应体 {"error": "..."}
403 已认证但无权限 缺作用域时 error 为 insufficient_scope
404 工程不存在或调用者不可见 故意合并,避免探测
409 唯一性冲突(用户名、slug、邮箱、待处理传输) 客户端按稳定短语匹配
410 分享链接已过期 share link expired
413 上传体积超限 文档或缩略图

所有错误响应都是带 error 字符串的 JSON 对象。客户端会按其中的稳定短语(例如 version conflict、share link expired、delete-protected)给出解释,所以自定义实现不要改写这些短语。

22.2.3 缓存规则

缓存不是可选项,它直接决定「撤销权限后对方还能不能看到」。

内容 响应头 效果
public / unlisted 的原始工程体(最新版与历史版) Cache-Control: public, no-cache + 强 ETag 可缓存但每次必须回源;If-None-Match 命中返回 304
private / organization / group 可见内容(含元数据列表) Cache-Control: private, no-store 不落任何共享缓存
已认证的 public 响应(因 canEdit 随调用者变化) private, no-store 不同用户不能复用缓存
组缩略图(open 组) public, max-age=3600 公开组可缓存一小时
组缩略图(invite / request 组) private, no-store 非成员得到 404

警告:raw URL 对私有工程不是 capability URL。改变可见性会立刻作用于所有版本,所以不要假设「对方拿到过链接就永远是旧的权限」。

22.2.4 跨域(CORS)

  • 跨域 Web 部署必须允许 GeoLibre Web 源携带 Authorization 与 Content-Type。
  • 自托管 Tauri 桌面端若走浏览器 fetch,还要显式允许 tauri://localhost 和/或 http://tauri.localhost。
  • 官方发行版桌面端对 https://share.geolibre.app 的认证请求走原生 HTTP,不依赖 CORS;但其它主机不会自动绕过。

参考实现的行为:GEOLIBRE_CORS_ORIGINS 接受逗号分隔的源,默认 *(此时不允许携带凭据),允许的方法为全部,允许的请求头仅 Authorization 与 Content-Type,并暴露 WWW-Authenticate。OAuth 的 CORS 永远包含已注册的 Web 回调源,但绝不继承 *。

22.2.5 限额

字段 限额
工程标题(由上传的工程推导) 100 个 Unicode 码点
用户名 3–39 个小写 ASCII 字母、数字或连字符,首尾不能是连字符
slug 1–100 个小写 ASCII 字母、数字或连字符
描述 2,000 个 Unicode 码点
标签 20 个,每个 40 个码点
工程文档 50 MiB UTF-8 JSON
缩略图 5 MiB;PNG / JPEG / WebP
分页 limit 默认 24,最大 100

服务端可以把上传上限调小,但必须返回 413 并说明具体限值。参考实现从 GEOLIBRE_MAX_PROJECT_BYTES 与 GEOLIBRE_MAX_THUMBNAIL_BYTES 读取(默认 50 MiB / 5 MiB)。

22.2.6 运营方必须自己补的两件事

参考实现明确把两项防护留给运维:

  1. 限流。除 OAuth 同意流程对单个浏览器绑定的待处理交互有上限外,没有通用请求限流器,而换一个 cookie 就能绕过那道上限。公开启用 OAuth 之前,必须在入口对 GET/POST /oauth/authorize、GET /oauth/sso/callback、POST /oauth/token、POST /api/auth/token、POST /api/accounts、POST /api/account/password 做按客户端 IP 的限制。

文档给出的做法是在 nginx 的 http 上下文声明 limit_req_zone(例如 rate=12r/m),并在 TLS 入口的 server 上对这组路由应用 limit_req(burst=6 nodelay)、把 client_max_body_size 压到 16k、关闭这些路径的 access log。

必须保留原始的 Host authority(含端口),否则 OAuth 的 host 绑定会拒绝请求。

  1. 请求体大小上限。服务端会在读取请求体之前拒绝声明了超大 Content-Length 的请求,但 chunked 或 HTTP/2 请求不声明长度,会被完整解析后才应用逐路由限制。因此代理也要设上限。

22.3 身份与令牌:账户、PAT 与会话管理

22.3.1 创建账户并领取个人令牌

POST /api/accounts 创建账户并只返回一次个人 API 令牌。当部署把身份委托给外部提供方时,这个端点可以被禁用。

{
  "username": "ada",
  "password": "correct horse battery staple",
  "email": "ada@example.org",
  "name": "GeoLibre desktop",
  "scopes": ["read:projects", "write:projects"],
  "expiresInDays": 30
}

响应 201:

name、scopes、expiresInDays 都是可选的。新令牌默认授予三个工程作用域。省略 expiresInDays 会沿用 v1 的「只能删除」生命周期——令牌不过期;显式有效期只接受 1–365 天。未知或空的 scopes 返回 400 invalid_scope,超范围的 expiresInDays 返回 400 invalid_request。密码短于 8 位或用户名格式错误是 422,用户名/邮箱被占用是 409。

22.3.2 用账号换令牌

POST /api/auth/token 接受同样的可选策略字段(但没有 email),返回同样结构。令牌是不透明字符串,数据库只存 SHA-256 摘要。

状态 error 触发条件
401 invalid username or password 凭证错误
401 account temporarily locked 组织锁定策略命中
403 password expired 需先改密码
403 single sign-on required 账号所属组织禁用了内置账号

22.3.3 账户信息与会话

端点 作用 备注
GET /api/account 返回 {"account": …} 任意有效凭证
PATCH /api/account 设置/清除邮箱 需要 write:projects;重复邮箱 409
DELETE /api/auth/token 撤销当前 Bearer 令牌 个人令牌被删除;OAuth 令牌撤销整个会话族;返回 204
GET /api/users/me 账户 + 生效作用域 + OAuth sessionId 个人令牌的 sessionId 为 null

由身份提供方创建的账户可能没有用户名。这种账号创建工程时必须返回 400,错误信息中包含稳定、大小写不敏感的哨兵文本 username required,客户端据此把用户引到账户设置页。

22.3.4 会话管理(只能由管理授权访问)

会话管理路由要求一个独立的 OAuth Bearer 授权,其唯一作用域是 manage:sessions。同账号的工程授权、以及所有个人 API 令牌访问这些路由都会得到 403 insufficient_scope。客户端的正确顺序是:先用工程凭证查出账号 ID 与工程 sessionId,再申请一次管理同意,比对管理授权的账号 ID 后才列东西。管理凭证不能持久化,也不能用于工程/画廊调用。

端点 行为
GET /api/auth/sessions?limit=50&offset=0&currentSessionId=<id> 列出活跃的工程 OAuth 会话与个人令牌,最新的在前;命中的那条标记 current: true
DELETE /api/auth/sessions/{id} 撤销整族;未知、他人或纯管理 ID 都返回同样的 404
POST /api/auth/sessions/revoke-others 原子地撤销该账号其它所有 OAuth 族与全部个人令牌,保留请求中命名的那条工程会话与调用方管理授权

limit 为 1–100,offset 非负。currentSessionId 若给了,必须是该管理账号拥有的活跃工程会话,否则 404。行内只出现活跃未过期的工程 OAuth 会话与个人令牌,短命的管理授权永不出现。每行只含公开 UUID,不含令牌本身:

每行字段是 id(公开 UUID)、kind、clientId、label、scopes、createdAt、lastUsedAt、expiresAt、current 与 legacy。

个人令牌的 kind 是 personal-token、clientId 为 null、current 为 false,且 expiresAt 可能为 null。历史令牌在列出时会被补写策略并标记 legacy: true,在撤销前仍然有效。

警告:revoke-others 会连带撤销脚本与 CI 使用的个人令牌。客户端必须先确认,并明确告知用户这一点。

22.3.5 作用域矩阵

作用域 授予 典型用途
read:projects 列出并打开自己的工程(含 unlisted/private);读组织/组成员关系、画廊、共享给自己的工程 只读浏览、CI 拉取
write:projects 创建/更新/删除/分叉可管理的工程;管理组织、组、成员与邀请;修改账户邮箱 自动化上传
share:public 创建 public 工程,或把工程可见性提升到 public 最小权限发布账号

默认授予全部三个。缺少所需作用域时返回 403,响应体 {"error":"insufficient_scope","requiredScope":"<scope>"},并带 WWW-Authenticate: Bearer error="insufficient_scope"。缺失凭证用普通 Bearer 挑战;格式错误、未知、已撤销、已过期的凭证用 Bearer error="invalid_token"。

22.4 可见性、组织与组

22.4.1 四种可见性

可见性 公开列表 读取权限 缓存
public 出现 无需认证 public, no-cache
unlisted 不出现 持 URL 者可见 public, no-cache
private 不出现 个人工程仅所有者;组织私有工程对管理员与在建者(仍是管理员/发布者/成员)可见;其它人需显式组共享 private, no-store
organization 不出现 组织内所有已登录成员 private, no-store

组织私有工程的原始体与缩略图要求与元数据端点同一枚 Bearer 令牌。

22.4.2 组织

POST /api/organizations 创建组织,调用者成为第一个 administrator。请求体包含 slug(全局唯一)、name、publicSharingPolicy(yes / publishers / no)、defaultVisibility,以及可选的 categories。public 默认可见性要求 publicSharingPolicy 为 yes,其它组合在创建或修改时返回 422。

角色 权限
administrator 管理设置与成员,可改动组织拥有的任意工程
publisher 创建组织内容;策略为 publishers 或 yes 时可公开发布
member 创建组织内容并在组织内共享;仅在策略为 yes 时可发布
viewer 只读组织可见内容

创建者对自己创建的组织内容保有管理权,前提是仍保有该组织角色。被降为 viewer 或移出组织后,管理权立刻消失,但工程仍归组织所有,不会变成孤儿。同一条规则也管私有读取:管理员与活跃的创建者能读他们能管理的私有组织工程;仅凭成员身份读不到别人的私有工程。

组织路由(节选):

端点 说明
GET /api/organizations/mine 列出成员关系与本人在每个组织的角色
GET/PATCH/DELETE /api/organizations/{id} 读设置 / 改设置 / 删除(删除会连带组织工程、对象、组、成员与邀请;若任一工程受删除保护则 409 整体拒绝)
GET/PUT/DELETE /api/organizations/{id}/members 列出 / 增改 / 移除成员;{username}=me 表示退出
POST/GET /api/organizations/{id}/invitations 创建/列出邀请;创建响应是唯一带原始 token 的地方
POST /api/organizations/invitations/{token}/accept 受邀者接受邀请
GET /api/organizations/{id}/projects 组织画廊

收紧 publicSharingPolicy 时,所有「创建者按新策略已无权公开发布」的 public 工程会被改成 organization,并为每个工程记一条 visibility_change 活动。最后一名管理员不能被移除、退出或降级(409);身份提供方的 break-glass 管理员也不能(422,需先清空 breakGlassUsername)。

22.4.3 组

POST /api/groups 创建独立组或组织附属组。请求体含 name、可选 description 与 organizationId、joinPolicy(invite / request / open)以及 sharedUpdate。组织附属组只接纳该组织成员,其它人加入/受邀/接受邀请/批准申请一律 403;sharedUpdate 在创建时固定,之后不能 patch。

组成角色 能力
owner 管理 manager、转移所有权;本人未转移前不能退出
manager 增删普通成员、发邀请、审批申请、把工程移出组
member 普通成员

组路由与组织路由同构:成员/邀请/加入/审批/工程目标都在 /api/groups/{id}/... 下。工程创建与 patch 接受 groupIds,调用者必须是每个目标组的已接受成员;组织工程的非管理员只能指向该组织的组。成员能读被共享到自己组的私有工程,但只有在 sharedUpdate 为 true 时才能更新内容。

注意:邀请令牌是 bearer 凭证。服务端必须只存 SHA-256 摘要、只在创建调用中返回原始令牌、在接受时对路径令牌做哈希后再查表。已接受与已撤销的令牌不能重用。

22.4.4 canEdit 与 deleteProtected

canEdit 由服务端按调用者计算,出现在已认证的工程、列表、创建与更新响应中:个人所有者、组织管理员、活跃的组织创建者、以及 sharedUpdate 为 true 的目标组成员为 true。客户端必须用这个值,而不是自己按角色推导。匿名响应不含该字段。

deleteProtected 是所有者级的「禁止删除」开关,默认为 false,在每个工程表示中都存在(含匿名响应)。为 true 时 DELETE /api/projects/{id} 被拒绝并返回稳定短语 delete-protected,用 PATCH 传 {"deleteProtected": false} 才能解锁删除。

22.5 企业登录:OIDC、SCIM 与可信头代理

22.5.1 组织安全策略

组织管理员可以给成员设置安全策略:

字段 允许范围 作用
idleTimeoutSeconds 300–2592000 空闲多久撤销会话
absoluteSessionSeconds 900–31536000 会话绝对寿命
adminReauthSeconds 60–86400 管理员敏感操作的重认证窗口
passwordMinLength 8–128 密码最小长度
passwordMinClasses 1–4 必须包含的字符类别数
passwordMaxAgeDays 1–3650 密码最长使用天数
lockoutThreshold 3–100 连续错误多少次锁定
lockoutSeconds 60–86400 锁定时长
adminIpAllowlist 最多 50 条 IP 或 CIDR 管理员来源限制

PUT /api/organizations/{id}/security-policy 整体替换策略(省略字段即置为 null)。账号属于多个组织时取最严:最短空闲超时、最短绝对寿命、最短密码年龄、最低锁定阈值;最长最小长度、最多字符类别、最长锁定时长。密码长度永远不会低于 8。

lockoutThreshold 与 lockoutSeconds 必须成对设置;adminIpAllowlist 必须包含管理员当前地址,否则返回 adminIpAllowlist must include your current address——防止把自己锁在门外。

客户端地址的判定:默认取直接对端地址;若对端列在 GEOLIBRE_TRUSTED_PROXIES(逗号分隔的 IP 或 CIDR)中,则取 X-Forwarded-For 最右侧的非可信条目。

管理员重认证命中时会返回 401 {"error":"reauthentication_required"},并带 RFC 9470 的 WWW-Authenticate: Bearer error="insufficient_user_authentication", max_age="<seconds>"。读取操作不受影响。

22.5.2 OIDC 单点登录

组织管理员可以给组织连接一个 OpenID Connect 提供方(Entra ID、Okta、Google、Keycloak、ADFS 2016+ 等)。

PUT /api/organizations/{id}/identity-provider 的请求体是一个 JSON 对象,字段包括 issuer、clientId、clientSecret、tokenEndpointAuthMethod、scopes、三个 claim 名、defaultRole、roleMappings、groupMappings、requireMfa、allowBuiltinAccounts、breakGlassUsername 与 enabled。下面只列容易踩坑的规则。

关键字段规则:

字段 规则
issuer 必填,≤512 字符,https:// 且无 query/fragment;与 ID Token 的 iss 精确比较,从不规范化
clientSecret 创建时必填;更新时省略或 null 表示保留原值;永不返回
authorizationEndpoint / tokenEndpoint / jwksUri 三个全给或全不给;全不给则读 issuer 的发现文档
scopes ≤20 个简单 scope,必须含 openid
roleMappings / groupMappings 各 ≤100 项;组映射的目标组必须属于本组织
requireMfa 要求 ID Token 的 amr 含 mfa;只列具体因子的提供方需要增加 claim 映射
allowBuiltinAccounts false 时该组织成员禁用密码登录
breakGlassUsername 保留密码登录的在职管理员;allowBuiltinAccounts 为 false 时必填

GET 返回同样的设置(scopes 为数组)外加 protocol("oidc")、实际存储的端点、clientSecretSet: true、redirectUri 与 updatedAt。redirectUri 形如 <issuer>/oauth/sso/callback(未配置 OAuth 时为 null),需要在身份提供方注册为机密客户端,使用授权码 + S256 PKCE。

未显式给出端点时,PUT 会抓取 <issuer>/.well-known/openid-configuration:其 issuer 必须与配置完全一致,且必须给出三个 https:// 端点;网络错误、非 200、超过 1 MiB、非法 JSON 都算发现失败。签名密钥在首次登录时从 jwksUri 拉取并缓存,改 issuer 或 jwksUri 会清缓存。一旦已有联邦身份关联,PUT 就不能再改这两个字段(409),以防止新的权威用同一 sub 解析到既有账号。

警告(SSRF):任何登录用户都能建组织并指定提供方 URL,所以服务端只连接公网地址。解析出的回环、私有、链路本地、CGNAT(100.64.0.0/10)、组播、保留与未指定地址会被丢弃(含其 IPv4-mapped、NAT64、6to4、Teredo 形式)。要在内网使用 IdP,必须把网段列进 GEOLIBRE_OIDC_ALLOWED_NETWORKS(非法条目会导致启动失败)。

账户派生规则:提供方 subject(sub)首次登录时创建关联账户;用户名取自 usernameClaim(否则 emailClaim),小写、在 `` 的 @ 处截断、其它字符替换为 -,重名时加 -2 至 -99 后缀;都不行时账号没有用户名(对应前文的 username required 哨兵)。邮箱只在 email_verified 为 true 且无其它账号占用时写入。登录永不按邮箱合并到既有账号,所以老成员切换到 SSO 会得到新账号——迁移要提前规划。

requireMfa 打开时,提供方必须在 amr 里给出 mfa,只列 pwd、otp 之类单因子的 IdP 需要补 claim 映射,否则每次登录都会被拒。联邦账户没有密码。

22.5.3 SCIM 2.0 用户与组同步

组织的身份提供方可以通过 SCIM 2.0(RFC 7643/7644)自动开通用户与组。管理员先签发令牌:

端点 说明
POST /api/organizations/{id}/scim-tokens body {"label": "Entra ID"}(1–100 字符),返回 201,含原始 token、令牌对象与 baseUrl(<public URL>/scim/v2/<organization id>)
GET /api/organizations/{id}/scim-tokens 列出令牌,最新在前
DELETE /api/organizations/{id}/scim-tokens/{tokenId} 撤销令牌,返回 204

令牌对象是 {"id", "label", "createdAt", "lastUsedAt", "revokedAt"}(lastUsedAt 最多每分钟更新一次)。

SCIM 端点:

端点 备注
GET /ServiceProviderConfig、/ResourceTypes、/Schemas 发现;支持 PATCH 与过滤(最多 100 条结果),不支持 bulk、排序、ETag 与改密
GET /Users startIndex(默认 1)、count(0–100,默认 100);filter 仅支持 userName eq "…"(大小写不敏感)或 externalId eq "…"
POST /Users 创建账号,或在启用中的 IdP 有唯一匹配时「认领」既有 SSO 账号;userName 1–255、存为小写、组织内唯一
GET/PUT/PATCH/DELETE /Users/{id} id 是账号 ID;PUT 替换 userName、externalId、displayName、emails 与(存在时)active
GET/POST /Groups 组的过滤与创建;新组由令牌创建者拥有,join_policy 为 invite
GET/PUT/PATCH/DELETE /Groups/{id} PUT 替换 displayName、externalId 与成员集合

SCIM 请求体是 application/scim+json(或 application/json),响应为 application/scim+json。错误体形如:

{"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "409", "detail": "userName already exists", "scimType": "uniqueness"}

注意:SCIM 是机器对机器调用,所以组织的管理员 IP 白名单与重认证窗口不适用于 SCIM 请求;要切断某个提供方,撤销令牌即可。另外,DELETE 用户是「停用」:撤销其 OAuth 会话与个人令牌,但保留联邦身份;同一用户名再次开通会重新激活该账号,且不恢复旧凭证。

22.5.4 可信头代理登录

在身份感知代理(oauth2-proxy、Authelia、Cloudflare Access 等)之后,可以不再单独登录项目服务器:

  1. 设 GEOLIBRE_PROXY_AUTH=true(或 1 / yes),并把 GEOLIBRE_TRUSTED_PROXIES 设为代理连到项目服务器时使用的源地址(IP 或 CIDR 列表,例如只有代理与服务器所在的 Docker 网络)。
  2. 让项目服务器只对代理可达。docker-compose.yml 默认把它发布在 127.0.0.1;若代理在同一 Compose 网络里,可以干脆去掉端口发布。
  3. 代理必须剥离客户端自带的身份头,否则任何能到达未被覆盖的路由的人都能冒充任意用户。

同意页会显示 Signed in through your organization's proxy as <user>,只问设备标签;首次登录创建账号。默认头名是 Remote-User 与 Remote-Email,oauth2-proxy 发送的是 X-Forwarded-User / X-Forwarded-Email,需要用 GEOLIBRE_PROXY_USER_HEADER / GEOLIBRE_PROXY_EMAIL_HEADER 改。只有在 GEOLIBRE_PROXY_AUTH 打开时才会读取身份头;单独设置 GEOLIBRE_TRUSTED_PROXIES 只表示信任 X-Forwarded-For。

Caddy 里「先剥离、再转发认证」的最小写法(来自 docs/self-hosting.md):

projects.example.org {
    # Caddy 保证 request_header 在 handle 之前执行
    request_header -Remote-User
    request_header -Remote-Email

    # 只有同意页走 forward auth
    handle /oauth/authorize {
        forward_auth authelia:9091 {
            uri /api/verify?rd=https://auth.example.org
            copy_headers Remote-User Remote-Email
        }
        reverse_proxy geolibre-server:8000
    }

    handle /oauth/* {
        reverse_proxy geolibre-server:8000
    }

    handle /api/* {
        reverse_proxy geolibre-server:8000
    }
}

提示:只有同意页走 forward auth。客户端会跨域用 bearer 令牌调用 token 端点与 API,如果这些路由也被重定向到登录页,OAuth 流程会直接坏掉。

22.6 项目 API:创建、版本、分叉、分享与活动

22.6.1 工程表示

{
  "id": "uuid",
  "username": "ada",
  "slug": "wetlands",
  "visibility": "public",
  "canEdit": true,
  "organization": {"slug": "watershed-lab", "name": "Watershed Lab"},
  "deleteProtected": false,
  "rawJsonUrl": "https://example.org/ada/wetlands.geolibre.json",
  "projectUrl": "https://example.org/ada/wetlands",
  "viewerUrl": "https://example.org/?project=https%3A%2F%2Fexample.org%2Fada%2Fwetlands.geolibre.json"
}

此外还有 description、groupIds、thumbnailUrl、views、forkCount、versionCount、featured、role、expiresAt、hasPassword、createdAt、updatedAt 与 tags。

organization 在组织拥有时非空(与可见性无关);groupIds 是共享目标组,没有则为空数组;canEdit 见 22.4.4 节。

22.6.2 创建与分享链接

POST /api/projects 需要认证,创建工程与它的第一个不可变版本:

{
  "filename": "Wetlands.geolibre.json",
  "content": "{\"version\":\"1.0\", ...}",
  "visibility": "public",
  "organizationId": "org-uuid",
  "groupIds": ["group-uuid-1", "group-uuid-2"]
}

content 是一段字符串,内容是合法的 GeoLibre 工程 JSON;filename 只提供标题/slug 兜底,工程文档里非空的 title 才是权威。visibility 可省略:组织工程用组织的 defaultVisibility,个人工程默认 private。可见性为 organization 时必须给 organizationId。

仅对 public / unlisted 有效的分享链接设置:role(view / comment / edit,默认 edit)、expiresIn(24h / 7d / 30d / never)、password。其它可见性一旦设置其中之一就返回 422。它们在工程表示里回显为 role、expiresAt、hasPassword。

  • 过期后,除工程管理者外所有读路由返回 410 share link expired。
  • 有密码时,读者在 POST /{username}/{slug}/access(组织工程为 POST /org/{organization}/{slug}/access)用 {"password":"…"} 解锁,返回 {"content":"<工程 JSON>","role":"view"}。
  • 同一客户端地址 5 分钟内连续 10 次错误密码后,该工程返回 429——即便之后输对也一样(计数在内存中,按服务进程)。
  • 版本列表路由不接收密码,因此在设了密码时对非管理者返回 401。

22.6.3 列出与查询

GET /api/projects 按最近更新倒序返回一页:

{"projects": [], "limit": 24, "offset": 0, "total": 0}

查询参数:

参数 含义
limit / offset 分页
mine=true 返回自己的工程(含 unlisted、private);需要认证
shared_with_me=true 返回可见的组织工程、组织公有工程、可管理的组织私有/unlisted 工程、显式共享到本人组的工程;不能与 mine 同时使用
shared_source=organizations|groups 与上一项联用,在分页与计数之前收窄范围;单独使用返回 422

只有设置 mine 或 shared_with_me 时才会返回非 public 工程。仅带 Authorization 头不会扩大公开列表。

GET /api/users/{username}/projects 返回某用户的工程;令牌身份就是该用户时才包含 unlisted/private,其他人只看到 public。非所有者得到的是收窄的 200 而不是 403,这样就不能用状态码探测用户是否存在。该路由接受 limit(1–100,默认 24)与 offset(默认 0)。

22.6.4 版本、更新与删除

端点 行为
GET /api/projects/{id}/versions 需要认证与读权限;返回 {"versions":[{"number":3,"createdAt":"…","url":"…"}]},最新在前
GET /api/projects/{id}/versions/{version} 返回该版本的不可变工程文档
PATCH /api/projects/{id} 接受 title、description、visibility、tags、organizationId、groupIds、deleteProtected
PUT /api/projects/{id}/content 新建一个不可变版本;body {"content":"…","expectedVersion":3}
DELETE /api/projects/{id} 需要所有权;删除元数据与对象;204

expectedVersion 是乐观锁的提示而非强制:不匹配时写入仍然成功(last-write-wins),但 201 响应会带一个包含稳定短语 version conflict 的 warning 字段。客户端应当把 warning 展示给用户。

对 visibility、organizationId、deleteProtected 这类非空字段显式传 null 会被 422 直接拒绝,而不是在提交阶段失败。删除工程会同时移除它的待处理传输与重定向行。

22.6.5 分享、活动、分叉与原始路由

端点 说明
GET /api/shares 需要 read:projects;列出自己管理且可见性为 public/unlisted 的工程(organization 不算链接分享),每条额外带 projectSlug;过期链接仍列出以便撤销
DELETE /api/shares/{id} 撤销分享:工程变回 private,role、过期与密码重置;工程、版本与组共享保留
GET /api/projects/{id}/activity 需要所有权;返回最多 100 条活动日志
POST /api/projects/{id}/forks 需要认证;body 可省略(等价于 {"visibility":"private"});源工程 forkCount 原子递增

活动动作与 details:version_save(version)、fork(forked_project_id)、visibility_change(before / after)、transfer(from / to)、原始 JSON 的 fetch(version)与工程页的 open。匿名访客的 open / fetch 从不按访客存储:按动作与 UTC 日期聚合成一行,带 count,不记录 IP 或任何指纹。行会在 GEOLIBRE_ACTIVITY_RETENTION_DAYS(默认 90)之后、下次记录活动时被清理。日志只对所有者可见,永不出现在列表里。

原始与「网站兼容」路由:

  • GET /{username}/{slug}.geolibre.json:最新工程文档,Content-Type: application/json。
  • GET /{username}/{slug}:HTML 工程页或跳转到配置的 viewer;这就是 API 自报的 projectUrl。
  • 组织版为 GET /org/{organizationSlug}/{slug}.geolibre.json 与 GET /org/{organizationSlug}/{slug}。
  • 每次成功读取最新原始文档可能递增 views;失败或未授权读取不得计数。

22.6.6 传输(Transfers)

工程可以移交给另一个用户(需对方接受),或移交给调用者管理的组织(立即生效)。工程保留 id、views、forkCount、版本历史与活动;只有命名空间与 slug 改变。每次传输都会清空工程的组共享(那是对旧受众的授权),并为腾空的地址记录永久重定向。

POST /api/projects/{id}/transfers 需要所有权(组织工程需管理员身份)与 write:projects,body 必须恰好给一个 username 或 organizationId,可选 slug:

例如 {"username": "bob", "slug": "wetlands"}。

状态 error 原因
409 slug already exists for the new owner 目标命名空间已占用该 slug,换一个重试
409 a transfer is already pending for this project 每个工程只允许一个待处理传输
404 user not found 没有该用户名
422 provide exactly one of username or organizationId 两个都给或都没给
422 project already belongs to that owner 目标已是所有者

接收与管理:GET /api/transfers/incoming、GET /api/transfers/outgoing、POST /api/transfers/{id}/accept(可带 {"slug":"…"} 覆盖目标 slug)、POST /api/transfers/{id}/decline、DELETE /api/transfers/{id}。接受时若发起方已不再管理该工程、或另一个请求先解决了该提议,返回 409 transfer is no longer valid。

传输可能改变可见性:public 工程移入 publicSharingPolicy 非 yes 的组织会变成 organization;organization 工程移给个人会变成 private。两者都会记 visibility_change 活动。腾空的地址在重定向存在期间保持保留:之后在同一命名空间上传同名工程会得到 -2 后缀,而不会顶替旧链接。只有调用者本就能看到目标工程时重定向才会被跟随,所以匿名访问腾空的私有地址得到 404 而不是 301。

22.6.7 缩略图

PUT /api/projects/{id}/thumbnail 需要所有权,接受图片字节与对应的图片 content type;GET 跟随工程可见性;DELETE 移除。上传与删除都返回 204。

22.7 OAuth 2.0 PKCE、作用域与兼容性

22.7.1 发现与注册

参考实现为公开客户端实现授权码 + PKCE(只支持 S256),不接受 client secret。注册表由 GEOLIBRE_OAUTH_CLIENTS 精确给出并在启动时校验,唯一受支持的 client id 是 geolibre-web 与 geolibre-desktop。配置为空或未设置时,所有 OAuth 路由关闭,但不影响个人令牌。

签发者(issuer)是 GEOLIBRE_PUBLIC_URL;启用 OAuth 时必须是绝对 HTTPS URL,回环 HTTP 仅允许 localhost 或 127.0.0.1 且带显式端口。请求的 Host 头(含端口)必须与 issuer 的 authority 一致。

发现文档是 GET /.well-known/oauth-authorization-server(RFC 8414)。若 issuer 带路径 /services/projects,路由为 /.well-known/oauth-authorization-server/services/projects。文档公布授权、令牌、撤销端点,授权码与 refresh 授权,S256,三个工程作用域,以及仅 OAuth 可用的 manage:sessions。

22.7.2 授权与同意

GET /oauth/authorize 每项只接受一个值:response_type=code、client_id、精确的 redirect_uri、非空 scope、state、code_challenge、code_challenge_method=S256;device_label 可选。state 为 16–512 个 URL 安全字符;S256 challenge 是 43 字符的无填充 base64url SHA-256。

重复的授权参数、未知客户端、未注册重定向、超过 512 字符的 state 会返回本地错误页而不带 Location;其它授权错误则重定向到已经校验过的回调,带 error、iss 与原始 state。

POST /oauth/authorize 提交服务器自有的同意表单,要求浏览器绑定 cookie、CSRF 值、同源 Origin 或 Referer、以及账号凭证。同意返回 303 到精确回调,带一次性 code、state 与 iss;取消返回 access_denied。授权码默认 60 秒过期(GEOLIBRE_OAUTH_CODE_TTL_SECONDS)。

Web 回调必须是绝对 HTTPS URL 且以 /oauth-callback.html 结尾(开发可用显式端口的回环 HTTP)。桌面端回调必须恰好是 org.geolibre.desktop:/oauth/callback:已安装的 Tauri 应用在系统浏览器中打开同意页,通过操作系统协议处理器接收回调,而不是监听 HTTP 端口。冷启动且没有待处理 verifier 的回调无法完成登录,必须重新发起同意。

22.7.3 令牌交换、轮换与撤销

POST /oauth/token 接受最大 16 KiB 的表单编码请求体:

  • grant_type=authorization_code:需要 client_id、code、redirect_uri 与 43–128 字符的 code_verifier。
  • grant_type=refresh_token:需要 client_id 与 refresh_token;可选 scope 必须与原授权的作用域集合一致(顺序无关)。

成功响应:

成功响应是一个 JSON 对象:access_token、token_type: "Bearer"、expires_in: 600、refresh_token 与空格分隔的 scope。

生命周期由三个变量控制:GEOLIBRE_OAUTH_ACCESS_TTL_SECONDS(默认 600)、GEOLIBRE_OAUTH_REFRESH_TTL_SECONDS(默认 2592000,即 30 天)、GEOLIBRE_OAUTH_CODE_TTL_SECONDS(默认 60)。refresh 令牌一次性使用、每次轮换;重用已消费的 refresh 令牌会撤销整个族,包括成功轮换产生的新令牌。族在「签发时间 + refresh TTL」到期,轮换永不延长。

请求 manage:sessions 单独用于一次性的 step-up 同意。把它与工程作用域混在一起是 invalid_scope。它的成功响应 "scope":"manage:sessions"、"expires_in":300(或更短),且没有 refresh_token;服务端不为该授权建 refresh 行,拒绝刷新请求,并把访问令牌与族都限制在五分钟内。客户端必须只在内存中持有管理令牌,并在会话管理关闭、工程会话变化或授权过期时丢弃。管理请求返回 401 时应重新同意,不能把工程会话登出。

POST /oauth/revoke 接受 client_id、token 与可选 token_type_hint;命中访问或刷新令牌都会撤销整族。未知、已撤销、错客户端的令牌都返回同样的空 200。

注意:原始 code 与令牌只返回一次,数据库只存 SHA-256 摘要。任何诊断日志或持久化工程里都不该出现授权码或令牌。

22.7.4 兼容性承诺

API 在 v1 内只增不改:实现不得改变字段语义或收窄可见性规则;可以新增可选字段与端点;破坏性变更必须开 /api/v2 命名空间。一致性基线是前端 share-geolibre.ts / share-gallery.ts 的测试加参考服务端的 API 测试。

22.8 Docker Compose 部署与卷、健康检查

22.8.1 一条命令起四件套

仓库根目录的 docker-compose.yml 定义了四个服务:

服务 镜像来源 端口 作用
geolibre-web 根 Dockerfile(nginx + 静态构建 + sidecar) 8080:80(GEOLIBRE_WEB_PORT 可改) Web 应用与 /sidecar
geolibre-server backend/geolibre_server_api 127.0.0.1:8000(GEOLIBRE_SERVER_PORT 可改) 项目与身份 API
geolibre-collab workers/collab-node/Dockerfile 8787(GEOLIBRE_COLLAB_PORT 可改) WebSocket 协作中继
postgres postgres:17-alpine 不发布 工程元数据

POSTGRES_PASSWORD 是必填而非默认值:那个账号拥有全部工程元数据,仓库里提交密码意味着每个部署都一样。Compose 会在变量缺失时报错并指名它。

POSTGRES_PASSWORD=choose-a-password docker compose up --build

打开 http://localhost:8080;项目 API 在 http://localhost:8000;中继在 ws://localhost:8787。真实部署前要把 GEOLIBRE_SHARE_URL、GEOLIBRE_COLLAB_URL、GEOLIBRE_VIEWER_URL、GEOLIBRE_CORS_ORIGINS 改成公网 TLS 源(注释里明确写了它们是浏览器可达 URL,不是 Compose 服务名)。

22.8.2 卷与健康检查

卷 挂载点 内容
geolibre-projects /data/objects 工程文档、版本、缩略图对象
geolibre-collab /data collab.sqlite
geolibre-postgres /var/lib/postgresql/data 数据库簇

Compose 里的健康检查原文如下(这是判断「容器起来了但没就绪」的第一手依据):

服务 健康检查 间隔 / 超时 / 重试 / 启动等待
geolibre-server python -c 用 urllib 请求 http://127.0.0.1:8000/health 10s / 5s / 10 / 10s
geolibre-collab node -e 用 fetch 请求 http://127.0.0.1:8787/health 10s / 5s / 10 / 5s
postgres pg_isready -U $POSTGRES_USER -d $POSTGRES_DB 5s / 5s / 10

postgres 用 pg_isready -U $POSTGRES_USER -d $POSTGRES_DB,间隔 5 秒。geolibre-web 通过 depends_on 等待 server 与 collab 达到 service_healthy,但不等 postgres:API 自己等。

GET /health 返回 {"ok": true, "service": "geolibre-server"};中继返回 { ok, service, identitySupported }。

22.8.3 服务端环境变量

变量 默认 说明
GEOLIBRE_DATABASE_URL sqlite:///./geolibre-server-api.db SQLAlchemy URL;Postgres 用 postgresql+psycopg://…
GEOLIBRE_STORAGE_PATH ./data 本地对象目录
GEOLIBRE_STORAGE filesystem 设为 s3 时用 S3 兼容存储(GEOLIBRE_S3_BUCKET 必填,可选 GEOLIBRE_S3_ENDPOINT / GEOLIBRE_S3_REGION)
GEOLIBRE_PUBLIC_URL http://localhost:8000 对外可达的 API URL,也是 OAuth issuer
GEOLIBRE_VIEWER_URL https://app.geolibre.org/ 用于拼 viewerUrl
GEOLIBRE_CORS_ORIGINS * 逗号分隔的 Web 源
GEOLIBRE_TRUSTED_PROXIES 空 信任其 X-Forwarded-For 的代理地址/CIDR
GEOLIBRE_PROXY_AUTH 空 允许可信代理用身份头登录同意页
GEOLIBRE_OIDC_CA_BUNDLE 空 额外信任的私有 CA(PEM)
GEOLIBRE_OIDC_ALLOWED_NETWORKS 空 允许 IdP 使用的内网网段
GEOLIBRE_HOST / GEOLIBRE_PORT 0.0.0.0 / 8000 绑定地址与端口;有反代时绑 127.0.0.1
GEOLIBRE_MAX_PROJECT_BYTES / GEOLIBRE_MAX_THUMBNAIL_BYTES 50 MiB / 5 MiB 上传上限

22.8.4 PostgreSQL 密码里的两个字符

POSTGRES_USER 与 POSTGRES_PASSWORD 是逐字代入连接串的,两个字符会咬人:

  • `` 的 at 符会提前切断 URL:p@ssw0rd 被解析成「密码 p、主机 ssw0rd@postgres」,随后 API 在一条从不提密码的 psycopg 错误上重启循环。
  • 百分号会启动转义:we%20ird 静默变成 we ird,于是用了一个你从未设置的密码去认证。

要么避开这两个字符,要么在 URL 里转义(%40 / %25),同时让 POSTGRES_PASSWORD 保持字面值。另外,Postgres 只在初始化数据目录时应用该密码,之后改密码不会改既有卷上的密码:要么在库里 ALTER USER,要么重建卷。

22.8.5 卷属主修复

服务端容器以非特权用户 geolibre(uid/gid 1000)运行,镜像里预先创建了 /data/objects,因此新建的命名卷会继承该属主。Docker 只对「它自己创建」的卷套用镜像属主;如果卷里已经有早期 root 镜像写入的数据,它会保持 root 所有,于是每次上传都 PermissionError。修复一次即可:

docker run --rm -v geolibre_geolibre-projects:/data/objects busybox \
  chown -R 1000:1000 /data/objects

中继同理(它跑非特权 node 用户,卷挂在 /data),症状是启动即 SQLITE_READONLY: attempt to write a readonly database:

docker run --rm -v geolibre_geolibre-collab:/data busybox chown -R 1000:1000 /data

22.8.6 启用服务端 OAuth

服务端的 OAuth 端点在 GEOLIBRE_OAUTH_CLIENTS 含精确注册前是关闭的。本地 Web 部署示例:

export GEOLIBRE_OAUTH_CLIENTS='[{"client_id":"geolibre-web","name":"GeoLibre Web","redirect_uris":["http://localhost:8080/oauth-callback.html"],"scopes":["read:projects","write:projects","share:public"]}]'
POSTGRES_PASSWORD=choose-a-password docker compose up --build

生产 Web 回调必须 HTTPS 且以 /oauth-callback.html 结尾,且必须匹配 Web 应用自身源(含 base path)。桌面端是 geolibre-desktop 注册项加 org.geolibre.desktop:/oauth/callback。Web 端通过弹窗打开服务端同意页,桌面端通过系统浏览器 + 协议处理器;个人 API 令牌仍是兜底方案。

警告:geolibre-server 的宿主端口默认只绑 127.0.0.1,就是因为它前面必须有一个限流代理。不要改成 0.0.0.0,也不要直接发布容器端口。中继的 8787 方便起见发布了;只允许 Web 容器从外部访问时,用 override 文件把它也绑到回环:

# docker-compose.override.yml
services:
  geolibre-collab:
    ports: ["127.0.0.1:8787:8787"]

22.9 反向代理、TLS 与容器策略强制

22.9.1 Web 镜像与路径部署

geolibre-web 由 nginx 提供静态构建,并在能力允许时反代内置 sidecar:

docker run -d --name geolibre -p 8081:80 \
  -e GEOLIBRE_SHARE_URL=off \
  ghcr.io/opengeos/geolibre:latest

路径部署(例如 /gis)要在构建期把 base path 烘进去,并在代理里剥掉前缀:

docker build --build-arg GEOLIBRE_APP_BASE=/gis/ -t geolibre-private .

22.9.2 容器路由策略(不是通用鉴权)

容器用最终部署策略(含环境变量覆盖)在 nginx 上守住这些路由族,每行是「任一能力即可」:

sidecar 路由前缀 需要的能力(任选其一)
/sidecar/whitebox、/sidecar/raster、/sidecar/vector processing:run
/sidecar/pointcloud、/sidecar/ml、/sidecar/sql processing:run
/sidecar/postgis、/sidecar/mssql data:add
/sidecar/conversion processing:run 或 data:add

被拒绝的路由在到达 sidecar 之前返回 403 与带 detail 的 application/json 体。省略 capabilities 表示授予全部能力;若最终授权既没有 processing:run 也没有 data:add,uvicorn 根本不会启动,所有 /sidecar/ 请求返回同样的 JSON 403。GEOLIBRE_DISABLE_SIDECAR=1 无论授权如何都产生同样效果。

uvicorn 运行时,/health、/algorithms、/shutdown、/run、/docs、/redoc、/openapi.json 不受能力守卫约束。这是路由族策略,不是按方法的授权;守卫使用 nginx 规范化后的 URI(含重复斜杠合并与百分号解码前缀)。

/ai 也由最终策略控制:ai.enabled: false 或缺失即禁用;启用需要 GEOLIBRE_AI_URL=/ai、GEOLIBRE_AI_PROXY_URL=https://<approved-origin>、GEOLIBRE_AI_PROXY_TOKEN=<instance-token> 三者齐备,否则启动失败。

警告:容器守卫不是通用授权。它们只覆盖自带容器的 nginx 路由,不约束浏览器 WASM 引擎、桌面端处理、或另行暴露的服务。sidecar 与 AI 上游必须留在 nginx 之后、保持文件系统限制,并用 Basic Auth 或真正的认证代理做用户认证。

22.9.3 把两套东西放在同一个认证层后面

单一口令用 Basic Auth 就够(GEOLIBRE_AUTH_USER / GEOLIBRE_AUTH_PASSWORD,nginx 同时保护应用与 /sidecar;/healthz 保持免认证以便健康检查)。要账号或 SSO,就把 oauth2-proxy、Authelia 或 Cloudflare Access 放到未修改的镜像前面,并把两个应用挂在同一个主机名下:

示例(Caddy):在 maps.example.org 上先 request_header -Remote-User 剥离客户端身份头,再用 forward_auth 指向 Authelia 的 /api/verify 并复制 Remote-User / Remote-Groups / Remote-Email;handle_path /gis/* 转发到 geolibre:80,handle_path /projects/* 用 file_server 提供静态工程,其余兜底转发到 GeoLens。

于是 https://maps.example.org/gis/?url=https://maps.example.org/projects/watershed.geolibre.json 对已认证用户打开私有工程,对其他人返回登录跳转。没有 CORS 头,没有 URL 里的令牌。

注意:url= 必须是绝对 http(s) URL,?url=/projects/watershed.geolibre.json 会被忽略;写成完整 URL 仍然是同源,session cookie 照样会带。

CSP 方面:容器允许 https: 数据与瓦片主机,但阻止明文 http:// 远程数据,以及除已配置协作服务器之外的 wss:// 主机。docker/nginx.conf 为了本地开发允许 http://localhost:* 与 http://127.0.0.1:* 进入 connect-src——在公网主机上请删掉,否则被服务的 JavaScript 可以探测每个访客的回环接口。

22.10 自托管两种模式与 GeoLens 私有数据

22.10.1 两种部署模式

属性 模式 A:GeoLibre 与数据同源(推荐) 模式 B:托管版 GeoLibre + 别处的私有数据
session cookie 能否到达数据 能 不能
需要 CORS 不需要 数据主机必须显式允许 https://web.geolibre.app
私有 ?url= 工程 可用 不可用(登录页或重定向)
私有瓦片、COG、GeoParquet、FlatGeobuf 可用 需签名 URL
数据或令牌流向第三方 无 应用代码来自第三方源
成本 多维护一个容器 无

模式 B 适合半公开数据(带 capability URL 的已发布地图),不适合敏感数据。存疑时选 A:Web 构建只是一堆静态文件,托管成本很低。

22.10.2 为什么同源如此关键

GeoLibre 的一切取数都由浏览器发起,于是浏览器规则就是系统规则:

  1. Cookie。请求使用浏览器默认凭据模式(same-origin)。SSO 层的会话 cookie 只有在数据 URL 与应用同源时才自动发送;跨源时无论服务端怎么配都不会带。同源是必要但非充分条件:cookie 自己的 Path 还必须覆盖被请求的 URL,把应用、API、工程文件摊在兄弟路径下时用 Path=/。
  2. CORS。同源请求完全不需要 CORS 头;跨源必须由数据主机显式允许 GeoLibre 的来源。
  3. 瓦片请求。MapLibre 自己发栅格与矢量瓦片请求,遵循同一套 cookie 与 CORS 规则——这就是「用会话 cookie 保护的瓦片端点只在同源时可用」的原因。
  4. ?url= 工程。深链接加载的 .geolibre.json 同理:同源时能加载,跨源时以网络/CORS 错误失败。
  5. CSP。镜像允许 https: 与本地回环,所以自托管数据服务必须走 HTTPS(明文 http:// 仅在 localhost / 127.0.0.1 可用)。桌面端额外允许任意主机的明文 http:,因此能连局域网里的 Ollama、SamGeo 等。

22.10.3 用 GeoLens 承载数据

GeoLens 是开源、自托管的空间数据目录与地图构建器(FastAPI + PostGIS + React + MapLibre),补的正是 GeoLibre 刻意不做的那部分。安装:

curl -fsSL https://getgeolens.com/install.sh | sh

至少要在它的 .env 里设好:

变量 为什么重要
PUBLIC_APP_URL / PUBLIC_API_URL 浏览器可见 URL,也是填进插件面板的值,必须是公网 HTTPS 而非 Compose 服务名
CORS_ALLOWED_ORIGINS 只有 GeoLibre 在不同源时才需要,精确列出那个源
ENVIRONMENT=production 隐藏 API 文档端点并加固 OAuth cookie
JWT_SECRET_KEY 会话签名密钥

GeoLens 提供什么、GeoLibre 怎么消费:

GeoLens 提供 GeoLibre 消费方式
数据集搜索(模糊 + 可选 pgvector 语义排序) GeoLens 插件面板里的目录列表
签名的 XYZ 矢量瓦片(MVT) 矢量瓦片图层,令牌自动刷新
OGC API Features(/api/collections/{id}/items) 带完整属性的 GeoJSON 图层
STAC 1.0 与服务端渲染的栅格瓦片 栅格图层
按数据集权限、账号、OAuth 2.0 / OIDC SSO、按用户 API key 决定插件能看到什么的身份
Shapefile、GeoPackage、GeoJSON、GeoParquet、CSV、XLSX、GeoTIFF/COG 上传 不必手工转换

22.10.4 在 GeoLibre 里连接 GeoLens

GeoLens 插件是内置的:打开 Plugins 菜单选 GeoLens,无需安装、无需市场步骤。面板里:

  1. 从列表选服务器,或直接填自己的 base URL,例如 https://maps.example.org。给服务器根而不是 /api——插件自己会拼 /api/...;末尾斜杠会被去掉,裸主机名按 https:// 解释。
  2. API key 留空可看公开数据集;粘贴 key 才能看私有数据集。同源部署时请求还会带上访客的 GeoLens 会话 cookie,所以若部署按会话授权 API,不填 key 也能看到私有数据。
  3. 搜目录并添加数据集。矢量数据集可以作为矢量瓦片或经 OGC API Features 作为 GeoJSON(完整属性、可编辑、受面板里的要素上限约束);栅格数据集作为服务端渲染的栅格瓦片。
  4. 每条结果都有 Metadata 链接回到 GeoLens 里的详情页。

加进地图后它就是普通 GeoLibre 图层:可样式化、分级、打开属性表、跑处理工具与空间 SQL、发布到故事地图或嵌入地图。

写回:以 GeoJSON 添加的数据集可以用 GeoEditor 重绘、在属性表里改值,插件的 Edits 区会把改动逐要素写回 GeoLens。这需要凭证且服务器开启了数据集编辑(enable_dataset_editing,在连接时从 /api/settings/feature-flags/ 读取)。功能关闭时插件直接显示「不可保存」,而不是提供一个会中途失败的写操作。

凭证处理(数据主权关键点):

  • API key 只在会话内存中持有,不写 localStorage,也不写进工程文件。
  • 保存的 .geolibre.json 只记录每个图层的 GeoLens 服务器 URL 与数据集 id;把工程发给别人不会把数据或密钥发出去。
  • 重新打开工程时,公开数据集的图层会自动重签瓦片令牌并渲染;私有数据集的图层保持空白,直到你在面板里用有权限的 key 重连。
  • 签名矢量瓦片令牌是按数据集铸造的短命 HMAC 令牌,插件在过期前刷新。
  • 私有栅格的 API key 只绑到该栅格的瓦片 URL 前缀上,别的什么都不带。
  • 停用插件会丢弃所有已注册的 key,私有图层随即停止渲染——凭证不会活得比插件久。

22.10.5 减少对外请求与上线检查单

功能 默认 收进内网的做法
底图 OpenFreeMap / CARTO 瓦片 Basemaps 插件的 custom style URL,自托管样式与 PMTiles 底图;或空白背景
地理编码 公共 Nominatim 指向自建 Nominatim 或 Pelias
路径规划与等时圈 公共 FOSSGIS Valhalla 设 VITE_ROUTING_ENDPOINT 指向自己的 Valhalla
指针高程读数 公共 Open-Meteo(3D 地形无采样时) 关闭读数(默认关),或拒绝首次远程查询前的同意提示
Python(Pyodide)矢量引擎 从 jsDelivr 加载 设 VITE_PYODIDE_INDEX_URL 指向镜像
工程分享 share.geolibre.app GEOLIBRE_SHARE_URL=off,或自己的项目服务器
遥测 无 Docker 镜像无法开启:Dockerfile 没有对应构建参数,CSP 也不允许 googletagmanager.com

私有部署检查单:

  • GeoLens(或自己的数据主机)跑在社区 VM 上,数据集默认私有。
  • GeoLibre Web 构建从同一源提供,例如 /gis,并用匹配的 GEOLIBRE_APP_BASE 构建。
  • 一个终止 TLS 的反向代理,把 SSO 层放在两者之前。
  • GEOLIBRE_SHARE_URL=off(或指向自己的服务器),避免误发布到外部。
  • 审查生成的公开 /deployment.json;实际访问被拒的 sidecar 路由,确认返回 403。
  • GEOLIBRE_CONVERSION_ROOTS 受限;不需要时 GEOLIBRE_POSTGIS_HOSTS 与 GEOLIBRE_MSSQL_HOSTS 留空;不用 sidecar 就关闭。
  • 从 CSP 里删掉回环 connect-src 白名单。

22.11 部署策略 JSON 与 UI Profiles

22.11.1 加载与优先级

deployment.json 是一个带版本号的单一文件,描述部署对客户端暴露的设置:能力、界面元素、可加载插件、策展服务库、分享端点与品牌。

形态 读取位置 失败行为
Web / Jupyter 首屏渲染前抓 <base>/deployment.json 404、HTML 回退页、非 JSON、未知 version、抓取超过 3 秒 → 视为无策略,改用下一优先级来源
Docker entrypoint 校验挂载源、按字段应用非空环境覆盖、每次启动写出 /usr/share/nginx/html/deployment.json 源文件校验失败直接停止启动
桌面 先读配置目录文件 仅「文件不存在」或「读取失败」才回退到 Web URL

优先级从高到低:最终策略 → 运行时 window.__GEOLIBRE_DEPLOYMENT_ENV__ → 构建期环境。桌面端配置目录文件与 Web 文件从不合并;桌面文件存在即权威,即使为空、格式错误或版本不支持。每个构建都带一个内容为 null 的桩文件,表示无策略,这样静态主机会正常应答而不是刷 404。

{"version": 1, "capabilities": ["project:edit", "data:add", "processing:run", "export:data"], "interface": {"level": "intermediate", "lock": true}, "plugins": {"allowed": ["acme-tools"], "sideload": false}, "sharing": {"shareUrl": "https://projects.example.com", "collabUrl": "wss://relay.example.com"}, "geolens": {"url": "same-origin"}, "branding": {"appName": "Acme Maps"}}

顶层只有 version 是必需的(且必须是 1,其它值整份忽略);每个 section 都可选,缺失表示「本文件不指定」,由下一级接手。在编辑器里加 "$schema": "https://raw.githubusercontent.com/opengeos/GeoLibre/main/schema/deployment.schema.json" 可获得补全与校验,GeoLibre 会忽略 $schema。

Section 关键字段与含义
capabilities 六个能力名;省略=全部,[]=一个都不给
interface enabled / level / lock + 四个 hidden*;非空对象整体替换旧 admin-profile.json,字段不合并
plugins registryUrl、allowed、blocked、sideload(默认 true)、defaultActive
services builtins(false 隐藏内置入门服务)与 catalog;条目需要 id、name、kind(wms / wfs / wmts / xyz / arcgis / csw)与非空 fields
sharing shareUrl(https://… 或 off)、collabUrl(wss://…)、embedOrigins(https://host 或 *;[] 关闭嵌入 API)
geolens url:https://…、same-origin 或 off
ai enabled(默认 false,指向同源 /ai)、model
branding appName(≤60 字符)、welcome: false 跳过首次启动向导

22.11.2 「省略」与「空数组」相反

对 capabilities 与 plugins.allowed:省略字段 = 本文件不施加限制;写 [] = 一个都不授予 / 一个都不允许。这是部署事故的高发点。

插件优先级是:先看 sideload 许可,再看 blocked,最后看 allowed;blocked 里的 id 即便也在 allowed 中永不加载。public/plugins/ 下随构建烘入的 drop-in 不受 allowed 与 sideload 约束,但仍受 blocked 约束;内置插件不是外部插件,不受这些加载限制管。sideload: false 时,清单 URL、zip、目录等控件消失,程序化安装被拒绝,工程自带的清单 URL 不产生信任提示也无法写入设置;注册表不可用时 fail closed。

警告:插件策略是客户端强制,不是服务端安全边界:改过的客户端可以绕过,它也不提供签名或沙箱。

22.11.3 能力(capabilities)

能力刻意做得「粗」:每一个命名的是一整类动作,而不是单个菜单项。

能力 授予
project:edit New、Open、Open Recent、Import、Project History、Save、Save As、Duplicate、Save as Template、Collaborate、StoryMap;Undo/Redo(菜单项与 Ctrl/Cmd+Z、Ctrl/Cmd+Shift+Z、Ctrl+Y);Export Selection;评审评论;嵌入 API 的 loadProject 与 layout=viewer 的 draw
data:add 整个 Add Data 菜单、把文件拖到地图、嵌入 API 的 addLayer 与 addData
processing:run 整个 Processing 菜单(Whitebox、SQL、Python、AI 助手、地理编码、Model Builder、转换/矢量/栅格)与 openTool
export:data Share、Export HTML、Print、Print Layout、Offline Basemap,以及 exportImage、getLayerFeatures、getDrawnFeatures
plugins:install Plugins 菜单、插件注册的工具栏菜单、内置菜单里的插件项、启用/停用插件、插件市场
settings:manage Settings 对话框与 Style Manager

未被列出的功能都是「非特权」,在任何配置下都可用:平移缩放、View 与 Controls 菜单、图层可见性与排序、identify、选择工具、Help。

docker run --rm -p 8080:80 -e GEOLIBRE_CAPABILITIES=none ghcr.io/opengeos/geolibre:latest
docker run --rm -p 8080:80 -e GEOLIBRE_CAPABILITIES=project:edit,data:add,processing:run,export:data ghcr.io/opengeos/geolibre:latest

GEOLIBRE_CAPABILITIES 用逗号分隔能力名,none 表示一个都不给;JSON 里等价写法是 "capabilities": []。历史构建期输入 VITE_GEOLIBRE_CAPABILITIES 仍被接受,但它只是客户端回退,不配置 nginx 守卫。

22.11.4 UI Profiles 与能力不是一回事

  UI Profiles 部署能力
目的 为受众减少干扰 钉住部署允许做什么
设置者 用户、主策略的 interface、或旧的 admin profile 主运行时策略;历史环境变量回退
应用内可逆 可以(Settings → Interface,除非设了 lock) 不行,永不出现在 UI 里
粒度 单个项目、数据源、插件 整类能力

两者同时生效时先查能力:部署没给的动作,无论 profile 怎么说都不会出现。用户侧默认是 Advanced(全部可见),可在 Settings → Interface 选 Beginner / Intermediate / Advanced,选择立刻生效;手动改任何一项会自动切到 Custom(这是状态而非按钮)。Settings 菜单本身与 Language / Layout / Interface 项永远显示,这样 profile UI 永远不会把自己藏掉。profile 偏好存本地,不随 .geolibre.json 走。

管理员侧的 interface 字段:

字段 类型 含义
level beginner / intermediate / advanced 按条目分级预填隐藏列表
lock boolean 用户不能从 Settings 改;解锁要在下次启动前移除该字段
hiddenDataSources / hiddenPlugins string[] 显式隐藏,存在时覆盖预设
hiddenMenus string[] 顶层菜单 id:project、edit、view、addData、processing、controls、plugins、help
hiddenMenuItems string[] 菜单项 id,例如 processing.raster、help.diagnostics、controls.minimap

数据源 id 来自 apps/geolibre-desktop/src/lib/ui-profile.ts 的目录(如 vector、xyz、mbtiles、postgres);插件 id 来自 packages/plugins/src/plugins/* 与 packages/plugins/src/plugin-ids.ts;菜单与菜单项 id 是该文件里的 TOP_LEVEL_MENUS 与 MENU_ITEM_CATALOG。level 预设生效时,异步加载的外部/drop-in 插件一出现就会被折进隐藏集合。

22.11.5 校验差异与桌面路径

客户端解析器宽松且按 section 工作:某 section 里有任何非法字段或未知键 → 整个 section 被丢弃并打警告,其它 section 仍生效;未知能力名会丢弃整个 capabilities section,于是回退到环境或默认全给(不是什么都不给);未知顶层键忽略并警告一次;非 JSON 或非对象静默忽略。

容器则是严格校验:拒绝未知键、重复或非法 id、trim 后冲突的服务 id,以及超出安全整数范围的数值型 service 字段。非法输入会以一行 ERROR: 指名 JSON 路径并停止启动,这样 nginx 永远不会以比要求更弱的策略启动。文件里写了 ai.enabled: true 但没有 AI 代理三件套也会启动失败。

Docker 环境覆盖:GEOLIBRE_CAPABILITIES → capabilities、GEOLIBRE_SERVICES_FILE → services.catalog、GEOLIBRE_BUILTIN_SERVICES=off → services.builtins: false、GEOLIBRE_SHARE_URL / GEOLIBRE_COLLAB_URL / GEOLIBRE_EMBED_ORIGINS → sharing.*、GEOLIBRE_GEOLENS_URL → geolens.url、GEOLIBRE_APP_NAME → branding.appName、GEOLIBRE_AI_URL / GEOLIBRE_AI_MODEL → ai.enabled / ai.model。每次覆盖都会在启动日志里留一行 Deployment policy: <path> from <VAR> = <value>;令牌永不出现,URL 的 query 或 fragment 被替换为 [redacted]。配置变更在下次容器启动生效。

桌面端策略文件放在 Tauri 的 <app_config_dir>。标准应用标识 org.geolibre.desktop 对应:

系统 路径
Linux $XDG_CONFIG_HOME/org.geolibre.desktop/deployment.json,未设时为 ~/.config/org.geolibre.desktop/deployment.json
macOS ~/Library/Application Support/org.geolibre.desktop/deployment.json
Windows %APPDATA%\org.geolibre.desktop\deployment.json(通常是 C:\Users\<user>\AppData\Roaming\org.geolibre.desktop\deployment.json)

策略(含能力限制)在首屏渲染前应用,无需重新构建,但改完要重启(不监听文件变化)。前导 UTF-8 BOM 可被解析;read_deployment_policy 命令返回原始文本,文件不存在时返回 null。

警告:deployment.json 会发给每个浏览器,绝不要放密钥。AI 代理 URL 与令牌、sidecar 令牌、可信代理、Basic Auth、CSP 都留在服务端环境变量与文件里。桌面端配置目录是用户可写的,那只是「桌面端预设」,不是安全边界。

22.12 嵌入 API 行为、安全清单与运维排障

22.12.1 嵌入 API:拒绝而不是静默失败

被能力拒绝的命令会抛错,这样宿主页面能区分「被拒」与「没效果」:

例如 await map.addData(...) 在缺少 data:add 时抛出 Error: Missing data:add capability,而不是静默返回。

空授权还能压掉 featuresChanged 事件,而 setView、highlightFeature 与图层可见性命令仍然可用。嵌入源白名单(GEOLIBRE_EMBED_ORIGINS)与能力是两件独立的事:白名单决定谁能发命令,能力决定哪些命令存在。前者默认关闭,所以被框架的部署不能被任意嵌入方驱动。

22.12.2 安全清单

类别 检查项
密钥 POSTGRES_PASSWORD 用环境文件或密钥管理器,不用 -e 明文(会进 shell history 与 docker inspect);AI 代理令牌与 sidecar 令牌只存在于服务端;deployment.json 里无秘密
令牌 个人令牌设 1–365 天有效期并定期轮换;吊销离职账号的全部令牌;SCIM 令牌按提供方一对一签发、可单独撤销
网络 geolibre-server 只绑回环或在 Compose 网络内;sidecar 与 AI 上游不直接暴露;GEOLIBRE_CONVERSION_ROOTS 精确指向挂载目录
代理 反向代理剥离客户端自带的身份头;只在 GEOLIBRE_TRUSTED_PROXIES 里列真实代理地址;保留 Host authority
缓存 私有内容必须是 private, no-store;CDN 不要缓存 /api/* 的认证响应
备份 Postgres 卷 + /data/objects + 中继的 collab.sqlite 三者同批备份;只备份数据库会丢工程正文
升级 拉新镜像后重建容器;服务端启动时会幂等补齐 schema 与索引(OAuth 索引在已填充库上会持写锁,先起一个实例再扩容);中继升级前备份 SQLite
日志 代理、WAF、负载均衡日志里抑制授权请求的 query string、回调 Location 头与回调 query string

备份示例(Compose 卷):

三条命令即可:pg_dump 导出元数据;两个 busybox tar 分别打包 geolibre-projects 与 geolibre-collab 卷;恢复时反向执行。

22.12.3 运维排障表

症状 最可能的原因 处理
容器启动即退出,日志有 ERROR: 指名 JSON 路径 deployment.json 严格校验失败 按日志路径修正源文件;环境覆盖修不了非法源
/sidecar/... 一律 403 JSON 最终策略既没给 processing:run 也没给 data:add,或设了 GEOLIBRE_DISABLE_SIDECAR=1 补能力或删变量;先看 /deployment.json 的实际内容
上传工程 PermissionError 卷属主是 root chown -R 1000:1000 修一次
API 重启循环,psycopg 报认证失败但不提密码 密码里含 at 符或百分号 转义或换密码;注意既有卷不会跟着改密码
identity provider discovery failed IdP 在内网被 SSRF 防护拦下,或发现文档不合规 把网段加进 GEOLIBRE_OIDC_ALLOWED_NETWORKS;检查 issuer 是否与 iss 完全一致
SCIM 请求一律 401 令牌缺失/已撤销/属别的组织,或签发者已不是在职管理员 重新签发令牌;SCIM 不受 IP 白名单与重认证限制
管理员被 administrative access is not allowed from this network 拒绝 adminIpAllowlist 不含当前出口地址,或反代地址没进可信代理 修正白名单;把代理地址加入 GEOLIBRE_TRUSTED_PROXIES

22.12.4 上线前的最后十分钟

  1. docker compose ps:四个服务都是 healthy(postgres 以 pg_isready 为准)。
  2. curl -s http://127.0.0.1:8000/health 得到 {"ok": true, ...};curl -s http://127.0.0.1:8787/health 同理。
  3. 浏览器打开 /deployment.json,确认 capabilities 与期望一致,且响应头是 no-store。
  4. 用一个没有权限的账号访问私有工程、/sidecar/whitebox、AI 路由,确认分别得到 404/403。

22.13 本章小结

本章把 GeoLibre 的服务端世界拆成三块,并给出了从契约到部署的完整链路:

  1. 组件边界:本地 sidecar(127.0.0.1:8765,单用户重型算子)≠ 项目与身份 API(8000,多用户账号/组织/分享)≠ 协作中继(8787,WebSocket 快照广播)。默认情况下一个都不需要。
  2. HTTP 契约:camelCase JSON、UTC ISO 8601、Bearer 认证、固定错误码与稳定短语;公共内容 public, no-cache + ETag,私有内容 private, no-store;CORS 只放 Authorization / Content-Type;限额与限流必须运营方补。
  3. 身份:账户与个人令牌(三种作用域、1–365 天)、manage:sessions 独立管理授权、组织与组两套角色、四种可见性、canEdit 由服务端计算。
  4. 企业能力:OIDC(SSRF 防护、发现、claim 映射、break-glass)、SCIM 2.0(令牌、Users/Groups、停用语义)、可信头代理(GEOLIBRE_PROXY_AUTH + 必须剥离客户端身份头)。
  5. 项目 API:版本不可变、内容更新 last-write-wins 并给出 version conflict 警告、分享链接(410/401/429)、分叉、活动聚合、传输与 301 重定向、删除保护。
  6. OAuth 2.0:只支持授权码 + S256 PKCE,两个固定 client id,access 600 秒 / refresh 30 天且轮换不延长族寿命,重用 refresh 撤销整族;manage:sessions 五分钟且无 refresh。
  7. Docker:compose 四服务、三个卷、三条健康检查;POSTGRES_PASSWORD 必填;密码里的 at 符与百分号是坑;卷属主要 1000:1000。
  8. 自托管:同源同时解决 cookie、CORS、瓦片、深链接、CSP 五个问题;GeoLens 补目录与按数据集权限;模式 A 适合敏感数据,模式 B 只适合半公开数据。
  9. 部署策略:deployment.json 的加载优先级、八个 section、能力矩阵、客户端宽松与容器严格的校验差异、桌面配置目录,以及 UI Profiles 与能力的本质区别。
  10. 安全与运维:客户端隐藏不是授权;容器守卫只覆盖自带 nginx 路由;限流、请求体上限、私有缓存、备份与升级顺序都要自己补。

下一章进入源码层面,解释这些能力在 monorepo 里是如何组织与构建的。

22.14 实践练习

  1. 契约阅读:从仓库根目录启动 docker compose up --build,用 curl 依次完成「建账号 → 拿令牌 → 建工程 → 读版本列表 → 分叉」五步,把每一步的状态码与关键响应字段记成表格。思考:哪一步返回 canEdit,为什么匿名响应没有它?
  2. 可见性实验:用两个账号创建同一条工程,分别设为 unlisted 与 private,对比 GET /api/projects(不带 token / 带 token / mine=true)的结果差异,并用 curl -I 记录 Cache-Control 与 ETag。
  3. 能力与策略:写一份只授予 export:data、并隐藏 processing 菜单的 deployment.json,用 GEOLIBRE_DEPLOYMENT_FILE 挂载启动,验证侧车路由返回 403、/deployment.json 内容正确、Processing 菜单消失;再把 capabilities 改成 [] 启动一次,说明两者差别。
  4. 代理登录:用 Caddy 或 nginx 模拟可信代理,设置 GEOLIBRE_PROXY_AUTH=true 与 GEOLIBRE_TRUSTED_PROXIES。先不剥离客户端身份头,演示冒充;再加上 request_header -Remote-User,证明冒充被堵住。
  5. 备份与恢复:按 22.12.2 的三条命令做一次完整备份,在干净目录里重建 Compose 栈并恢复,验证工程正文、缩略图与中继会话都回来了,记录实际耗时与体积。
  6. 限额验证:上传一个超过 GEOLIBRE_MAX_PROJECT_BYTES 的工程,确认返回 413 且错误信息带具体限值;再把限额调小到 1 MiB 复现一次,说明为什么限额要在服务端与代理两侧都设。