本文件回答:谁能访问什么、鉴权分几层、会话 / 授信设备 / API Token 各自的规则、授权中间件做什么。 依赖:data-model.md
Haven 的鉴权分三个独立维度,共同决定一次请求能否通过:
visibility 和 user_id 共同决定。三个维度正交,授权中间件依次判断,任一环节失败即拒绝。
username + password;sessions 表),下发会话 cookie;trusted_devices 记录,下发授信设备 cookie。username + password;关键点:授信设备不免除密码,只免除 TOTP。密码始终要输。授信设备 cookie 单独存放,与会话 cookie 独立。
users.totp_enabled = false 时,登录只校验密码,不要求 TOTP。这是首次登录的默认状态——用户登录后在设置页绑定 TOTP,绑定成功后 totp_enabled 置为 true。
用途:维护“已登录”状态,是最短命的凭证。
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
uuid | PK | |
user_id |
uuid | NOT NULL, FK → users(id) ON DELETE CASCADE | |
token_hash |
text | NOT NULL, UNIQUE | 会话 token 的 SHA-256 哈希,明文只存在于 cookie |
expires_at |
timestamptz | NOT NULL | 过期时间,默认创建后 7 天 |
last_used_at |
timestamptz | NOT NULL | 最后使用时间,每次请求更新 |
user_agent |
text | NOT NULL DEFAULT '' | 创建会话时的 UA,仅用于设置页展示 |
created_at |
timestamptz | NOT NULL |
规则:
HttpOnly、Secure(HTTPS 下)、SameSite=Lax。生产环境若为 HTTP(自用局域网),Secure 可配为 false,但要在 architecture.md 里注明风险。last_used_at,但不延长 expires_at。用途:标记“这台浏览器可信”,用于跳过 TOTP。不代表登录态,不代表免密。
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
uuid | PK | |
user_id |
uuid | NOT NULL, FK → users(id) ON DELETE CASCADE | |
token_hash |
text | NOT NULL, UNIQUE | 设备 token 的 SHA-256 哈希 |
device_name |
text | NOT NULL DEFAULT '' | 用户可命名,如“家里台式机” |
expires_at |
timestamptz | NOT NULL | 默认 30 天 |
last_used_at |
timestamptz | NOT NULL | |
created_at |
timestamptz | NOT NULL |
规则:
haven_trusted_device。用途:供 TUI / Neovim / 脚本长期使用的凭证,与登录会话完全解耦。
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
uuid | PK | |
user_id |
uuid | NOT NULL, FK → users(id) ON DELETE CASCADE | |
name |
text | NOT NULL | 用户命名,如“TUI @ 家里”“Neovim @ 公司” |
token_hash |
text | NOT NULL, UNIQUE | Token 的 SHA-256 哈希 |
token_prefix |
text | NOT NULL | 明文前 8 位,用于设置页展示与识别,不用于鉴权 |
scope |
text | NOT NULL, CHECK IN ('read','read_write') | 权限档位 |
expires_at |
timestamptz | NULL | NULL 表示永不过期 |
last_used_at |
timestamptz | NULL | 从未使用时为 NULL |
revoked_at |
timestamptz | NULL | 非 NULL 表示已撤销 |
created_at |
timestamptz | NOT NULL |
规则:
token_prefix。这是行业惯例,防止数据库泄露后 Token 被直接使用。haven_<scope>_<32字节随机串base64url>,例如 haven_ro_AbCd...。前缀便于用户和日志识别,但鉴权时不解析前缀,只查哈希。read:只能读,不能写;read_write:读写都可以。
没有更细的权限,也不区分“只能读某类资源”。revoked_at,不是删除记录。保留记录便于审计追溯。已撤销的 Token 鉴权时直接拒绝。name、token_prefix、scope、last_used_at、created_at,并提供撤销按钮。所有 API 请求(除了公开接口白名单)必须经过授权中间件。中间件按以下顺序执行:
Authorization: Bearer <token>:
tokens 表,按 token_hash 匹配;revoked_at IS NULL)、未过期(expires_at IS NULL OR expires_at > now());TokenUser{user_id, scope};sessions 表,按 token_hash 匹配;SessionUser{user_id},更新 last_used_at;Anonymous。优先级:Authorization header 优先于 cookie。两者同时存在时以 header 为准,便于脚本调用时不受浏览器 cookie 干扰。
根据接口要求,中间件拒绝或放行:
anonymous_ok:任何人可访问,但仍需计算可见性过滤(见 6.3);login_required:必须是 SessionUser 或 TokenUser,Anonymous 返回 401;write:必须是 SessionUser,或 scope = read_write 的 TokenUser。scope = read 的 TokenUser 返回 403。totp_verified:仅 SessionUser 且会话创建时通过过 TOTP 才满足。本期除“关闭 TOTP”“修改密码”“创建 Token”这几个高危操作外,不强制要求。中间件计算出当前请求的数据可见性条件,注入 context,供 handler 使用:
Anonymous:WHERE visibility = 'public' AND published_at IS NOT NULL;SessionUser / TokenUser:WHERE (user_id = current_user_id) OR (visibility = 'public' AND published_at IS NOT NULL)。所有读接口必须使用这个条件,不得自己拼 visibility 判断。 这是 conventions.md 第四节那条禁令的具体落点。
在 6.3 基础上,所有写操作与私有读操作必须额外加 user_id = current_user_id。跨用户访问返回 404,不返回 403(不泄露资源存在性)。
| 操作 | Anonymous | Token (read) | Token (read_write) | SessionUser |
|---|---|---|---|---|
| 浏览公开笔记列表 | ✅ | ✅ | ✅ | ✅ |
| 读公开笔记详情 | ✅ | ✅ | ✅ | ✅ |
| 读私有笔记 | ❌ 401 | ✅(自己的) | ✅(自己的) | ✅(自己的) |
| 创建 / 编辑 / 删除笔记 | ❌ | ❌ 403 | ✅ | ✅ |
| publish / 撤回笔记 | ❌ | ❌ 403 | ✅ | ✅ |
| 搜索笔记与标签 | ✅(仅公开结果) | ✅(自己 + 公开) | ✅(自己 + 公开) | ✅(自己 + 公开) |
| 创建 / 删除标签 | ❌ | ❌ 403 | ✅ | ✅ |
| 查看作品集 | ✅ | ✅ | ✅ | ✅ |
| 管理作品集 | ❌ | ❌ 403 | ✅ | ✅ |
| 创建 / 撤销 Token | ❌ | ❌ 403 | ❌ 403 | ✅(需 TOTP) |
| 查看 / 撤销授信设备 | ❌ | ❌ 403 | ❌ 403 | ✅ |
| 查看 / 撤销会话 | ❌ | ❌ 403 | ❌ 403 | ✅ |
| 绑定 / 关闭 TOTP | ❌ | ❌ 403 | ❌ 403 | ✅(需原 TOTP) |
| 修改密码 | ❌ | ❌ 403 | ❌ 403 | ✅(需原密码 + TOTP) |
说明:Token(包括 read_write)不能管理凭证类资源(Token / 授信设备 / 会话 / TOTP / 密码)。理由:Token 本身是凭证,如果它又能创建新 Token,泄露一次就等于永久后门。凭证管理只允许通过 Web 会话操作。
users.password_hash text NOT NULL。users.totp_secret text NULL(未绑定时为 NULL)。修改密码成功后,在同一个事务内:
sessions 记录;trusted_devices 记录;tokens 记录(见第五节规则 8);audit.md)。users 表的完整字段如下,data-model.md 第九节待补部分以此为准:
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|---|---|---|---|---|
id |
uuid | PRIMARY KEY | gen_random_uuid() |
|
username |
text | NOT NULL, UNIQUE | — | 登录名,本期只有一条记录 |
password_hash |
text | NOT NULL | — | bcrypt 哈希 |
totp_secret |
text | NULL | NULL | TOTP 密钥,未绑定为 NULL |
totp_enabled |
boolean | NOT NULL | false |
是否已启用 TOTP |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
规则:
users 记录,由部署时的初始化脚本创建(见 architecture.md)。username 不区分大小写唯一(用 lower(username) 唯一索引),避免 Admin / admin 混淆。