feat(dcauth): 新增后台手机号与设备认证登录

登录链路改为账号密码 + 短信验证码 + DCAuth 设备签名,并新增后台手机号绑定、
登录日志、设备绑定与重置接口。
This commit is contained in:
yml2213
2026-10-21 20:46:24 +08:00
parent 7542d547bf
commit 040100f290
36 changed files with 3128 additions and 41 deletions
@@ -0,0 +1,116 @@
-- 102_admin_phone_device_auth.sql —— 后台账号手机号、短信验证码与设备认证。
--
-- 登录链路:账号/密码 + 短信验证码 + DCAuth 设备签名。
-- 旧账号未绑定手机号时仍可使用原密码登录;绑定手机号后强制走新链路。
ALTER TABLE admin_users
ADD COLUMN IF NOT EXISTS phone TEXT NOT NULL DEFAULT '';
ALTER TABLE admin_users
ADD COLUMN IF NOT EXISTS phone_bound_at TIMESTAMPTZ;
ALTER TABLE admin_users
ADD COLUMN IF NOT EXISTS phone_bound_by BIGINT;
ALTER TABLE admin_users
ADD COLUMN IF NOT EXISTS test_account BOOLEAN NOT NULL DEFAULT FALSE;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_admin_users_phone_bound_by'
) THEN
ALTER TABLE admin_users
ADD CONSTRAINT fk_admin_users_phone_bound_by
FOREIGN KEY (phone_bound_by) REFERENCES admin_users(id) ON DELETE SET NULL;
END IF;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS idx_admin_users_phone
ON admin_users(phone)
WHERE phone <> '';
CREATE TABLE IF NOT EXISTS admin_sms_codes (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
phone TEXT NOT NULL,
purpose TEXT NOT NULL DEFAULT 'login',
code_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ,
failed_attempt_count INTEGER NOT NULL DEFAULT 0,
request_ip TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_admin_sms_codes_user_created
ON admin_sms_codes(user_id, purpose, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_admin_sms_codes_phone_created
ON admin_sms_codes(phone, created_at DESC);
CREATE TABLE IF NOT EXISTS admin_user_devices (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
device_id TEXT NOT NULL,
public_key TEXT NOT NULL,
machine_code_hash TEXT NOT NULL,
machine_code_mask TEXT NOT NULL DEFAULT '',
client_version TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
bound_at TIMESTAMPTZ NOT NULL,
last_used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
revoked_by BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_admin_user_devices_active_user
ON admin_user_devices(user_id)
WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_admin_user_devices_device_id
ON admin_user_devices(device_id);
CREATE TABLE IF NOT EXISTS admin_device_challenges (
id BIGSERIAL PRIMARY KEY,
challenge_id TEXT NOT NULL UNIQUE,
app TEXT NOT NULL,
purpose TEXT NOT NULL DEFAULT 'login',
challenge TEXT NOT NULL,
poll_token_hash TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
user_id BIGINT REFERENCES admin_users(id) ON DELETE SET NULL,
device_id TEXT NOT NULL DEFAULT '',
public_key TEXT NOT NULL DEFAULT '',
machine_code_hash TEXT NOT NULL DEFAULT '',
machine_code_mask TEXT NOT NULL DEFAULT '',
client_version TEXT NOT NULL DEFAULT '',
device_ticket_hash TEXT NOT NULL DEFAULT '',
expires_at TIMESTAMPTZ NOT NULL,
authorized_at TIMESTAMPTZ,
ticket_issued_at TIMESTAMPTZ,
consumed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_admin_device_challenges_expires
ON admin_device_challenges(expires_at);
CREATE INDEX IF NOT EXISTS idx_admin_device_challenges_user
ON admin_device_challenges(user_id, created_at DESC);
ALTER TABLE admin_login_logs
ADD COLUMN IF NOT EXISTS login_method TEXT NOT NULL DEFAULT 'password';
ALTER TABLE admin_login_logs
ADD COLUMN IF NOT EXISTS machine_code_mask TEXT NOT NULL DEFAULT '';
ALTER TABLE admin_sessions
ADD COLUMN IF NOT EXISTS device_id TEXT NOT NULL DEFAULT '';
COMMENT ON COLUMN admin_users.phone IS '后台账号绑定手机号,用于登录短信验证码';
COMMENT ON COLUMN admin_users.test_account IS '技术测试账号:允许仅账号密码登录,跳过短信和 DCAuth 设备验证';
COMMENT ON TABLE admin_sms_codes IS '后台登录短信验证码(哈希存储、一次性消费)';
COMMENT ON TABLE admin_user_devices IS '后台账号绑定的 DCAuth 设备(公钥 + 机器码哈希)';
COMMENT ON TABLE admin_device_challenges IS 'DCAuth 设备认证 challenge 与一次性 ticket';
COMMENT ON COLUMN admin_login_logs.login_method IS 'password / sms_device';
COMMENT ON COLUMN admin_login_logs.machine_code_mask IS '登录设备机器码脱敏值';
COMMENT ON COLUMN admin_sessions.device_id IS '登录会话绑定的设备 ID';