dsh-login

August 31, 2026 · View on GitHub

给 dsh-web 加一道登录门:打开 dsh-web 的端口时先要输入用户名和密码,验证通过后才 能看到界面。账号信息不落明文:密码用 scrypt(随机盐)哈希,整个凭据文件再用 AES-256-GCM 加密后写入本地配置文件($DSH_HOME/dsh-login.json,默认 ~/.dsh/dsh-login.json)。

适合部署于服务器上的dsh-web服务 需要自行架设nginx服务,把服务映射到局域网内使用

  • 纯主机侧 Cordis 插件,不修改任何 dsh 源码;
  • 通过 dsh plugin 以 bundle 方式挂载(dsh.bundle.patch),依赖列表只有 cordis / dsh-host-webserver / schemastery 三个 peer;
  • 自带独立 CLI(dsh-login),不启动 web 也能管理账号。

特性

能力说明
全量请求门禁包住 webserver 的 register / registerUpgrade / registerFallback:SPA 回退、/api 传输、WebSocket/SSE 升级全部先过会话校验
会话内存会话 + HttpOnly; SameSite=Strict Cookie(默认 24h,可配置);服务重启全员下线
首启引导没有账号时打开端口显示“创建管理员账号”页(bootstrap),建完即登录
加密存储凭据文件 AES-256-GCM 加密;密钥来自 DSH_LOGIN_SECRET(64 hex 或口令)→ 密钥文件 $DSH_HOME/dsh-login.key(自动生成)→ 明文模式(警告)
口令安全scrypt(N=2142^{14}, r=8, p=1)随机盐哈希,恒时比较;用户名不存在时烧一次假哈希防枚举
爆破防护每用户名+IP 滑动窗口限速(默认 5 次/30s → 429)+ 失败延时
多用户任意多账号,登录按用户名独立校验
账号管理页登录后访问 /accounts:网页上加用户 / 改名 / 改密 / 删用户(最后 1 个账号不允许网页删除)
右上角用户控件登录后 dsh-web 界面右上角出现「当前用户」头像菜单:账号管理 / 修改密码(内联弹窗)/ 切换用户 / 退出登录;若右上角被其它插件(如侧边栏)遮挡,会自动挪到左上角/下角(宿主侧 tapIndex 注入,无需客户端构建链)
管理 APIGET/POST /api/accounts/usersPUT/DELETE /api/accounts/users/<name>(需会话)
CLIstatus / set-user / remove-user / rename-user / list-users / reset,运行中改账号会被 mtime 轮询热加载

安装(本机 web profile)

# 1) 把本插件加入 web profile(等价于 pnpm add link:... 到 ~/.dsh/profiles/web)
dsh plugin --profile web add link:D:\code\dsh\dsh-login-plugin

# 2) 重启 dsh web
#    下次打开 http://127.0.0.1:3080 会先看到登录页;首次访问创建管理员账号。

仓库内 node_modules 是指向 ~/.dsh/profiles/node_modules 的 junction (与 dsh-ssh 等本地开发包的做法一致,测试与 link: 安装后的模块解析都靠它)。 若把仓库挪到别处,重建 junction: New-Item -ItemType Junction -Path <repo>\node_modules -Target $env:USERPROFILE\.dsh\profiles\node_modules

卸载:dsh plugin --profile web remove dsh-login,重启后端口恢复开放。

首次使用(二选一)

A. 网页引导(默认):打开端口 → “创建管理员账号” → 设置用户名密码 → 自动登录。

B. CLI(适合禁掉网页引导)

# 在运行 dsh web 的机器上执行(无需启动 web)
node D:\code\dsh\dsh-login-plugin\lib\cli.js set-user --user admin
# 输入密码(交互式提示,避免命令行历史残留)

之后把 bootstrap 设为 denied(见下)即可禁止网页自助建号。

配置

