第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 运营方必须自己补的两件事
参考实现明确把两项防护留给运维:
- 限流。除 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 绑定会拒绝请求。
- 请求体大小上限。服务端会在读取请求体之前拒绝声明了超大
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¤tSessionId=<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 等)之后,可以不再单独登录项目服务器:
- 设
GEOLIBRE_PROXY_AUTH=true(或1/yes),并把GEOLIBRE_TRUSTED_PROXIES设为代理连到项目服务器时使用的源地址(IP 或 CIDR 列表,例如只有代理与服务器所在的 Docker 网络)。 - 让项目服务器只对代理可达。
docker-compose.yml默认把它发布在127.0.0.1;若代理在同一 Compose 网络里,可以干脆去掉端口发布。 - 代理必须剥离客户端自带的身份头,否则任何能到达未被覆盖的路由的人都能冒充任意用户。
同意页会显示 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 的一切取数都由浏览器发起,于是浏览器规则就是系统规则:
- Cookie。请求使用浏览器默认凭据模式(
same-origin)。SSO 层的会话 cookie 只有在数据 URL 与应用同源时才自动发送;跨源时无论服务端怎么配都不会带。同源是必要但非充分条件:cookie 自己的Path还必须覆盖被请求的 URL,把应用、API、工程文件摊在兄弟路径下时用Path=/。 - CORS。同源请求完全不需要 CORS 头;跨源必须由数据主机显式允许 GeoLibre 的来源。
- 瓦片请求。MapLibre 自己发栅格与矢量瓦片请求,遵循同一套 cookie 与 CORS 规则——这就是「用会话 cookie 保护的瓦片端点只在同源时可用」的原因。
?url=工程。深链接加载的.geolibre.json同理:同源时能加载,跨源时以网络/CORS 错误失败。- 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,无需安装、无需市场步骤。面板里:
- 从列表选服务器,或直接填自己的 base URL,例如
https://maps.example.org。给服务器根而不是/api——插件自己会拼/api/...;末尾斜杠会被去掉,裸主机名按https://解释。 - API key 留空可看公开数据集;粘贴 key 才能看私有数据集。同源部署时请求还会带上访客的 GeoLens 会话 cookie,所以若部署按会话授权 API,不填 key 也能看到私有数据。
- 搜目录并添加数据集。矢量数据集可以作为矢量瓦片或经 OGC API Features 作为 GeoJSON(完整属性、可编辑、受面板里的要素上限约束);栅格数据集作为服务端渲染的栅格瓦片。
- 每条结果都有 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 上线前的最后十分钟
docker compose ps:四个服务都是healthy(postgres 以pg_isready为准)。curl -s http://127.0.0.1:8000/health得到{"ok": true, ...};curl -s http://127.0.0.1:8787/health同理。- 浏览器打开
/deployment.json,确认capabilities与期望一致,且响应头是no-store。 - 用一个没有权限的账号访问私有工程、
/sidecar/whitebox、AI 路由,确认分别得到 404/403。
22.13 本章小结
本章把 GeoLibre 的服务端世界拆成三块,并给出了从契约到部署的完整链路:
- 组件边界:本地 sidecar(127.0.0.1:8765,单用户重型算子)≠ 项目与身份 API(8000,多用户账号/组织/分享)≠ 协作中继(8787,WebSocket 快照广播)。默认情况下一个都不需要。
- HTTP 契约:camelCase JSON、UTC ISO 8601、Bearer 认证、固定错误码与稳定短语;公共内容
public, no-cache+ ETag,私有内容private, no-store;CORS 只放Authorization/Content-Type;限额与限流必须运营方补。 - 身份:账户与个人令牌(三种作用域、1–365 天)、
manage:sessions独立管理授权、组织与组两套角色、四种可见性、canEdit由服务端计算。 - 企业能力:OIDC(SSRF 防护、发现、claim 映射、break-glass)、SCIM 2.0(令牌、Users/Groups、停用语义)、可信头代理(
GEOLIBRE_PROXY_AUTH+ 必须剥离客户端身份头)。 - 项目 API:版本不可变、内容更新 last-write-wins 并给出
version conflict警告、分享链接(410/401/429)、分叉、活动聚合、传输与 301 重定向、删除保护。 - OAuth 2.0:只支持授权码 + S256 PKCE,两个固定 client id,access 600 秒 / refresh 30 天且轮换不延长族寿命,重用 refresh 撤销整族;
manage:sessions五分钟且无 refresh。 - Docker:compose 四服务、三个卷、三条健康检查;
POSTGRES_PASSWORD必填;密码里的 at 符与百分号是坑;卷属主要 1000:1000。 - 自托管:同源同时解决 cookie、CORS、瓦片、深链接、CSP 五个问题;GeoLens 补目录与按数据集权限;模式 A 适合敏感数据,模式 B 只适合半公开数据。
- 部署策略:
deployment.json的加载优先级、八个 section、能力矩阵、客户端宽松与容器严格的校验差异、桌面配置目录,以及 UI Profiles 与能力的本质区别。 - 安全与运维:客户端隐藏不是授权;容器守卫只覆盖自带 nginx 路由;限流、请求体上限、私有缓存、备份与升级顺序都要自己补。
下一章进入源码层面,解释这些能力在 monorepo 里是如何组织与构建的。
22.14 实践练习
- 契约阅读:从仓库根目录启动
docker compose up --build,用curl依次完成「建账号 → 拿令牌 → 建工程 → 读版本列表 → 分叉」五步,把每一步的状态码与关键响应字段记成表格。思考:哪一步返回canEdit,为什么匿名响应没有它? - 可见性实验:用两个账号创建同一条工程,分别设为
unlisted与private,对比GET /api/projects(不带 token / 带 token /mine=true)的结果差异,并用curl -I记录Cache-Control与ETag。 - 能力与策略:写一份只授予
export:data、并隐藏processing菜单的deployment.json,用GEOLIBRE_DEPLOYMENT_FILE挂载启动,验证侧车路由返回 403、/deployment.json内容正确、Processing 菜单消失;再把capabilities改成[]启动一次,说明两者差别。 - 代理登录:用 Caddy 或 nginx 模拟可信代理,设置
GEOLIBRE_PROXY_AUTH=true与GEOLIBRE_TRUSTED_PROXIES。先不剥离客户端身份头,演示冒充;再加上request_header -Remote-User,证明冒充被堵住。 - 备份与恢复:按 22.12.2 的三条命令做一次完整备份,在干净目录里重建 Compose 栈并恢复,验证工程正文、缩略图与中继会话都回来了,记录实际耗时与体积。
- 限额验证:上传一个超过
GEOLIBRE_MAX_PROJECT_BYTES的工程,确认返回413且错误信息带具体限值;再把限额调小到 1 MiB 复现一次,说明为什么限额要在服务端与代理两侧都设。