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=, r=8, p=1)随机盐哈希,恒时比较;用户名不存在时烧一次假哈希防枚举 |
| 爆破防护 | 每用户名+IP 滑动窗口限速(默认 5 次/30s → 429)+ 失败延时 |
| 多用户 | 任意多账号,登录按用户名独立校验 |
| 账号管理页 | 登录后访问 /accounts:网页上加用户 / 改名 / 改密 / 删用户(最后 1 个账号不允许网页删除) |
| 右上角用户控件 | 登录后 dsh-web 界面右上角出现「当前用户」头像菜单:账号管理 / 修改密码(内联弹窗)/ 切换用户 / 退出登录;若右上角被其它插件(如侧边栏)遮挡,会自动挪到左上角/下角(宿主侧 tapIndex 注入,无需客户端构建链) |
| 管理 API | GET/POST /api/accounts/users、PUT/DELETE /api/accounts/users/<name>(需会话) |
| CLI | status / 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)、cookieName、
bootstrap(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/*的直接访问、静态资源探测)→401JSON; - 未登录 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 级数据仍是共享的。