在 profile 的用户层给插件行加配置(dsh plugin --profile web add 后会自动把 dsh-login 行插入组合;改配置就编辑 ~/.dsh/profiles/web/cordis.patch.yml- insert: 里那行 dsh-login,或直接读插件的 cordis.patch.yml):

- id: dsh-login
  config:
    sessionTtlHours: 12        # 会话有效期(小时),默认 24
    bootstrap: denied          # 禁止网页自助建号,只用 CLI 管账号
    maxAttempts: 10            # 爆破限速:10 次/30s
    cookieName: dsh_session
    loginPath: /login
    logoutPath: /logout
    storePollMs: 2000          # 检测 CLI 改账号的轮询间隔(0=关闭)

全部配置项(均带默认值):enabled(true)、storeFile($DSH_HOME/dsh-login.json)、 keyFile($DSH_HOME/dsh-login.key)、sessionTtlHours(24)、cookieNamebootstrap(auto|denied)、maxAttempts(5)、attemptWindowMs(30000)、 failDelayMs(400)、storePollMs(2000)、loginPath(/login)、logoutPath(/logout)、 accountsPath(/accounts)、sessionWidget(true)。

主密钥优先级:环境变量 DSH_LOGIN_SECRET(64位 hex 原样使用;任意长口令则 scrypt 派生)→ 密钥文件(不存在则自动生成 64 hex,0600)→ 无密钥时降级明文 JSON(仍 scrypt 哈希,插件会打警告)。

