用户、空间与认证授权
本页说明Molore的账号体系与访问边界:用户如何注册登录、令牌如何签发与吊销、个人空间与团队空间如何隔离数据、游客演示模式与管理后台的边界,以及会员功能的门控口径。理解这一层是理解检索、打标、图谱等所有上层模块「数据从谁的库里取」的前提。
核心概念
| 概念 | 说明 |
|---|---|
| 本地账户 | 本地注册永久免费,邮箱 + 密码开户,密码以 bcrypt 哈希落库 |
| JWT | 登录凭证,签发时携带 token_version 声明,用于服务端吊销 |
| token_version | 用户表上的版本号,改密/登出时递增,旧 token 立即失效 |
| 个人空间 | 默认作用域,内容按 user_id + tenant_id IS NULL 归属 |
| 团队空间 | 多人共享工作区,内容按 tenant_id = T 归属,全员共享口径 |
| 游客演示模式 | 未登录 GET 请求自动注入只读演示账号(demo@wenmo.local) |
| admin | 独立于普通用户体系的管理员通道,单独密钥签发 |
| 会员门控 | Pro 功能判定走 plan features + feature_flags.membership_enabled 总开关 |
注册与登录
- 本地注册永久免费:注册即开户(
POST /api/v1/auth/register,app/api/v1/endpoints/auth.py),赠送免费层容量护栏,条数不设限。 - 密码复杂度:8~128 位且须含大小写字母与数字,由
PASSWORD_COMPLEXITY_RE校验(app/core/security.py)。 - 密码哈希:bcrypt(
get_password_hash,rounds=12)。bcrypt 只认前 72 字节——哈希与校验两侧同口径按 UTF-8 截断 72 字节(app/core/security.py:48-61)。两侧必须对称截断,否则超长密码会永远「密码不正确」,这是登录/注销/改密全链路的既有教训。 - 登录成功签发 access token + refresh token 一对(
_create_token_pair),refresh token 不可当 access token 用(token_use声明区分)。
令牌吊销:token_version
JWT 本身无状态,吊销靠版本号实现(app/core/security.py:116):
- 签发时把用户当前
token_version写入 token 声明; - 每次请求
get_current_user比对 token 声明与用户表当前值,不一致即 401「Token has been revoked」; - 改密(
change-password)与登出(logout)都会递增token_version,此后所有旧 access/refresh token 一并失效(app/api/v1/endpoints/auth.py:307-374)。
安全事件(登录失败、无效 token、吊销重放等)统一走 app.security logger,只记事件类型 + IP + 账号标识,不记密码与 token 本体。
云端与本地是两套库、两个密码
桌面端本地库与云端库是两个独立数据库、两套独立密码:
- 改密只改当前所在的那一侧,不会同步到另一侧;
- 云登录自动建本地账户时,本地密码取刚验证过的云端密码,此后两库各自演化;
- 两侧的
token_version同样各自独立。
个人空间与团队空间
租户上下文由 app/core/tenant_scope.py 收口,全模块共用同一口径:
get_active_tenant:返回当前激活租户;成员被移除、租户停用或tenant_id残留时一律回落个人空间。scope_condition:团队上下文 →model.tenant_id == T;个人空间 →user_id == 本人 AND tenant_id IS NULL。笔记、剪藏、知识单元、文档、文件夹、标签全部按此条件过滤。- 团队空间内内容全员共享,不做部门/分组隔离;成员被移出后,其在空间内创建的内容保留在团队中。
- 会话始终是创建者私有:团队空间建的会话打上空间戳,但
user_id条件不随空间放宽(app/models/chat.py)。 - 图谱构建状态表
graph_build_state使用复合主键(user_id, tenant_id),个人空间以空串''占位(迁移migrate_graph_build_state_space_pk,app/core/db_migrations.py),保证每个空间各一行构建状态。
席位行锁
团队拉人有席位上限(默认 10 席)。并发拉人通过对租户行加锁读防超卖(app/api/v1/endpoints/tenants.py:229):
db.query(Tenant).filter(Tenant.id == tenant_id).with_for_update().first()PostgreSQL 下这是 SELECT ... FOR UPDATE;SQLite 下 with_for_update 为空操作,由单连接串行提交兜底。席位占满时拉人返回 403。
游客演示模式
app/core/guest_demo.py 实现「打开即所见」的演示:
- 未携带凭证的 GET 请求命中
/api/v1/前缀时,中间件自动注入演示账号(demo@wenmo.local)的长效 token; - 写操作(POST/PUT/DELETE/PATCH)不注入,仍需真实登录——演示账号只读(仅工作区切换 activate/deactivate 两个幂等端点例外,供演示团队空间);
- 演示 token 限 30 天且携带
token_version,改密/登出后旧缓存 token 被拒,重启自愈; - 演示账号只放可公开数据;自托管不播种演示账号时该功能自然关闭。
admin 独立于普通用户
管理后台通道与用户通道完全分离(app/core/security.py):
- admin token 用独立的
ADMIN_SECRET_KEY签发,payloadtype=admin;普通用户 token 是type=user,两者互不通用; - 铸造方式:
create_access_token({"sub": admin_id}, is_admin=True); - 管理端点挂在
app/api/admin/下,与普通用户 API(app/api/v1/)分属两套路由。
会员门控
Pro 功能(BYOK、云端备份同步、模型管家等)的权限判定有两个层次:
- 功能位判定:
BillingService.check_feature_access(user_id, feature)(app/services/billing_service.py:250)按订阅 plan 的 features 逐项判定,续费/折扣/权益全走此口径。 - 总开关:
feature_flags.membership_enabled(默认开,缺键即开)。管理员关闭期间所有会员功能全员放行。桌面端经desktop_gate.membership_enforced/membership_enforced_for判定(app/core/desktop_gate.py):本地开关 + 云端开关缓存双重判定,云端开关随/users/me回源缓存进cloud_bindings表。
断网语义是 fail-closed:断网沿用旧缓存;从未拉到过云端开关(缓存为 NULL)时维持门禁(app/core/desktop_gate.py:271-274),信息不足不擅自放行。
下一步
- 笔记、剪藏与知识管理——四类内容如何入库与归档
- 分块与索引机制——空间口径在三索引上的落地
- 团队空间使用指南——拉人、角色与审计日志
- 价格与会员——Pro 各档与按量计费口径