018a77b13a
接续 90a3790(扫码 A0+A1+B + step-up 雏形)。蓝本见 AUTH.md。
OIDC 统一到 cdrop 自签 token
- /auth/exchange 先 JWKS 验 id_token 签名(自签 token 的唯一信任锚)再签 cdrop
自签 HS256 access token(绑 sid),不再把 IdP RS256 下发浏览器;验签失败仅告警
并回退 RS256(会话仍建、下次 refresh 自愈),登录不中断
- /auth/refresh oidc 路径仍打 IdP 轮换 refresh_token + 探测 IdP 侧吊销,但同样回
自签 token;createWebSession 改返回行 id
- verify 链保留 RS256 分支仅供桌面端 loopback(其即时吊销留后续)
吊销即时生效 + 被吊销设备自知回退
- verifySelfToken 每请求按 sid 查 web_sessions 行,行删即拒 → 吊销在被吊销设备
下一次请求即生效(不等 TTL),oidc / self / guest 一致
- 前端仅在「服务端以 401 确证会话失效」时 forceLogout 清登录态、回登录页;短期连接
失败(5xx / 网络)一律保留登录态(refreshTokens 改三态 refreshed/invalid/transient,
cookieRefresh 改 discriminated 结果)
- __root 反应式守卫:prod 下失登录态且非认证路由即跳登录页;新增 auth.sessionLost.* 三语
设备列表并入会话列表 + 孤儿自动清除
- GET /api/auth/sessions 统一 web_sessions 与原生客户端设备(桌面 / iOS,自 devices
登记表呈现,native=true,离线也列);同名不重复列出,排除 shortcut 与无会话 browser
- 吊销网页会话连带删其设备登记(无同名在用会话时);原生「登出」走
DELETE /api/devices/{name}(同要求 step-up)
- 新增 DeleteOrphanBrowserDevices(browser 型且无存活 web_session),接入设备清扫器(1h)
应用内扫码 + 聚珍崩溃 / CJK 禁则修复
- 登录页内置 getUserMedia + jsqr 扫码,不再外跳系统应用(避免开错浏览器)
- 修聚珍(cjk-autospace)MutationObserver 与 React 重渲染同子树冲突致的 insertBefore
崩溃:扫码 / 显码 / 批准三状态机页整页跳过聚珍(data-jz-skip)
- 修流动正文未跑 CJK 禁则(jinze 为段落级、须 opt-in):设置 / 快捷指令 / 桌面页一批
hint 补 data-jz-level="paragraph" + justify,短标签 / 状态仍留文本级
step-up 完善
- auth_time 改可选(Casdoor 不下发,原强制致死循环);stepped_up_at 按会话绑定、
StepUpMaxAgeSeconds 窗口内不复要求;新增 POST /api/auth/stepup
- 批准页 persist 编进 step-up returnTo 防整页跳转丢失;scope 随档绑定(信任=full /
仅此次=guest),默认偏安全的「仅此次」
文档与测试
- AUTH.md:折中架构、accounts 薄表、令牌架构、扫码流程、step-up、§4.4 统一会话列表
- 新增 Go 测试:OIDC 自签验证、verifySelfToken 吊销即拒、会话吊销连带删设备、
统一会话列表(含原生 / 不含 shortcut / 不重复)、孤儿清扫、删设备需 step-up
147 lines
7.0 KiB
SQL
147 lines
7.0 KiB
SQL
-- cdrop MVP schema (PROJECT_BRIEF.md §4)
|
||
-- 以 IF NOT EXISTS 形式由 internal/db/bootstrap.go 启动期单事务执行。
|
||
|
||
CREATE TABLE IF NOT EXISTS devices (
|
||
user_id TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
type TEXT NOT NULL,
|
||
last_seen INTEGER NOT NULL,
|
||
PRIMARY KEY (user_id, name)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS shortcut_tokens (
|
||
jti TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL,
|
||
label TEXT NOT NULL,
|
||
scopes TEXT NOT NULL,
|
||
created_at INTEGER NOT NULL,
|
||
expires_at INTEGER NOT NULL,
|
||
last_used_at INTEGER,
|
||
revoked INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS transfer_sessions (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL,
|
||
sender_name TEXT NOT NULL,
|
||
receiver_name TEXT NOT NULL,
|
||
state TEXT NOT NULL,
|
||
mode TEXT,
|
||
file_name TEXT NOT NULL,
|
||
file_size INTEGER NOT NULL,
|
||
file_sha256 TEXT,
|
||
bytes_transferred INTEGER NOT NULL DEFAULT 0,
|
||
created_at INTEGER NOT NULL,
|
||
finished_at INTEGER,
|
||
fail_reason TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS clipboard_state (
|
||
user_id TEXT PRIMARY KEY,
|
||
content_type TEXT NOT NULL,
|
||
content TEXT,
|
||
source_device TEXT,
|
||
updated_at INTEGER NOT NULL,
|
||
-- version 是每用户严格单调递增的版本号(每次写 +1),变更探测 / ETag 用;
|
||
-- updated_at 仍是墙钟(TTL / 展示)。既有库由 bootstrap 的幂等 ALTER 补列。
|
||
version INTEGER NOT NULL DEFAULT 0,
|
||
-- origin_ts 是内容在源设备被复制的时刻(ms)。冲突收敛按它做 LWW——写入仅在
|
||
-- origin_ts 严格大于现存时被接受,故延迟/乱序到达的旧复制不会盖掉新的。
|
||
origin_ts INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
|
||
-- web_sessions:浏览器端「免重登」。cookie 里只放不透明随机串,本表 id 存其
|
||
-- SHA-256,故 DB 单独泄露也换不出可用 cookie;refresh_token 以 AES-256-GCM 落盘
|
||
-- (密钥在容器环境、不在库内)。仅浏览器使用——桌面端 refresh_token 由 Go keyring
|
||
-- 持有、走自有 loopback flow,绝不入此表。device_name 随会话存,使 PWA 存储被清后
|
||
-- 开机仍能恢复本机设备名。
|
||
CREATE TABLE IF NOT EXISTS web_sessions (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL,
|
||
refresh_token TEXT NOT NULL,
|
||
device_name TEXT NOT NULL DEFAULT '',
|
||
user_agent TEXT NOT NULL DEFAULT '',
|
||
created_at INTEGER NOT NULL,
|
||
last_used_at INTEGER NOT NULL,
|
||
expires_at INTEGER NOT NULL,
|
||
-- kind 区分会话来源(AUTH.md §2.2):oidc=绑定 IdP refresh_token 的正常浏览器
|
||
-- 登录(refresh 时向 IdP 续);self/guest=cdrop 自签会话,无 IdP refresh_token,
|
||
-- refresh 时仅按本行自签 access token、不触 IdP。既有行经 bootstrap 的幂等 ALTER
|
||
-- 补列后默认视为 oidc/full,行为不变。
|
||
kind TEXT NOT NULL DEFAULT 'oidc',
|
||
-- scope=full/guest。guest(扫码受限借用设备)服务端强制受限:能收发文件,
|
||
-- 但不能改账号、不能再批准别的设备、不能签发长效 token(路由层 requireFullSession 守门)。
|
||
scope TEXT NOT NULL DEFAULT 'full',
|
||
-- granted_by=扫码批准者的 session id,供审计与「吊销我批准过的借用设备」连带吊销。
|
||
granted_by TEXT NOT NULL DEFAULT '',
|
||
-- stepped_up_at=本会话最近一次通过 step-up 再认证的 Unix 秒(AUTH.md §6)。在
|
||
-- StepUpMaxAgeSeconds 窗口内,敏感动作不再重复要求再认证(避免每次都弹再登录)。
|
||
stepped_up_at INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_web_sessions_expires ON web_sessions (expires_at);
|
||
|
||
-- push_subscriptions:浏览器 / PWA 的 Web Push 订阅端点。当某设备「页面已关闭」
|
||
-- (无活的 SSE 连接)时,服务端按此表向其推送系统通知;页面开着时事件仍走 SSE,
|
||
-- 前端自弹 toast,不触发 push。id 是 SHA-256(endpoint) 的 hex,使同一浏览器重复
|
||
-- 订阅幂等覆盖(endpoint 唯一标识一条推送通道)。p256dh/auth 是 RFC 8291 消息加密
|
||
-- 用的客户端公钥与鉴权密钥(base64url)。按 (user_id, device_name) 定位收件设备,
|
||
-- 与 hub.SendTo 用 device_name 作 deviceID 一致。桌面端不入此表——它走本地原生通知。
|
||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL,
|
||
device_name TEXT NOT NULL,
|
||
endpoint TEXT NOT NULL,
|
||
p256dh TEXT NOT NULL,
|
||
auth TEXT NOT NULL,
|
||
user_agent TEXT NOT NULL DEFAULT '',
|
||
-- locale 随订阅存:推送文案在服务端渲染(SW 只负责展示收到的 title/body),
|
||
-- 故须知道该设备的界面语言;用户切换语言时前端重新订阅会幂等刷新此列。
|
||
locale TEXT NOT NULL DEFAULT 'zh-CN',
|
||
created_at INTEGER NOT NULL,
|
||
last_used_at INTEGER NOT NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_device
|
||
ON push_subscriptions (user_id, device_name);
|
||
|
||
-- accounts:cdrop 侧「薄账户数据层」(AUTH.md §2.1)。身份来源仍是 OAuth provider,
|
||
-- user_id 继续等于 OIDC sub;本表只存 cdrop 自维护的账户元数据,不含任何凭证(密码 /
|
||
-- 第二因子全在 provider)。OIDC exchange 成功后 upsert:捕获 match_key(默认 email
|
||
-- claim,仅在管理员开启「迁移标记」时用作跨源关联的 join key)、显示名 / 头像、roles
|
||
-- (groups claim 缓存,admin 判定用)。
|
||
CREATE TABLE IF NOT EXISTS accounts (
|
||
user_id TEXT PRIMARY KEY,
|
||
match_key TEXT NOT NULL DEFAULT '',
|
||
display_name TEXT NOT NULL DEFAULT '',
|
||
avatar_url TEXT NOT NULL DEFAULT '',
|
||
roles TEXT NOT NULL DEFAULT '',
|
||
provider TEXT NOT NULL DEFAULT '',
|
||
created_at INTEGER NOT NULL,
|
||
last_login_at INTEGER NOT NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_accounts_match_key ON accounts (match_key);
|
||
|
||
-- login_requests:扫码登录的待批准请求(AUTH.md §2.3、§4)。三方密钥分离——
|
||
-- poll_secret 存其 SHA-256(新设备私有、QR 不含、领取会话凭它);approval_code 随 QR
|
||
-- 给批准方。短 TTL(默认 120s),consumed/expired 后不可复用,由 reaper 清理。
|
||
CREATE TABLE IF NOT EXISTS login_requests (
|
||
id TEXT PRIMARY KEY,
|
||
poll_secret TEXT NOT NULL,
|
||
approval_code TEXT NOT NULL,
|
||
status TEXT NOT NULL,
|
||
new_device_name TEXT NOT NULL,
|
||
new_device_type TEXT NOT NULL,
|
||
user_agent TEXT NOT NULL DEFAULT '',
|
||
request_ip TEXT NOT NULL DEFAULT '',
|
||
approver_user_id TEXT NOT NULL DEFAULT '',
|
||
grant_scope TEXT NOT NULL DEFAULT '',
|
||
grant_persist TEXT NOT NULL DEFAULT '',
|
||
created_at INTEGER NOT NULL,
|
||
expires_at INTEGER NOT NULL,
|
||
approved_at INTEGER
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_login_requests_expires ON login_requests (expires_at);
|