登录机制

  • 未登录导航请求(GET/HEAD + Accept: text/html)→ 返回登录页(200);
  • 未登录其它请求(含对 /api/* 的直接访问、静态资源探测)→ 401 JSON;
  • 未登录 WebSocket/SSE 升级 → 握手直接 401 关闭;
  • 已登录 → 原 handler 照常处理。
  • 公开路径(免登录,精确匹配):登录页、退出页、/api/auth/login/api/auth/bootstrap/api/auth/logout/api/auth/status。其它一切路径 (包括管理页 /accounts/api/accounts/users*)都需要会话。
  • 右上角用户控件:插件通过 webserver 的 tapIndex 在 SPA 外壳(index.html)注入 一段内联脚本;登录后脚本读取 /api/auth/status,在 dsh-web 界面右上角渲染 「当前用户」头像气泡菜单(账号管理 / 修改密码 / 切换用户 / 退出登录 / 恢复自动位置),未登录时 不渲染。修改密码走内联弹窗(PUT /api/accounts/users/<当前用户>),无需离开 当前界面。气泡可按住拖拽到任意位置(位置持久化在 localStorage,刷新恢复);菜单里的「恢复自动位置」可回到四角自适应。未拖过时默认右上角,若被其它插件遮挡会通过 elementFromPoint 检测并自动 依次尝试左上角 / 右下角 / 左下角(SPA 挂载后延迟复检几次)。关闭:配置 sessionWidget: false
  • 退出:POST /api/auth/logout(清 Cookie),或访问 /logout 页面;会话只存内存, 重启即全部失效。

账号管理(多用户)

登录后打开 /accounts(登录页下方也有入口链接),可:

  • 查看账号列表(当前登录用户有“当前”标记);
  • 添加用户:填用户名 + 密码(≥8 位);
  • 改名:把某用户重命名为新名字(大小写不敏感判定重名);
  • 改密:只影响目标用户;
  • 删除用户:最后一个账号网页端拒绝删除(防止误锁死所有人);
  • 返回界面 / 退出登录。

管理接口(全部要求会话,否则 401):

方法路径说明
GET/api/accounts/users列用户名 {users:[...]}
POST/api/accounts/users{username,password} 新建(409 重名 / 400 策略违规)
PUT/api/accounts/users/<name>{password} 改密 和/或 {newUsername} 改名
DELETE/api/accounts/users/<name>删除(409 删除最后一个账号 / 404 不存在)

CLI 与网页共用同一个加密存储:任一侧的改动都会被另一侧(网页端靠 mtime 轮询) 热加载。

CLI 参考

dsh-login status
dsh-login set-user --user <name> [--password <p>]   # 新建或重置密码
dsh-login remove-user --user <name>
dsh-login rename-user --user <old> --new <new>      # 改名
dsh-login list-users
dsh-login reset --yes                                # 删除凭据文件(全部账号)

测试

node test/unit.test.mjs          # 18 项:crypto / store / sessions / 页面 / 账号策略 / 右上角控件
node test/integration.test.mjs   # 5 项:真实 webserver + frontend-static + 插件

集成测试覆盖:真实挂载顺序(登录插件最后加载,回退表已注册→原位重包)、反序 (注册期包装)、bootstrap→登录→SPA/API→登出全流程、WebSocket 升级鉴权、密文 存储无明文、CLI 外部改账号被热加载、bootstrap 禁用路径、多用户管理全流程 (增/改密/改名/删 + 最后一个账号保护 + 鉴权)、SPA 外壳右上角用户控件注入。 测试通过 scrypt 实际计算,运行约 1–2 s。

安全模型(请读)

  • 默认只绑 127.0.0.1:登录门禁解决“端口裸奔”,但明文 HTTP 在网络上可被嗅探。 若 --host 0.0.0.0 暴露到局域网/公网,请在前面加 TLS 反向代理,并设置 强口令DSH_LOGIN_SECRET 建议放环境变量(环境变量与 web 进程同属一个 用户空间,这是本项目能提供的最高一级的主密钥隔离)。
  • 自动生成的密钥文件与凭据文件同目录:防的是“文件被拷贝/误发/备份泄露”这类 意外读取;对能读整个 $DSH_HOME 的攻击者,scrypt 哈希才是最后防线。
  • 首启引导:零账号时第一个到达的人即可建管理员账号,所以在暴露到不可信网络 之前先用 CLI 建号并设 bootstrap: denied
  • 会话令牌 256 位随机,仅存内存与 HttpOnly Cookie;SameSite=Strict 挡住跨站 请求伪造。若把 GUI 暴露给多用户,请自行评估多账号并发(本插件支持多用户, 会话相互独立)。

目录结构

lib/crypto.js        scrypt 哈希/校验、AES-256-GCM、密钥派生
lib/store.js         加密凭据存储(原子写、热重载、账号策略)
lib/sessions.js      内存会话 + Cookie 工具
lib/login-page.js    登录 / 引导 / 退出页(单文件内联样式脚本)
lib/accounts-page.js 账号管理页(多用户:增删改改名改密)
lib/paths.js         $DSH_HOME 与密钥文件解析
lib/index.js         插件本体:门禁包装 + 公开路由 + 管理 API
lib/cli.js           账号管理 CLI
test/unit.test.mjs / test/integration.test.mjs
cordis.patch.yml     bundle 补丁(把 dsh-login 行插入组合)

用户工作区隔离(可选)

配置 userWorkspaces 后,单个 dsh web 实例内的多个登录用户各有一块独立工作区: 每个用户的工作区固定为 <userWorkspaces>/<用户名>

  • 未配置 userWorkspaces:行为与旧版完全一致,所有用户共享同一工作区/会话历史。
  • 配置后:
    • workspace.*sessions.* 按当前登录用户名过滤/强制路径;
    • 侧栏只显示自己的工作区与会话历史,新开会话自动落在自己的用户目录;
    • 无法通过界面看到或操作其他用户的会话。

~/.dsh/profiles/web/package.json 的插件配置中开启:

"dsh-login": {
  "enabled": true,
  "userWorkspaces": "D:\\code\\users"   // 未配置 = 关闭;目录不存在会自动创建
}

已知边界:

  • 界面/API 层的软隔离,不做文件系统级沙箱(知道绝对路径的人仍可能直接读写文件);
  • 启用前已存在、不在任何用户根目录下的工作区/会话会被隐藏,可将旧目录移入某用户的目录后恢复可见;
  • 附件等 profile 级数据仍是共享的。