整套系统在移植阶段采用共享密钥的 JWT 方案:一个服务签发,所有服务用同一个 JWT_SECRET 本地校验。本页直接描述项目实现;需要先理解 token、签名和 Bearer,可阅读 JWT 是什么。
JWT 形态
user_manager 在登录/注册成功后签发 HS256 token,payload 形如:
{ "user_id": 1, "username": "admin", "role": "root", "exp": 1781618346 }
- 算法:HS256,密钥为环境变量
JWT_SECRET。
- 客户端把它放进
Authorization: Bearer <jwt> 发送。
- 其余服务本地解码校验,暂不做服务间内省(introspection)。
所有服务必须配置相同的 JWT_SECRET,否则一个服务签发的 token 在另一个服务会校验失败。scripts/run_all_servers.sh 会自动为六个服务注入同一个密钥。
Token 传播
- 客户端 → 控制器、客户端 → collection_server:携带同一个 token。
- 控制器先本地解码出
{id, username, role},在任何下游调用之前就完成可见性 / 归属 / root 校验(不可见返回 404,无权限返回 403)。
- 控制器 → agent、user_manager:原样转发同一个 Bearer,下游本地解码并信任其角色与入参。
- 控制器 / collection_server → data_server 的受保护内部接口:除用户 Bearer 外,必须额外携带一张短期服务断言(见下节)。
- collection_server 本地解码 Bearer 做鉴权与参数校验,但不再拥有数据库;它转发给 data_server,由后者校验断言并按断言里的
actor_user_id 限定每个查询(用户只能看到自己的收藏)。
服务间断言(X-Rakull-Service-Assertion)
data_server 的内部写接口(自适应学习、Scenario/Journey、文章收藏、收藏夹等)采用双凭证:用户 Bearer 选出”以谁的身份”,服务断言证明”请求确实来自我方允许的对等服务、且只申请白名单能力”。两张凭证故意分离,密钥也是两个:Bearer 用 JWT_SECRET,断言用 CONTROLLER_ASSERTION_SECRET。
断言是一张 HS256 JWT,放在 X-Rakull-Service-Assertion 头,claims 为:
iss / sub:调用方服务标识(immersive_study_server 或 collection_server),二者必须相等且在白名单内;
aud:固定 data_server;
scope:非空字符串数组,每项都必须在 data_server 的能力白名单内,且该调用方有权签发该 scope;
actor_user_id:终端用户 ID(正整数),必须与 Bearer token 的用户一致;
iat / exp:短期有效(有效期不超过 120 秒),拒绝未来签发。
收藏域的两个 scope collections:data:read / collections:data:write 只允许 collection_server 签发。断言缺失、过期、scope 越权或 caller 不匹配一律返回 403。详见 collection_server。
| 角色 | 能力 |
|---|
user | 普通用户:管理自己的文章/收藏,阅读可见文章 |
root | 管理员:用户管理、邀请码、注册审批、查看全部文章 |
管理员(root)在初始化时直接写入数据库(user_manager 的 app/db/seed.py,幂等);其余注册都进入待审批队列,由 root 审批 —— 不再有“第一个注册用户自动成为 root”的捷径。
文章可见性
控制器在读取文章时套用与原单体一致的可见性规则:
| 情况 | 是否可见 |
|---|
| 自己是作者(owner) | 是 |
is_public = 1 | 对已登录用户可见 |
guest_visible = 1 | 对匿名访客也可见 |
| 其他 | 否(返回 404,不泄露存在性) |
公开权限与“申请公开”
- 用户上传的文章默认私有(
is_public = 0)。
- 只有管理员(
root)可以更改可见性:PATCH /api/articles/{id}/visibility 现在要求 root。
- 普通作者只能申请公开:
POST /api/articles/{id}/publish-request(校验调用方为作者且文章当前私有),请求以 publish_request 类别写入 feedback 表。
- 管理员在「Publish requests」中审批:通过
POST /api/admin/publish-requests/{id}/approve(置为公开并清除该文章的请求)或 POST /api/admin/publish-requests/{id}/reject(仅清除请求)。
匿名访问
- 没有 token 时,只允许通过控制器读取
public 且 guest_visible 的内容。
- 控制器会以
user_id=None / role='user' 的语义传给 data_server,行为与原单体一致。
- 收藏始终需要 token。
与 Supabase 的关系
user_manager 同时保留了原有的 Supabase OAuth 路由(/v1/*,如 /v1/login/config)。本地账号体系(/api/auth/* + SQLite)与 Supabase 相互独立:即使 Supabase 不可达,本地鉴权仍可离线工作(启动时 Supabase 连接失败只记录告警、不阻断服务)。详见 user_manager。