本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2026年05月30日 统计字数: 7840字 阅读时间: 16分钟阅读 本文链接: https://soulteary.com/2026/05/30/warden-maintainable-access-roster-from-shared-password.html ----- # Warden(守望者):从共享口令,到一份可维护的准入名册 Warden(守望者)是一个轻量的准入名单数据服务。 它负责维护“谁可以登录”,以及这些人对应的邮箱、手机号、用户 ID、状态、角色和权限范围等信息。它可以接在 Stargate 后面,作为认证入口背后的准入名册;也可以独立运行,给内部网关、平台、脚本或其他系统提供统一的用户查询能力。 ## 写在前面 上一篇里,我们聊了 Stargate。 它负责把认证逻辑前移到网关侧,让反向代理先替我们拦住请求,而不是让每个内部服务都重复实现登录、鉴权、跳转和会话判断。这一步解决的是一个很基础的问题: > 有没有登录? 但门装上之后,很快会遇到下一个问题: > 谁可以通过这道门? 一开始,用共享口令就够了。给 Stargate 配一个密码,知道密码的人能进,不知道密码的人进不来。对 HomeLab、测试环境、小团队内部工具,或者几个临时服务来说,这个方案很轻,也很容易跑通。 > 这没什么问题。 很多内部服务一开始都应该这么做。先把门装上,比继续裸奔要好得多。 但只要使用的人多起来,共享口令就开始变得不那么舒服。有人离开项目之后,要不要换密码?如果换密码,是不是所有还在使用的人都要一起更新?如果密码被复制到聊天记录、脚本、配置文件里,又该怎么算? 更麻烦的是,共享口令只能证明一件事: > 这个人知道密码。 但它证明不了另一件事: > 这个人是谁?他现在还应该被允许登录吗? 当问题走到这里,就不能只靠一串密码了。你需要一份名单。 这份名单不一定要很复杂。它可以先从一个本地 JSON 文件开始,里面写着谁可以登录,对应的邮箱、手机号、用户 ID、状态、角色和权限范围是什么。但它应该是独立的,可以被 Stargate 查询,也可以被其他内部系统查询;可以先用本地文件维护,后面再接远程数据源、Redis、多实例和监控。 我不希望 Stargate 变成用户数据库。 Stargate 应该继续做入口、会话和登录流程。用户是谁、现在还能不能进、验证码应该发到哪里,这些信息可以单独放到另一个地方。 这就是 Warden 想解决的事情。 它不是完整 IAM,也不想替代公司里的 SSO、LDAP、OIDC 或身份中台。它只先做好一件小事: **维护一份可以被机器稳定查询的准入名册。** ![soulteary/warden](https://attachment.soulteary.com/2026/05/30/warden-github.jpg) 开源项目地址:[https://github.com/soulteary/warden](https://github.com/soulteary/warden) 如果 Stargate 是先把门装上,那么 Warden 做的事情,就是让这道门不要只认一把共享钥匙,**让它开始认人。** ## 共享口令的问题是怎么出现的 很多内部服务最早的认证方式,都是从一个共享口令开始的。这没什么问题,尤其是在 HomeLab、测试环境和小团队内部工具里,共享口令很适合做第一步。 它不需要数据库、账号系统和注册流程,也不需要接入复杂的身份服务。只要准备一个密码,再把入口保护起来,服务就可以先安全一点地跑起来。 这也是我在做 Stargate 时比较在意的事情: > 先把门装上。 很多时候,第一步不应该太重。如果一开始就要求接 SSO、配回调、建用户表、发验证码、接短信邮件服务,那很多内部小工具最后大概率还是继续裸奔。 所以,共享口令不是问题。 **长期只靠共享口令,才是问题。** 当服务只有你自己用,或者只有两三个人临时用一下,共享口令足够简单。但只要使用时间拉长,使用的人变多,它的边界就会慢慢出现。 第一个问题,是换密码很麻烦。有人离开项目之后,要不要换密码?如果换,所有还在使用的人都要一起更新;如果不换,这个密码就会变成一把一直流转在外面的钥匙。 第二个问题,是你很难知道密码到底流到哪里去了。它可能在聊天记录里,可能在某个脚本里,可能在浏览器密码管理器里,也可能被复制到某个临时文档、部署说明或者 `.env` 文件里。一旦它扩散出去,你很难再把它完整收回来。 第三个问题,是共享口令没有“人”的概念。它只能回答一个很粗的问题: > 这个请求里带来的密码对不对? 但它回答不了更多后续问题: - 这个人是谁; - 他现在还在不在项目里; - 他应该看到哪些服务; - 验证码应该发到哪个邮箱或手机号; - 下游服务能不能知道他的角色。 这些问题不是共享口令擅长回答的。因为共享口令本质上只是一把钥匙,谁拿到这把钥匙,谁就能开门。 但真实的访问控制,很多时候并不是只看钥匙。你还会关心拿钥匙的人是谁,他现在是否还应该拿着这把钥匙,他的状态是不是正常,他能不能继续走后面的登录流程。 当问题走到这一步,就需要把“密码”往后退一步,把“人”放到前面来。 这时候,我们需要的不是更长的共享口令,也不是把更多用户信息塞进 Stargate 的环境变量里。我们需要一份名单,一份可以被系统查询的名单。 它不一定复杂。一开始甚至可以只是一个 JSON 文件。但这份名单应该能回答几个基础问题: - 这个邮箱在不在; - 这个手机号对应谁; - 这个用户现在是不是 `active`; - 他有没有 `user_id`; - 后续验证码应该发到哪里; - 下游服务需要的话,能不能拿到 `role` 和 `scope`。 这就是 Warden 要补上的那一块,Stargate 负责把门装上,Warden 负责告诉这道门:**谁还应该被允许通过。** ## 以前通常怎么解决 遇到“谁可以登录”这个问题,常见做法大概有几种。 **第一种,是继续把名单写在认证入口里。** 比如把允许登录的邮箱、手机号、用户状态,直接写进 Stargate 的配置里。用户很少的时候,这样当然能用。两三个人、几个测试账号、一两个内部服务,放在环境变量或者配置文件里都不算麻烦。 但这个做法很容易越走越偏。一开始只是加几个邮箱,后来要加手机号,再后来要区分用户状态,接着又想返回 `user_id`、`role`、`scope`,让下游服务也能知道当前用户是谁。再往后,可能还想接远程数据源、做缓存、做同步、做审计。 到这一步,认证入口就不再只是认证入口了。它会慢慢变成半个用户数据库。 这不是我希望 Stargate 走的方向。 Stargate 应该继续守入口、管会话、处理登录流程,而不是把所有用户资料都塞进自己身体里。 **第二种,是每个服务自己维护一份用户列表。** 这也很常见。Grafana 有一份,文档站有一份,内部后台有一份,测试系统有一份,自动化脚本里可能还有一份。 如果这些系统本来就有自己的用户体系,这没什么问题。但很多内部工具并不是这样。它们只是被放在同一个内部环境里,临时给一组人使用。 这时每个服务都维护一份名单,就会出现很现实的麻烦:加一个人,要改很多地方;禁用一个人,也要改很多地方;有的地方改了,有的地方忘了改。最后你很难回答一个简单问题: > 这个人到底还能访问哪些东西? 名单一旦散在各处,真正麻烦的不是“添加用户”,而是“确认一个人已经被完整移除”。这件事靠人肉检查,很容易漏。 **第三种,是用表格或文档维护名单。** 这可能是很多团队最自然的做法。一个表格里写着邮箱、手机号、备注、所属项目、是否还在使用。人看起来很清楚,编辑起来也方便。 如果只是管理流程,表格很好。但如果它开始参与登录流程,问题就来了。 **表格适合人看,不适合系统在运行时依赖。** 你当然可以把表格作为数据源,比如定期导出成 JSON,或者通过内部接口同步到某个服务。但认证流程本身不应该靠人手复制粘贴,也不应该在登录时临时去翻一个文档。 登录链路需要的是稳定、可查询、可控制返回字段的接口。 不是一个“大家记得去更新”的共享文档。 **第四种,是直接接入完整身份系统。** 比如公司 SSO、LDAP、OIDC、IAM,或者 Keycloak、Authentik、Authelia 这类更完整的身份方案。 如果你已经有这些东西,并且它们已经稳定服务于团队,那当然应该优先使用它们。完整身份系统能处理的问题更多:用户生命周期、组织架构、权限策略、多因素认证、单点登录、审计、协议兼容,这些都不是一个轻量小工具应该重新发明的东西。 但很多内部服务遇到的问题,并没有一开始就到这个规模。你可能只是有几个 HomeLab 服务、几个测试面板、一个内部文档站、一组临时工具,或者一个还没确定会不会长期使用的小平台。 为了这些东西第一天就接完整 IAM,有时会显得太重。不是能力不好,而是启动成本太高。 你可能要准备数据库、回调地址、证书、邮件服务、管理员账号、用户导入、策略配置,还要让每个服务都理解这套登录方式。最后很容易变成:明明只是想把几个人的访问控制住,却先搭了一整套身份中台。 所以,我想要的是一个中间方案。 它不把用户资料继续塞进认证入口里,也不要求每个服务自己维护一份名单;它可以从本地文件开始,也可以后面再接远程系统。它不负责完整身份治理,不负责登录页面,不负责发送验证码,也不负责校验 OTP。 它**只**先做好一件事:维护一份可以被系统查询的准入名册。 这份名单可以很小,也可以慢慢变大。一开始,它只是一个 `data.json`。后面,它可以接远程 API、Redis、多实例、监控和审计。 但不管后面怎么变,它回答的问题都很具体: - 这个人,在不在名单里? - 他现在是不是 `active`? - 后面的登录流程,应该继续,还是停在这里? 这就是 Warden 想站立的位置。 ## 为什么要把名单单独拆出来 做到这里,其实会有一个很自然的选择: 既然 Stargate 已经站在入口前面了,那能不能直接让 Stargate 维护这份名单? 当然可以。 如果只是几个人、几个邮箱、几个手机号,把它们写进 Stargate 的配置里,并不是不能用。甚至在最早期,这样可能还更省事。 但,我不太想让 Stargate 往这个方向长。 因为一旦认证入口开始维护用户资料,它很快就会多出很多原本不属于自己的事情。比如用户的邮箱是什么,手机号是什么,`user_id` 是什么,现在是不是 `active`,`role` 和 `scope` 要不要返回给下游服务,名单来自本地文件还是远程系统。如果远程系统挂了,要不要回退到本地数据?多个来源里同一个用户冲突了,又应该以谁为准? 这些问题看起来都和“登录”有关,但它们其实不是入口认证本身。 Stargate 真正要做的事情,是守在门口。它应该关心请求有没有登录、会话是不是有效、没有登录时要不要跳转登录页、API 请求要不要返回 401、登录完成后怎么把用户带回原来的服务。 这些事情已经足够具体了。 如果再把用户资料、名单同步、状态判断、字段过滤、远程数据源、缓存和合并策略都塞进去,Stargate 会变得越来越重。最后它就不再只是一个入口认证网关,而会变成半个用户系统。 这不是我想要的方向。 所以我把“名单”这件事单独拆了出来。Warden 只关心一件事:**谁在准入名单里。** 它不负责展示登录页,不负责签发会话,不负责发送验证码,不负责校验 OTP,也不打算替代完整 IAM。它要做的是更小的一块:从某个地方拿到用户数据,把这份数据整理好,然后提供一个稳定的查询接口。 调用方可以问它几个很具体的问题: - 这个邮箱在不在; - 这个手机号对应谁; - 这个 `user_id` 是否存在; - 这个用户现在是不是 `active`; - 后续验证码应该发到哪里; - 如果下游服务需要,能不能拿到 `role` 和 `scope`。 Warden 回答这些问题。 至于拿到答案之后,要不要继续登录、要不要发验证码、要不要创建会话,那是调用方的事情。 这样拆开以后,后面替换数据源、验证码通道和入口逻辑时,都不需要互相牵连。 - Stargate 负责守入口。 - Warden 负责管名单。 - Herald 负责送验证码。 这个分工不是为了把系统拆得更复杂,恰恰相反,是为了让每一块都不要长得太快。Stargate 不需要知道名单到底来自哪里,Warden 不需要知道登录页长什么样,Herald 也不需要判断谁有资格登录。每个服务只把自己的事情做好,后面替换起来也会更容易。 比如,一开始你可以让 Warden 只读本地 `data.json`。等名单变多了,再把不同团队拆成多个 JSON 文件。再往后,如果已经有内部用户系统,可以让 Warden 去同步远程数据。如果服务变多了,再加 Redis、缓存、多实例和监控。 这些变化都不应该影响 Stargate 的入口逻辑。 同样,后面如果验证码通道从邮件换成短信,或者从某个服务商换到另一个服务商,也不应该影响 Warden 的数据来源。 **这就是拆开的好处。** 不是每个小工具都要做成平台,也不是每个内部服务都需要完整身份中台。很多时候,我们只是需要在共享口令和完整 IAM 之间,补上一个很薄、但能长期维护的中间层。 Warden 就是这个中间层。 它可以接在 Stargate 后面,也可以独立给其他系统用。内部网关可以查它,脚本可以查它,平台可以查它,验证码服务也可以查它。 只要你的问题是:这个邮箱、手机号或 `user_id`,现在还允不允许继续往下走?那么,Warden 就可以站在这里,先把这件事回答清楚。 ## Warden 是怎么工作的 Warden 的工作方式很直接:你给它一份用户数据,它把这份数据加载起来,然后通过 HTTP API 对外提供查询能力。 调用方可以用邮箱、手机号或用户 ID 来问它:这个人是不是在名单里?他现在是什么状态?后续验证应该发到哪里?如果下游服务需要,能不能拿到他的 `user_id`、`role` 和 `scope`? Warden 不负责决定整个登录流程。 它只是把准入名单这部分信息整理好,稳定地返回给调用方。至于拿到结果之后,是继续发验证码,还是直接拒绝登录,还是写入会话,那是 Stargate、Herald 或其他调用方要做的事情。 最小的数据文件可以很简单: ```json [ { "phone": "13800138000", "mail": "admin@example.com" } ] ``` 这已经能回答一个基础问题: > 这个手机号或邮箱,在不在名单里? 但真实使用时,我更建议至少把 `user_id` 和 `status` 写上。 比如: ```json [ { "phone": "13800138000", "mail": "admin@example.com", "user_id": "admin-001", "status": "active" } ] ``` 这样 Warden 就不只是一个联系方式列表,而是开始变成一份真正的准入名册。 稍微完整一点的数据,可以继续加上角色、权限范围和显示名称: ```json [ { "phone": "13800138000", "mail": "admin@example.com", "user_id": "admin-001", "status": "active", "scope": ["read", "write", "admin"], "role": "admin", "name": "管理员" }, { "phone": "13900139000", "mail": "user@example.com", "user_id": "user-001", "status": "active", "scope": ["read"], "role": "user", "name": "普通用户" } ] ``` 这里最重要的字段,是 `status`。 很多时候,名单里有没有这个人,和这个人现在能不能继续登录,不完全是一回事。 比如,一个人曾经在项目里,后来离开了。你可以直接把他从名单里删掉,但有时候,保留记录会更方便排查问题。这时就可以把状态从: ```json { "mail": "user@example.com", "status": "active" } ``` 改成: ```json { "mail": "user@example.com", "status": "inactive" } ``` 或者: ```json { "mail": "user@example.com", "status": "suspended" } ``` 这样调用方看到的就不是一个模糊的“查不到用户”,而是一个更明确的结果: > 这个人存在,但现在不应该继续登录。 这对排查问题很有用,也比直接删除更容易留下管理痕迹。 作为独立服务时,Warden 的流程大概是这样: ```Text 调用方拿到邮箱、手机号或 user_id ↓ 调用 Warden 查询用户 ↓ Warden 在准入名单里查找对应用户 ↓ Warden 返回用户状态、联系方式和基础身份信息 ↓ 调用方根据返回结果决定后续动作 ``` 如果只是给某个内部脚本或平台使用,这个流程已经够了。 脚本可以问: ```Text admin@example.com 是否在名单里? ``` 内部平台可以问: ```Text 这个 user_id 对应的角色是什么? ``` 验证码服务可以问: ```Text 后续验证码应该发到邮箱,还是手机号? ``` 这些问题都可以通过同一份名单回答。 如果 Warden 接在 Stargate 后面,流程会更像这样: ```Text 用户访问受保护服务 ↓ Stargate 发现用户还没有登录 ↓ 用户输入邮箱或手机号 ↓ Stargate 调用 Warden 查询用户 ↓ Warden 返回用户是否存在、当前状态和联系方式 ↓ Stargate 判断是否继续后续登录流程 ``` 通常来说,只有用户存在,并且状态是 `active` 时,登录流程才应该继续。如果用户不存在,流程应该停下来;如果用户存在,但状态是 `inactive` 或 `suspended`,也不应该继续发验证码。 这件事不应该靠共享口令来判断,也不应该靠人工确认。 它应该变成登录流程里一次稳定的机器查询。 如果后面再接 Herald,流程会再多一步: ```Text 用户访问受保护服务 ↓ Stargate 发现用户还没有登录 ↓ 用户输入邮箱或手机号 ↓ Stargate 调用 Warden 查询用户 ↓ Warden 返回用户状态和联系方式 ↓ Stargate 判断用户是否允许继续 ↓ 允许继续:Stargate 调用 Herald 发送验证码 ↓ 用户输入验证码 ↓ Stargate 完成会话 ``` 这个过程里,Warden 仍然只负责名单和状态。后续要不要发验证码、怎么创建会话,都交给调用方继续处理。 这样拆开以后,后面调整起来会轻很多。 比如,用户数据一开始来自本地 `data.json`,后来你想接远程 API,再后来你想接 Redis、缓存和多实例,这些变化主要发生在 Warden 里。Stargate 不需要知道名单到底从哪里来。 同样,如果验证码以后从邮件换成短信,或者从一个服务商换成另一个服务商,也主要是 Herald 的事情。Warden 仍然只需要返回联系方式和状态。 这就是我希望 Warden 保持的边界。 它不是登录系统,不是验证码系统,也不是完整身份中台。它只是在认证链路里,把原来最容易散落在配置、表格、脚本和聊天记录里的那部分东西,收成一份可以被机器查询的名单。 先让系统知道:**谁是这个人。** 以及,他现在还能不能继续往下走。 ## 用 Docker Compose 快速跑起来 理解了 Warden 的工作方式之后,我们先把它跑起来。 这一节,我们先不接 Stargate,不接 Herald,不接远程 API,也不启用 Redis。只做一件事: 让 Warden 能从本地文件里加载一份名单,然后回答“这个人是不是在名单里”。 先创建一个 `data.json`: ```bash cat > data.json < 星门启闭,守望验身,鸦使送信。 下一篇,我们继续聊“鸦使”:Herald。 --EOF