Warden(守望者)是一个轻量的准入名单数据服务。
它负责维护“谁可以登录”,以及这些人对应的邮箱、手机号、用户 ID、状态、角色和权限范围等信息。它可以接在 Stargate 后面,作为认证入口背后的准入名册;也可以独立运行,给内部网关、平台、脚本或其他系统提供统一的用户查询能力。
写在前面
上一篇里,我们聊了 Stargate。
它负责把认证逻辑前移到网关侧,让反向代理先替我们拦住请求,而不是让每个内部服务都重复实现登录、鉴权、跳转和会话判断。这一步解决的是一个很基础的问题:
有没有登录?
但门装上之后,很快会遇到下一个问题:
谁可以通过这道门?
一开始,用共享口令就够了。给 Stargate 配一个密码,知道密码的人能进,不知道密码的人进不来。对 HomeLab、测试环境、小团队内部工具,或者几个临时服务来说,这个方案很轻,也很容易跑通。
这没什么问题。
很多内部服务一开始都应该这么做。先把门装上,比继续裸奔要好得多。
但只要使用的人多起来,共享口令就开始变得不那么舒服。有人离开项目之后,要不要换密码?如果换密码,是不是所有还在使用的人都要一起更新?如果密码被复制到聊天记录、脚本、配置文件里,又该怎么算?
更麻烦的是,共享口令只能证明一件事:
这个人知道密码。
但它证明不了另一件事:
这个人是谁?他现在还应该被允许登录吗?
当问题走到这里,就不能只靠一串密码了。你需要一份名单。
这份名单不一定要很复杂。它可以先从一个本地 JSON 文件开始,里面写着谁可以登录,对应的邮箱、手机号、用户 ID、状态、角色和权限范围是什么。但它应该是独立的,可以被 Stargate 查询,也可以被其他内部系统查询;可以先用本地文件维护,后面再接远程数据源、Redis、多实例和监控。
我不希望 Stargate 变成用户数据库。
Stargate 应该继续做入口、会话和登录流程。用户是谁、现在还能不能进、验证码应该发到哪里,这些信息可以单独放到另一个地方。
这就是 Warden 想解决的事情。
它不是完整 IAM,也不想替代公司里的 SSO、LDAP、OIDC 或身份中台。它只先做好一件小事:
维护一份可以被机器稳定查询的准入名册。

开源项目地址: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 或其他调用方要做的事情。
最小的数据文件可以很简单:
[
{
"phone": "13800138000",
"mail": "admin@example.com"
}
]
这已经能回答一个基础问题:
这个手机号或邮箱,在不在名单里?
但真实使用时,我更建议至少把 user_id 和 status 写上。
比如:
[
{
"phone": "13800138000",
"mail": "admin@example.com",
"user_id": "admin-001",
"status": "active"
}
]
这样 Warden 就不只是一个联系方式列表,而是开始变成一份真正的准入名册。
稍微完整一点的数据,可以继续加上角色、权限范围和显示名称:
[
{
"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。
很多时候,名单里有没有这个人,和这个人现在能不能继续登录,不完全是一回事。
比如,一个人曾经在项目里,后来离开了。你可以直接把他从名单里删掉,但有时候,保留记录会更方便排查问题。这时就可以把状态从:
{
"mail": "user@example.com",
"status": "active"
}
改成:
{
"mail": "user@example.com",
"status": "inactive"
}
或者:
{
"mail": "user@example.com",
"status": "suspended"
}
这样调用方看到的就不是一个模糊的“查不到用户”,而是一个更明确的结果:
这个人存在,但现在不应该继续登录。
这对排查问题很有用,也比直接删除更容易留下管理痕迹。
作为独立服务时,Warden 的流程大概是这样:
调用方拿到邮箱、手机号或 user_id
↓
调用 Warden 查询用户
↓
Warden 在准入名单里查找对应用户
↓
Warden 返回用户状态、联系方式和基础身份信息
↓
调用方根据返回结果决定后续动作
如果只是给某个内部脚本或平台使用,这个流程已经够了。
脚本可以问:
admin@example.com 是否在名单里?
内部平台可以问:
这个 user_id 对应的角色是什么?
验证码服务可以问:
后续验证码应该发到邮箱,还是手机号?
这些问题都可以通过同一份名单回答。
如果 Warden 接在 Stargate 后面,流程会更像这样:
用户访问受保护服务
↓
Stargate 发现用户还没有登录
↓
用户输入邮箱或手机号
↓
Stargate 调用 Warden 查询用户
↓
Warden 返回用户是否存在、当前状态和联系方式
↓
Stargate 判断是否继续后续登录流程
通常来说,只有用户存在,并且状态是 active 时,登录流程才应该继续。如果用户不存在,流程应该停下来;如果用户存在,但状态是 inactive 或 suspended,也不应该继续发验证码。
这件事不应该靠共享口令来判断,也不应该靠人工确认。
它应该变成登录流程里一次稳定的机器查询。
如果后面再接 Herald,流程会再多一步:
用户访问受保护服务
↓
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:
cat > data.json <<EOF
[
{
"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": "普通用户"
}
]
EOF
这份文件里只有两个用户:第一个是管理员,第二个是普通用户。它们都有手机号、邮箱、用户 ID 和状态。
第一次跑起来时,重点不是把用户模型一次性想完整,而是确认 Warden 能正常加载数据、保护接口、返回查询结果。所以字段不用一开始就设计得太复杂,先让最短链路跑通。
接着创建 compose.yaml:
services:
warden:
image: ghcr.io/soulteary/warden:latest
container_name: warden
restart: unless-stopped
ports:
- "8081:8081"
environment:
- PORT=8081
- MODE=ONLY_LOCAL
- DATA_FILE=/app/data.json
- REDIS_ENABLED=false
- API_KEY=your-secret-api-key-here
- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
volumes:
- ./data.json:/app/data.json:ro
- /etc/localtime:/etc/localtime:ro
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8081/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
这里为了本地演示,直接把 8081 端口映射到了宿主机。
真实部署时,不建议把 Warden 直接暴露到公网。更合适的方式,是让它只跑在内部网络里,由 Stargate、内部网关或其他可信调用方访问。这里先映射端口,只是为了方便本地用 curl 验证。
这段配置里,第一次需要看懂的配置不多。
- MODE=ONLY_LOCAL
表示只使用本地数据文件,也就是刚才创建的 data.json。
- DATA_FILE=/app/data.json
表示 Warden 在容器里从这个路径读取用户数据。前面的 volumes 已经把本地的 ./data.json 挂载到了这个位置。
- REDIS_ENABLED=false
表示这个最小示例先不启用 Redis。本地文件、单实例、少量用户的场景里,先禁用它更容易排查问题。
- API_KEY=your-secret-api-key-here
表示查询用户数据时需要带上 API Key。Warden 管的是准入名单,不应该让任何人都能直接查。即使只是本地测试,也建议从第一天就把 API Key 带上。
- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
表示接口只返回这些字段。这个配置不是本地测试必须的,但我建议早点用起来。Warden 后面会越来越像一份内部身份目录,字段能少给就少给。
确认文件准备好之后,启动服务:
docker compose up -d
先看容器状态:
docker compose ps
如果服务正常,再看健康检查:
curl http://localhost:8081/health
正常情况下,会看到类似这样的结果:
{
"status": "ok",
"details": {
"redis": "disabled",
"data_loaded": true,
"user_count": 2
},
"mode": "ONLY_LOCAL"
}
这里重点看两个字段:data_loaded 是 true,说明本地名单已经加载成功;user_count 是 2,说明刚才写入的两个用户都被 Warden 读到了。
健康检查通过之后,再查一下完整名单:
curl -H "X-API-Key: your-secret-api-key-here" \
http://localhost:8081/
也可以查看当前合并后的 data.json:
curl -H "X-API-Key: your-secret-api-key-here" \
http://localhost:8081/data.json
这两个接口更适合排查数据是否加载正确。真实登录流程里,一般不会频繁拉完整名单,更多时候只需要查一个具体的人。
比如按邮箱查询:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?mail=admin@example.com"
按手机号查询:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?phone=13800138000"
或者按用户 ID 查询:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?user_id=admin-001"
如果后面准备接 Stargate、Herald 或其他登录流程,更建议先看这个接口:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/v1/lookup?identifier=admin@example.com"
identifier 可以是邮箱、手机号或用户 ID。返回结果大概会像这样:
{
"user_id": "admin-001",
"destination": {
"email": "admin@example.com",
"phone": "13800138000"
},
"status": "active",
"channel_hint": "sms",
"name": "管理员"
}
这里的 destination 是后续验证流程可能用到的联系方式。如果用户有手机号,channel_hint 可以提示后续流程优先走短信;如果只有邮箱,就可以走邮件。
当然,Warden 本身不负责发送验证码。
它只是把“这个人是谁、状态是什么、后续应该联系哪里”这几件事返回给调用方。
到这里,Warden 的最小链路就跑通了。我们已经验证了几件事:
Warden 能正常启动
Warden 能加载本地 data.json
Warden 能通过 API Key 保护查询接口
Warden 能按邮箱、手机号或 user_id 查到用户
Warden 能给登录流程返回 lookup 结果
这一步完成后,先不要急着接远程数据源,也不要急着上 Redis,更不要一上来就把所有服务都接进来。
我更建议先把这个最小示例跑顺。
可以改一下 data.json,加一个用户,把某个用户的 status 从 active 改成 inactive,再查一次 /v1/lookup。确认自己理解 Warden 怎么读数据、怎么返回状态之后,再继续接 Stargate。
基础设施工具最好一步一步来。
先让名单能被稳定查询,再让入口服务依赖这份名单。
常用配置说明
Warden 的配置方式比较灵活,可以用命令行参数,也可以用环境变量,还可以用配置文件。不过第一次使用时,不需要把所有配置都看完。
我建议先把配置分成两组。
第一组,是第一天就会用到的配置:
API_KEY
DATA_FILE / DATA_DIR
MODE
RESPONSE_FIELDS
第二组,是后面接远程数据源、多实例和长期运行时再看的配置:
CONFIG / KEY
REDIS_ENABLED / REDIS
PORT
这样拆开以后,配置会好理解很多。先让本地文件、API Key 和查询接口跑通,再去考虑远程数据源、Redis、多实例和监控。
API_KEY:先把查询接口保护起来
Warden 管的是准入名单。这份名单里可能有邮箱、手机号、用户 ID、角色、权限范围,后面还可能有更多内部字段,所以它不应该裸着给人查。
最小示例里也建议设置:
API_KEY=your-secret-api-key-here
查询时带上:
curl -H "X-API-Key: your-secret-api-key-here" \
http://localhost:8081/
也可以使用 Bearer 形式:
curl -H "Authorization: Bearer your-secret-api-key-here" \
http://localhost:8081/
本地测试时,示例密钥只是为了方便演示。但只要进入真实环境,就不要继续用这种占位值,也不要把 API_KEY 写进仓库。
Warden 本身不负责登录页面,也不签发会话,但它会回答“谁可以继续往下走”。这类接口从第一天开始就应该被保护起来。
DATA_FILE:从一个本地文件开始
最简单的数据来源,就是一个本地 JSON 文件。
默认可以从当前目录的 data.json 开始:
DATA_FILE=./data.json
在 Docker Compose 里,一般会把本地文件挂载进容器:
volumes:
- ./data.json:/app/data.json:ro
如果想写得更明确,也可以直接指定容器里的路径:
environment:
- DATA_FILE=/app/data.json
本地文件很适合 Warden 的第一步。数据量不大、变更不频繁、使用的人也不多时,一个 data.json 已经足够好用。
它的好处是直观。你能很清楚地看到名单里有哪些人,每个人的邮箱、手机号、状态、角色和权限范围是什么。调试时也简单,改完文件,重启服务,重新查一次接口,就能确认结果。
这比一开始就接远程 API、缓存、多实例要轻很多。
DATA_DIR:名单变长之后再拆开
等用户多起来之后,一个 data.json 可能会变得不太好维护。这时可以把名单拆成多个文件。
比如:
data/
admin.json
team-a.json
team-b.json
readonly-users.json
然后配置:
DATA_DIR=/app/data
Warden 会读取这个目录下的数据文件,并把它们合并成一份可以查询的名单。
这个方式很适合把管理员和普通用户分开维护,或者让不同团队各自维护自己的名单。你也可以把手工维护的数据和导出的数据分开放,后面排查问题时会更清楚:
- 这个用户来自哪一份名单;
- 这个状态是谁改的;
- 这个团队是不是已经把离开的成员移除了。
这些问题在一个很长的 JSON 文件里会比较难看出来。拆成目录之后,会好很多。
MODE:先只用本地数据
MODE 用来决定 Warden 怎么使用本地数据和远程数据。
第一次使用时,我建议直接用:
MODE=ONLY_LOCAL
也就是只读取本地文件。这最容易理解,也最方便排查问题。等本地文件、API Key、用户查询都跑顺之后,再考虑远程数据源。
常见模式可以先这样理解:
ONLY_LOCAL
只使用本地文件。
ONLY_REMOTE
只使用远程数据。
REMOTE_FIRST
远程数据优先,本地数据作为补充。
LOCAL_FIRST
本地数据优先,远程数据作为补充。
REMOTE_FIRST_ALLOW_REMOTE_FAILED
远程优先,但远程失败时允许回退到本地数据。
LOCAL_FIRST_ALLOW_REMOTE_FAILED
本地优先,但本地失败时允许回退到远程数据。
这些名字看起来有点长,但含义比较直接。如果你只是本地测试,选 ONLY_LOCAL。如果已经有一套内部用户系统,Warden 只是同步一份准入名单,可以考虑 ONLY_REMOTE 或 REMOTE_FIRST。
带 ALLOW_REMOTE_FAILED 的模式要谨慎。它会让系统更稳,但也可能带来另一个问题:远程系统里已经禁用的人,本地文件里是不是还存在?如果本地文件没有同步更新,回退时会不会放过不该放过的人?
所以生产环境里,不要只看“可用性”,还要看这份名单是不是仍然可信。
RESPONSE_FIELDS:不要把所有字段都返回出去
默认情况下,如果用户对象里有很多字段,调用方可能会拿到比较完整的数据。这在本地调试时很方便,但真实使用时不一定合适。
比如你的用户数据里可能有一些内部备注:
{
"mail": "admin@example.com",
"phone": "13800138000",
"user_id": "admin-001",
"status": "active",
"role": "admin",
"scope": ["read", "write", "admin"],
"name": "管理员",
"source": "manual",
"note": "临时加入测试环境"
}
Stargate 可能只需要知道:
user_id
mail
phone
status
scope
role
name
那就没有必要把 source、note 或其他内部字段都返回出去。可以用 RESPONSE_FIELDS 控制返回字段:
RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
这个配置很容易被忽略,但我建议尽早用起来。因为 Warden 的数据会越来越像一份内部身份目录,一旦调用方变多,字段暴露就应该收紧。
让每个调用方只拿到自己需要的字段,比“先全给出去,后面再收回来”要好很多。
CONFIG 和 KEY:接远程数据源
如果名单不再适合手工维护,或者你已经有内部用户系统,可以让 Warden 从远程接口读取用户数据。
比如:
CONFIG=https://example.com/api/users.json
如果远程接口需要认证,可以加上:
KEY=Bearer your-token-here
远程接口返回的数据格式,应该和本地 data.json 保持一致,也就是一组用户对象:
[
{
"mail": "admin@example.com",
"phone": "13800138000",
"user_id": "admin-001",
"status": "active",
"scope": ["read", "write", "admin"],
"role": "admin"
}
]
这样 Warden 不需要关心数据到底来自哪里。本地文件也好,远程 API 也好,最后都整理成同一份准入名单。
这也是把名单单独拆出来的好处。Stargate 不需要知道用户数据是手写的,还是从远程系统同步的。它只需要问 Warden:
这个人现在能不能继续往下走?
不过,远程数据源进入生产环境时要多注意几件事:尽量使用 HTTPS,远程接口需要认证,不要把 Token 写进仓库,不要跳过 TLS 校验,也不要把远程数据源指向不可信地址。
Warden 是认证链路里的一环。它的数据源如果不可信,后面的登录流程也就不可信了。
REDIS_ENABLED 和 REDIS:后面再考虑多实例
最小使用时,可以先禁用 Redis:
REDIS_ENABLED=false
如果只是一个 Warden 实例,读本地文件,用户数量也不多,先不用 Redis 会更容易排查问题。
等 Warden 开始长期运行,或者你准备做多实例部署时,再考虑打开:
REDIS_ENABLED=true
REDIS=warden-redis:6379
Redis 主要用于缓存、分布式锁和多实例场景。它不是第一天必须要有的东西,但当 Warden 变成多个服务都会依赖的准入名册时,它会让同步、缓存和实例之间的协调更稳一些。
这里还是同一个原则:先让最短链路跑通,再让它跑得更稳。
PORT:通常不用改
Warden 默认监听 8081。
PORT=8081
本地测试时,保持默认即可。如果你的环境里这个端口已经被占用,或者你想和其他服务统一端口规划,再改它。
比如:
PORT=18081
在 Docker Compose 里,也要同步调整映射关系:
ports:
- "18081:18081"
environment:
- PORT=18081
不过真实部署时,我更建议不要太依赖宿主机端口暴露。Warden 更适合跑在内部网络里,让 Stargate、内部网关或可信调用方通过服务名访问。
比如在同一个 Compose 网络里,Stargate 可以直接访问:
http://warden:8081
这样就不需要把 Warden 暴露给外部访问者。
一个更完整一点的配置示例
如果只是本地文件起步,配置可以保持很小:
environment:
- PORT=8081
- MODE=ONLY_LOCAL
- DATA_FILE=/app/data.json
- REDIS_ENABLED=false
- API_KEY=your-secret-api-key-here
- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
如果后面改成远程数据源,可以变成:
environment:
- PORT=8081
- MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED
- DATA_FILE=/app/data.json
- CONFIG=https://example.com/api/users.json
- KEY=Bearer your-token-here
- REDIS_ENABLED=false
- API_KEY=your-secret-api-key-here
- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
如果再往后需要多实例和缓存,可以继续加 Redis:
environment:
- PORT=8081
- MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED
- DATA_FILE=/app/data.json
- CONFIG=https://example.com/api/users.json
- KEY=Bearer your-token-here
- REDIS_ENABLED=true
- REDIS=warden-redis:6379
- API_KEY=your-secret-api-key-here
- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
但,我不建议一开始就用最后这一版。
第一次跑 Warden,最重要的是确认三件事:
数据能不能加载
接口能不能查询
返回结果是不是你预期的那个人
这些都确认之后,再去考虑远程数据、Redis、多实例和监控。配置不是越多越好,能解释清楚每一项为什么存在,才比较重要。
作为独立服务怎么用
Warden 不一定要接在 Stargate 后面。它本身也可以作为一个很小的准入数据服务,给内部系统使用。
比如,你有一个内部网关,需要判断某个邮箱能不能访问;或者有一个自动化脚本,需要确认某个手机号是不是还在允许名单里;又或者有一个内部平台,只想从统一的地方拿到用户的 user_id、role 和 scope。
这些场景里,Warden 都可以独立工作。
它不关心调用方是不是 Stargate。只要调用方能带着 API Key 来查,它就负责回答几个问题:
- 这个人是不是在名单里;
- 这个人现在是什么状态;
- 后续流程需要联系他时,应该用邮箱还是手机号;
- 下游系统需要用户信息时,能不能拿到稳定的
user_id。
常用接口其实不多。
查询当前名单
如果你想看 Warden 当前加载出来的完整名单,可以访问根路径:
curl -H "X-API-Key: your-secret-api-key-here" \
http://localhost:8081/
也可以访问合并后的 data.json:
curl -H "X-API-Key: your-secret-api-key-here" \
http://localhost:8081/data.json
这两个接口更适合排查数据。比如你刚改完本地文件,或者刚接入远程数据源,想确认 Warden 最终看到的名单是什么样。
如果用户数量比较多,也可以分页查询:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/?page=1&page_size=100"
不过真实登录流程里,一般不会频繁拉完整名单。更多时候,调用方只需要查一个具体的人。
查询单个用户
Warden 支持按手机号、邮箱或用户 ID 查询用户。
按手机号查:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?phone=13800138000"
按邮箱查:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?mail=admin@example.com"
按用户 ID 查:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?user_id=admin-001"
这个接口适合内部系统做精确查询。比如内部平台已经知道当前用户的邮箱,只想查他的 role 和 scope;或者某个脚本拿到了手机号,想确认这个人是不是还在准入名单里。
返回结果里通常会包含用户对象中的字段:
{
"phone": "13800138000",
"mail": "admin@example.com",
"user_id": "admin-001",
"status": "active",
"scope": ["read", "write", "admin"],
"role": "admin",
"name": "管理员"
}
这里需要注意一件事:
查到用户,不等于一定应该继续放行。
调用方还应该看 status。一般来说,只有 active 状态才应该继续往下走。如果是 inactive 或 suspended,即使这个人还在名单里,也不应该继续登录或访问。
这也是我建议保留 status 字段的原因。它能把“查不到用户”和“用户存在但不允许继续访问”区分开。
给登录流程用的 lookup
如果调用方是在做登录流程,推荐使用 /v1/lookup。
它的输入更简单:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/v1/lookup?identifier=admin@example.com"
identifier 可以是邮箱、手机号,也可以是用户 ID。调用方不需要提前判断用户输入的是哪一种。Warden 会尝试在名单里匹配对应用户,然后返回更适合登录流程使用的数据。
比如:
{
"user_id": "admin-001",
"destination": {
"email": "admin@example.com",
"phone": "13800138000"
},
"status": "active",
"channel_hint": "sms",
"name": "管理员"
}
这里的 destination 是后续验证流程可以使用的联系方式。如果用户有手机号,后面可以走短信;如果只有邮箱,后面可以走邮件。channel_hint 只是提示后续流程更适合使用哪个通道。
Warden 自己不发送验证码。
它只是把“可以联系到哪里”这件事告诉调用方。
比如 Stargate 可以先调用 Warden,确认这个邮箱是不是在名单里、用户现在是不是 active、验证码应该发到哪里。拿到结果之后,再决定要不要继续调用 Herald 发送验证码。
内部平台也可以这样用:确认某个 user_id 是否有效,判断用户有没有 admin scope,或者在用户状态是 suspended 时停止后续流程。
Warden 的价值不在于它有很多接口,而在于这些系统不用再各自维护一份名单。同一个用户,尽量从同一份准入数据里查出来。
健康检查
Warden 也提供健康检查接口:
curl http://localhost:8081/health
或者:
curl http://localhost:8081/healthcheck
这类接口适合接入 Docker、Kubernetes、反向代理或监控系统。本地调试时,你可以直接用它确认服务是否启动、数据是否加载。
比如:
{
"status": "ok",
"details": {
"redis": "disabled",
"data_loaded": true,
"user_count": 2
},
"mode": "ONLY_LOCAL"
}
这里重点看 data_loaded 和 user_count。如果 data_loaded 是 false,说明数据文件没有正确加载;如果 user_count 不符合预期,说明名单内容可能没有被正确读取,或者远程数据源没有按预期返回。
但生产环境里,不建议把健康检查直接暴露给公网。它虽然不是用户查询接口,但仍然可能暴露服务状态、数据加载情况和运行模式。更合适的做法,是只允许内部网络、反向代理或监控系统访问。
一个独立使用的小例子
假设你有一个内部脚本,准备在执行敏感操作前确认某个邮箱是否还在准入名单里。它可以先调用:
curl -s -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/v1/lookup?identifier=admin@example.com"
然后检查返回里的 status:
status == active 继续执行
status != active 停止执行
用户不存在 停止执行
这样脚本不需要自己维护一份邮箱列表,也不需要知道名单到底来自本地文件,还是远程 API。它只需要问 Warden。
再比如,你有一个内部平台,需要展示当前用户的角色和权限范围。它也可以按邮箱或 user_id 查询:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?user_id=admin-001"
拿到结果后,只使用自己需要的字段:
{
"user_id": "admin-001",
"status": "active",
"role": "admin",
"scope": ["read", "write", "admin"]
}
这时最好配合前面说的 RESPONSE_FIELDS。
一个内部脚本可能只需要 status,一个登录流程可能只需要 user_id、destination 和 status,一个后端服务可能只需要 role 和 scope。字段能少给,就少给。
这不是为了把系统做复杂,而是因为 Warden 后面会越来越接近一份内部身份目录。一旦调用方变多,最开始随手返回出去的字段,后面再想收回来就比较麻烦。
所以独立使用 Warden 时,我会把它当成一个很薄的内部能力:它不负责登录,不负责验证码,不负责会话,也不负责复杂权限策略。它只负责让其他系统不用再各自维护一份名单。
大家都问同一个地方:
- 这个人是谁?
- 他现在还能不能继续往下走?
需要的话,应该把哪些基础身份信息交给下游?
接到 Stargate 后会变成什么样
前面我们一直把 Warden 当成一个独立服务来看。它能加载名单,能查询用户,能返回状态、联系方式、角色和权限范围。
但如果你已经按照上一篇把 Stargate 跑起来,那么接入 Warden 之后,登录流程会多一个很关键的判断:
这个输入的邮箱、手机号或 user_id,是不是还在准入名单里?
上一篇里,Stargate 可以只靠共享口令完成登录。这很轻,也很适合第一步。但它的判断方式比较简单:
你知道密码吗?
接入 Warden 之后,判断方式会变成:
你是谁?
你在不在名单里?
你现在是不是 active?
后续验证应该发到哪里?
这不是把 Stargate 变复杂,而是把原来塞不进共享口令里的那部分信息,交给 Warden 来回答。
原来的流程大概是:
用户访问受保护服务
↓
Stargate 发现用户还没有登录
↓
用户输入共享口令
↓
口令正确,创建会话
↓
回到原来的服务
接入 Warden 之后,流程会变成:
用户访问受保护服务
↓
Stargate 发现用户还没有登录
↓
用户输入邮箱、手机号或 user_id
↓
Stargate 调用 Warden 查询用户
↓
Warden 返回用户是否存在、当前状态和联系方式
↓
Stargate 根据结果决定是否继续登录流程
如果用户不存在,流程应该停下来。如果用户存在,但 status 不是 active,流程也应该停下来。只有用户存在,并且状态正常,后面才继续做验证、创建会话或进入其他登录步骤。
这样一来,Stargate 仍然负责入口和会话,Warden 只负责回答名单问题。Stargate 不需要自己维护用户数据,也不需要知道名单来自本地文件,还是远程 API。
它只需要在登录流程里问一次 Warden:这个人现在还能不能继续往下走?
Stargate 侧怎么配置
Stargate 侧启用 Warden,大概需要这些配置:
WARDEN_ENABLED=true
WARDEN_URL=http://warden:8081
WARDEN_API_KEY=your-secret-api-key-here
WARDEN_CACHE_TTL=300
WARDEN_ENABLED 表示启用 Warden 查询:
WARDEN_ENABLED=true
不开这个开关时,Stargate 可以继续按原来的共享口令方式工作。打开之后,Stargate 就会在登录流程里增加 Warden 查询。
WARDEN_URL 表示 Warden 服务地址:
WARDEN_URL=http://warden:8081
如果 Stargate 和 Warden 在同一个 Docker Compose 网络里,这里通常可以直接使用服务名:
http://warden:8081
不一定需要把 Warden 暴露到宿主机端口。真实部署时,我也更建议这样做。Warden 只给内部服务访问,不直接对公网开放。
WARDEN_API_KEY 需要和 Warden 侧的 API_KEY 保持一致:
WARDEN_API_KEY=your-secret-api-key-here
也就是说,Warden 里配置的是:
API_KEY=your-secret-api-key-here
Stargate 里配置的是:
WARDEN_API_KEY=your-secret-api-key-here
这两个值不一致时,Stargate 就查不到 Warden。这类问题排查起来很容易绕路,所以第一次接入时,建议先不要同时打开太多能力。先确认 Stargate 能访问 Warden,再确认 API Key 没写错,最后再看用户状态和后续验证流程。
WARDEN_CACHE_TTL 用来控制缓存时间:
WARDEN_CACHE_TTL=300
这里的 300 通常表示缓存 300 秒(具体缓存行为以 Stargate 当前版本实现为准)。缓存的好处是减少重复查询,比如同一个用户短时间内多次触发登录流程,Stargate 不需要每次都去问 Warden。
但缓存也有代价。如果你刚刚把某个用户从 active 改成 inactive,Stargate 可能会在缓存过期前继续使用旧结果。所以这个值不要随手设得太长。内部测试环境可以稍微宽松一点,对准入变更比较敏感的环境,缓存时间就应该短一些,甚至先关掉缓存做验证。
一个简单的组合示例
如果把 Stargate 和 Warden 放在同一个 Compose 项目里,结构可以类似这样:
services:
stargate:
image: soulteary/stargate:latest
container_name: stargate
restart: unless-stopped
environment:
- AUTH_HOST=auth.test.localhost
- LANGUAGE=zh
- WARDEN_ENABLED=true
- WARDEN_URL=http://warden:8081
- WARDEN_API_KEY=your-secret-api-key-here
- WARDEN_CACHE_TTL=300
networks:
- proxy
- auth-internal
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.stargate.entrypoints=http
- traefik.http.routers.stargate.rule=Host(`auth.test.localhost`) || Path(`/_session_exchange`)
- traefik.http.routers.stargate.service=stargate
- traefik.http.services.stargate.loadbalancer.server.port=80
- traefik.http.middlewares.stargate.forwardauth.address=http://stargate/_auth
- traefik.http.middlewares.stargate.forwardauth.trustForwardHeader=true
warden:
image: ghcr.io/soulteary/warden:latest
container_name: warden
restart: unless-stopped
environment:
- PORT=8081
- MODE=ONLY_LOCAL
- DATA_FILE=/app/data.json
- REDIS_ENABLED=false
- API_KEY=your-secret-api-key-here
- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
volumes:
- ./data.json:/app/data.json:ro
- /etc/localtime:/etc/localtime:ro
networks:
- auth-internal
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8081/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
networks:
proxy:
external: true
auth-internal:
internal: true
这只是一个示意配置,关键点有两个。
第一,Stargate 同时在 proxy 和 auth-internal 网络里。它需要通过 proxy 被 Traefik 访问,也需要通过 auth-internal 访问 Warden。
第二,Warden 只在 auth-internal 网络里。它不需要挂 Traefik label,也不需要暴露 ports,因为正常情况下,外部用户不应该直接访问 Warden。
真正会访问 Warden 的,是 Stargate 或其他可信内部调用方。
这和前面本地演示不一样。本地演示为了方便,我们用了:
ports:
- "8081:8081"
真实部署时,如果 Stargate 和 Warden 在同一个 Docker 网络里,就可以去掉端口暴露。这样访问路径会更清楚:
用户浏览器
↓
Traefik
↓
Stargate
↓
Warden
用户能看到 Stargate 的登录页面,但看不到 Warden。
用户状态怎么影响登录
接入 Warden 之后,status 就变得很重要了。
假设名单里有一个用户:
{
"mail": "admin@example.com",
"phone": "13800138000",
"user_id": "admin-001",
"status": "active",
"role": "admin",
"scope": ["read", "write", "admin"],
"name": "管理员"
}
当用户输入 admin@example.com 时,Stargate 会去问 Warden。如果 Warden 返回:
{
"user_id": "admin-001",
"destination": {
"email": "admin@example.com",
"phone": "13800138000"
},
"status": "active",
"channel_hint": "sms",
"name": "管理员"
}
Stargate 就可以继续后面的登录流程。
如果你把这个用户改成:
{
"mail": "admin@example.com",
"phone": "13800138000",
"user_id": "admin-001",
"status": "inactive",
"role": "admin",
"scope": ["read", "write", "admin"],
"name": "管理员"
}
那他虽然还在名单里,但不应该继续登录。
这和直接删除用户不一样。直接删除时,系统只能知道:
查不到这个人
改成 inactive 时,系统能知道:
这个人存在,但现在不允许继续访问
对排查问题来说,后者会清楚很多。尤其是多人协作时,有人反馈“我为什么登录不了”,你可以直接从名单里看到他的状态,而不是反复猜他是不是输错了邮箱、手机号或密码。
role 和 scope 怎么用
Warden 返回 role 和 scope,并不代表它要变成权限系统。它只是把基础身份信息提供给调用方。真正要不要使用这些字段,要看后面的系统怎么设计。
比如,Stargate 可以在认证成功后,把一部分用户信息写入请求头,再交给后端服务。后端服务可以看到:
X-Forwarded-User-Id: admin-001
X-Forwarded-Email: admin@example.com
X-Forwarded-Role: admin
如果你使用 Traefik ForwardAuth,还需要在 middleware 里显式配置允许转发哪些响应头。比如:
- traefik.http.middlewares.stargate.forwardauth.authResponseHeaders=X-Forwarded-User,X-Forwarded-Email,X-Forwarded-User-Id,X-Forwarded-Role
这件事要谨慎。
请求头一旦进入后端服务,就会被后端当成可信身份信息。所以后端服务应该只接受来自反向代理的流量,不要让用户绕过 Traefik 直接访问后端。否则,用户就有机会自己伪造这些请求头。
这个问题和 Stargate 本身一样:
入口要统一。
后端不要裸露。
Warden 只负责提供身份信息,不能替你修复绕过入口的问题。
接入时最容易出错的地方
第一次把 Warden 接到 Stargate 后面,最容易出问题的地方通常不是代码,而是网络和配置。
比如,Stargate 访问不到 Warden。这时可以进到 Stargate 容器里测试:
wget -qO- http://warden:8081/health
如果这里都访问不了,先看 Docker 网络,不要急着看登录逻辑。
再比如,API Key 不一致。Warden 配的是:
API_KEY=abc
Stargate 配的是:
WARDEN_API_KEY=abcd
这种情况下,Warden 会拒绝查询。看起来像是“用户不存在”或者“登录失败”,但根因其实是服务间鉴权没通过。
还有一种常见问题,是用户状态不对。名单里有这个人,但状态是:
"status": "inactive"
或者字段名写错了。比如写成了:
"email": "admin@example.com"
但 Warden 期望的是:
"mail": "admin@example.com"
这类问题最好先用 /v1/lookup 单独验证:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/v1/lookup?identifier=admin@example.com"
确认 Warden 自己能返回正确结果之后,再去看 Stargate。
基础设施排错时,我一般会把链路拆开看:
Warden 自己能不能查到用户?
↓
Stargate 能不能访问 Warden?
↓
Stargate 带的 API Key 对不对?
↓
用户 status 是不是 active?
↓
后续登录流程有没有继续?
不要一上来就从浏览器登录失败开始猜。链路越长,越要一段一段确认。
接入之后,Stargate 还是 Stargate
最后再强调一下边界。
接入 Warden 之后,Stargate 并没有变成用户系统,它只是多了一个准入判断来源。用户数据仍然在 Warden,登录流程仍然在 Stargate。后续如果再接 Herald,验证码发送和校验也应该交给 Herald。
这样拆开之后,后面替换起来会比较轻。你可以先用本地 data.json 跑 Warden,等名单变多了,再让 Warden 接远程 API。你可以先用 Stargate 的简单登录流程,等多人使用了,再加验证码。你也可以先不传 role 和 scope 给后端,等确实有服务需要这些字段,再通过请求头往下传。
不要第一天就把所有能力都打开。
先让 Stargate 能问 Warden。
先让 Warden 能稳定回答:
这个人还在不在名单里。
他现在还能不能继续往下走。
这一步跑通之后,再继续接 Herald。
真实使用时的几个建议
如果你准备把 Warden 放进自己的环境里,我建议不要一上来就把所有东西都接上。
基础设施类工具最怕的不是能力不够,而是第一步就铺得太大。一开始就接 Stargate、接 Herald、接远程数据源、接 Redis、配多实例、开监控,看起来很完整,但一旦出了问题,排查起来会很麻烦。
你很难判断问题到底出在哪里:是 Warden 没读到数据,是 API Key 不对,是 Stargate 访问不到 Warden,是用户状态不是 active,是 Herald 没把验证码发出去,还是反向代理和网络本身有问题。
所以我更建议小步接入。
先让名单自己跑起来,再让入口服务依赖这份名单,最后再把验证码、远程数据源、多实例和监控这些能力补上。
第一步,先用本地文件跑通
第一次使用 Warden,不要急着接远程 API,也不要急着接 Stargate。先准备一个最简单的 data.json,里面放一两个用户。
比如:
[
{
"phone": "13800138000",
"mail": "admin@example.com",
"user_id": "admin-001",
"status": "active",
"scope": ["read", "write", "admin"],
"role": "admin",
"name": "管理员"
}
]
然后用 MODE=ONLY_LOCAL 启动 Warden。
这一步只验证一件事:
Warden 能不能稳定地回答“这个人是不是在名单里”。
可以依次确认几个接口:
curl http://localhost:8081/health
确认服务启动正常。
curl -H "X-API-Key: your-secret-api-key-here" \
http://localhost:8081/
确认名单被加载。
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/user?mail=admin@example.com"
确认可以按邮箱查到用户。
curl -H "X-API-Key: your-secret-api-key-here" \
"http://localhost:8081/v1/lookup?identifier=admin@example.com"
确认登录流程用的查询接口也能返回预期结果。
这一步跑通之前,不要急着接其他东西。因为 Warden 自己如果都还没稳定返回数据,后面接 Stargate 或 Herald,只会把问题藏得更深。
先把最短链路跑顺。
这比一次性把配置写完整更重要。
第二步,把 status 用起来
不要只把 Warden 当成邮箱和手机号列表。如果只是维护联系方式,那它很快会变成另一个通讯录。
Warden 更重要的价值,是让名单里的人有状态。
比如:
{
"mail": "user@example.com",
"phone": "13900139000",
"user_id": "user-001",
"status": "active"
}
当这个人还应该继续访问内部服务时,状态是 active。如果他已经离开项目,或者暂时不应该继续登录,可以改成:
{
"mail": "user@example.com",
"phone": "13900139000",
"user_id": "user-001",
"status": "inactive"
}
或者:
{
"mail": "user@example.com",
"phone": "13900139000",
"user_id": "user-001",
"status": "suspended"
}
这样做比直接删除更容易排查问题。
直接删除时,系统只能知道:
查不到这个人。
保留用户并修改状态时,系统能知道:
这个人存在,但现在不允许继续访问。
这两个结果在管理上是不一样的。前者更像数据缺失,后者更像明确拒绝。
多人使用时,这个区别很有用。有人反馈“我为什么登录不了”,你可以直接看名单里的状态,而不是反复猜他是不是输错了邮箱、手机号,或者是不是某个配置没生效。
所以我建议从一开始就把 status 当成必填字段。
哪怕你的名单现在只有两个人,也最好写上。后面用户变多时,这个习惯会省很多事。
第三步,再接 Stargate 或其他调用方
等 Warden 自己跑稳之后,再让 Stargate、内部网关、脚本或平台来调用它。
这一步重点不是“登录页面好不好看”,而是确认服务之间的调用链路是不是可靠。
比如先确认 Stargate 能访问 Warden:
wget -qO- http://warden:8081/health
如果这里访问不了,先看 Docker 网络、服务名、端口和网络隔离,不要急着看登录逻辑。
然后确认 API Key 是否一致。
Warden 侧配置的是:
API_KEY=your-secret-api-key-here
Stargate 侧就应该是:
WARDEN_API_KEY=your-secret-api-key-here
这两个值不一致时,Stargate 查询 Warden 会失败。看起来可能像是“用户不存在”或者“登录失败”,但根因其实只是服务间鉴权没通过。
再确认用户状态。名单里有这个人,不代表他应该继续登录。如果返回的是:
{
"status": "inactive"
}
或者:
{
"status": "suspended"
}
那调用方就应该停下来。
最后再确认返回字段是否符合预期。比如 Stargate 需要 user_id、mail、phone 和 status,下游服务可能还需要 role 和 scope,那就检查 RESPONSE_FIELDS 有没有把这些字段放出来。
可以先用 /v1/lookup 单独验证:
curl -H "X-API-Key: your-secret-api-key-here" \
"http://warden:8081/v1/lookup?identifier=admin@example.com"
确认 Warden 返回正确之后,再去看 Stargate 的登录流程。
排查时尽量按顺序拆开:
Warden 自己能不能查到用户?
↓
调用方能不能访问 Warden?
↓
API Key 是否一致?
↓
用户状态是不是 active?
↓
返回字段是否够用?
↓
后续登录流程有没有继续?
不要一开始就从浏览器里的登录失败开始猜。
链路越长,越要一段一段确认。
第四步,再接 Herald
当你确认 Warden 已经能稳定回答“谁可以登录”之后,再接 Herald。
这时登录流程就可以从共享口令,逐步变成更适合多人使用的方式:
用户输入邮箱或手机号
↓
Stargate 调用 Warden 查询用户
↓
Warden 确认用户存在,并且状态是 active
↓
Stargate 调用 Herald 发送验证码
↓
用户输入验证码
↓
Stargate 创建会话
这里 Warden 仍然不发送验证码。它只返回用户状态和联系方式。
比如:
{
"user_id": "admin-001",
"destination": {
"email": "admin@example.com",
"phone": "13800138000"
},
"status": "active",
"channel_hint": "sms",
"name": "管理员"
}
Herald 再根据这些信息决定怎么发送验证码:发短信,还是发邮件;验证码多久过期;失败几次要不要限制。这些都不应该塞进 Warden。
Warden 只管名单,Herald 只管送信,Stargate 只管登录流程和会话。
这样拆开之后,后面替换起来会轻很多。短信服务商换了,是 Herald 的事情;名单从本地文件换成远程 API,是 Warden 的事情;登录页、Cookie、ForwardAuth 和会话,是 Stargate 的事情。
不要让一个工具因为“顺手”就开始管太多。
一开始看起来省事,后面会很难拆。
第五步,再考虑远程数据源、Redis 和多实例
如果只是少量用户,本地文件已经很好用。尤其是 HomeLab、测试环境、小团队内部工具,或者几个临时服务,一个 data.json 加上 Git 记录,可能就够用了。
等名单开始变多,或者已经有其他系统在维护用户数据,再考虑远程数据源。
比如:
CONFIG=https://example.com/api/users.json
KEY=Bearer your-token-here
MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED
这时 Warden 可以从远程接口同步名单,也可以在远程异常时回退到本地数据。
但回退策略要谨慎。
它不是只有好处。如果远程系统里已经禁用了某个用户,而本地文件里还保留着旧的 active 状态,那么远程失败后回退到本地数据,就可能带来风险。
所以只要启用回退,就要想清楚几个问题:
- 本地数据是不是可信;
- 本地数据多久更新一次;
- 远程和本地冲突时,以谁为准;
- 远程失败时,是宁可拒绝登录,还是允许使用本地兜底。
这些不是配置问题,而是准入策略问题。
等 Warden 被多个系统依赖,或者需要多实例运行时,再考虑 Redis。
REDIS_ENABLED=true
REDIS=warden-redis:6379
Redis 可以帮助做缓存、分布式锁和多实例协调,但它不应该成为第一天的门槛。如果你只是想确认 Warden 能不能读一个 JSON 文件、查一个邮箱、返回一个状态,Redis 只会增加排查成本。
等最小链路稳定之后,再让系统变得更稳。
同样,OpenTelemetry、Prometheus、审计日志、健康检查访问限制,也都适合后面再加。这些能力很重要,但它们不应该挡在第一步前面。
我更建议的顺序是:
本地文件
↓
status 状态
↓
Stargate 或其他调用方
↓
Herald
↓
远程数据源
↓
Redis、多实例、监控和审计
这条路径不是最完整的,但它比较容易排错,也比较容易建立信心。
最后再提醒一句:Warden 不是完整 IAM。
它不管理组织架构,不负责复杂权限策略,不处理 OIDC、SAML,不提供完整管理后台,也不应该替代公司里已经成熟的 SSO 或身份中台。
如果你已经有这些系统,应该优先使用它们。
Warden 更适合站在中间地带:
你还没有必要引入完整身份系统,但已经不想继续靠共享口令、表格、聊天记录和散落在各个服务里的配置维护准入关系。
这时 Warden 可以先把名单收起来,让它变成一个可以被机器查询、可以被多个系统复用、可以逐步替换数据来源的小服务。
先别急着把它做大。
让它先把“谁还能进”这件事回答清楚。
上线前的最小安全检查
Warden 管的是准入名单,位置比普通内部小工具更敏感。
它本身不负责登录页面,也不签发会话,但会告诉调用方:这个人是谁、现在是不是 active、后续验证应该联系哪里,以及下游服务能不能拿到 role 和 scope。
所以 Warden 不应该裸奔,哪怕它只跑在内网里,也应该先把最基本的安全边界立起来。
查询接口必须有 API Key
第一件事,是确认 API_KEY 已经换成真实密钥,并且所有用户查询接口都必须带上它。不要让任何人都能直接查询用户名单。
至少下面这些接口,都应该被保护起来:
GET /
GET /data.json
GET /user
GET /v1/lookup
本地测试时,你可能会写:
API_KEY=your-secret-api-key-here
本地示例里的 your-secret-api-key-here 只适合演示,不要带进真实环境。真实环境里,不要继续用这种占位值,也不要用太短、太容易猜的字符串。
调用时带上:
curl -H "X-API-Key: your-real-api-key" \
http://warden:8081/v1/lookup?identifier=admin@example.com
或者使用 Bearer 形式:
curl -H "Authorization: Bearer your-real-api-key" \
http://warden:8081/v1/lookup?identifier=admin@example.com
这一步看起来很基础,但很重要。Warden 不是公开通讯录,更不是让外部用户随便探测“某个邮箱在不在名单里”的接口。
哪怕只是内部服务调用,也应该先有服务间鉴权。
不要把 Warden 直接暴露到公网
本地演示时,为了方便测试,我们用了:
ports:
- "8081:8081"
这样浏览器和 curl 都能直接访问 Warden。
但真实部署时,我不建议这样做。Warden 更适合只跑在内部网络里,由 Stargate、内部网关或其他可信调用方访问。
比如在 Docker Compose 里,让 Stargate 和 Warden 共享一个内部网络:
networks:
auth-internal:
internal: true
然后 Warden 只加入这个网络:
services:
warden:
image: ghcr.io/soulteary/warden:latest
networks:
- auth-internal
Stargate 也加入这个网络:
services:
stargate:
image: soulteary/stargate:latest
networks:
- proxy
- auth-internal
这样访问链路会更清楚:
用户浏览器
↓
Traefik / Nginx
↓
Stargate
↓
Warden
用户能访问的是入口服务,Warden 只给 Stargate 或其他可信内部调用方访问。
如果你的环境里确实需要跨机器访问 Warden,也尽量通过内网地址、VPN、专线、服务网格或受控反向代理来做。不要把它直接挂到公网域名下面。
如果不得不暴露,也至少要同时考虑:
TLS
API Key
IP 白名单
反向代理访问控制
速率限制
访问日志
不要只依赖“接口路径没人知道”。
这不是安全边界。
健康检查也不要随便暴露
Warden 的健康检查很方便:
curl http://localhost:8081/health
或者:
curl http://localhost:8081/healthcheck
它能告诉你服务是否正常、数据是否加载、Redis 是否启用、当前模式是什么。这对运维和排查问题很有用,但它也可能暴露一些你不想给外部知道的信息。
比如:
{
"status": "ok",
"details": {
"redis": "disabled",
"data_loaded": true,
"user_count": 2
},
"mode": "ONLY_LOCAL"
}
这里虽然没有直接泄露用户数据,但已经能看出服务状态、数据加载情况和运行模式。
所以生产环境里,不建议把健康检查直接暴露给公网。更合适的方式,是只允许本机、内网、反向代理或监控系统访问。
可以使用 Warden 的健康检查白名单:
HEALTH_CHECK_IP_WHITELIST=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
也可以在 Nginx、Traefik、VPN 或内网安全组层面再做一层访问限制。
控制返回字段,别把所有信息都给出去
Warden 的用户对象可能会慢慢变多。
一开始只有:
{
"mail": "admin@example.com",
"phone": "13800138000",
"status": "active"
}
后面可能会加上:
{
"user_id": "admin-001",
"role": "admin",
"scope": ["read", "write", "admin"],
"name": "管理员",
"source": "manual",
"note": "临时加入测试环境"
}
调试时,返回完整对象很方便。但真实环境里,不一定应该把所有字段都返回给所有调用方。
比如 Stargate 可能只需要:
user_id
mail
phone
status
scope
role
name
那就可以配置:
RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
不要把 note、source、内部标记、同步来源、管理备注这类字段顺手给出去。
尤其是当 Warden 不只被 Stargate 调用,而是被多个内部系统复用时,更应该收紧返回字段。
字段暴露出去很容易,后面想收回来会麻烦很多。
远程数据源要可信
如果只用本地 data.json,风险主要在文件和部署环境。但如果开始接远程数据源,就要多想一步。
比如:
CONFIG=https://example.com/api/users.json
KEY=Bearer your-token-here
这里的远程接口会变成 Warden 的数据来源。也就是说,它会影响后续登录流程里“谁可以继续往下走”。
所以生产环境里,远程数据源至少应该满足几件事:
使用 HTTPS
接口需要认证
Token 不写进仓库
不跳过 TLS 校验
不指向不可信地址
远程返回的数据结构可控
不要从一个不受控的公开地址加载名单,也不要为了“先跑起来”就关闭 TLS 校验。
Warden 自己再怎么保护接口,如果数据源不可信,整条准入链路也会变得不可信。
另外,如果你用了远程失败回退模式,比如:
MODE=REMOTE_FIRST_ALLOW_REMOTE_FAILED
也要想清楚本地兜底数据是不是仍然可信。
远程系统里已经禁用的人,本地文件里是不是还保留着旧的 active 状态?如果远程挂了,回退到本地时,会不会把不该放行的人重新放进来?
这类问题不是 Warden 能替你自动决定的。它属于准入策略,你需要根据自己的环境做取舍。
对某些场景来说,远程失败时宁可拒绝登录;对另一些内部测试环境来说,本地兜底可能更实用。
关键是不要无意识地开启回退。
密钥不要进仓库
Warden 相关的密钥,最好都不要写进 Git 仓库。
包括:
API_KEY
KEY
REDIS_PASSWORD
HMAC 密钥
TLS 私钥
远程 API Token
本地测试写在 compose.yaml 里没什么问题。但只要进入真实环境,就应该换成更合适的方式。
比如:
环境变量
.env 文件
Docker Secret
Kubernetes Secret
部署平台的 Secret 管理
专门的密钥管理系统
如果使用 .env,至少确认它不会被提交:
.env
*.env
示例文件可以保留,比如:
.env.example
里面只放占位值:
API_KEY=change-me
KEY=Bearer change-me
REDIS_PASSWORD=change-me
不要为了方便,把真实 Token 写进示例配置。
这类东西一旦进了仓库,后面就不只是删掉那么简单。你还需要轮换密钥。
服务间通信可以逐步增强
第一阶段,用 API Key 就够简单。
Stargate 调用 Warden 时带上:
WARDEN_API_KEY=your-real-api-key
Warden 侧配置:
API_KEY=your-real-api-key
这能解决最基础的服务间鉴权问题。
但如果 Warden 已经变成认证链路里的关键组件,就可以继续往上加。
比如 HMAC。调用方不仅带一个静态 API Key,还对请求内容和时间戳做签名,这样可以降低请求被重放或伪造的风险。
再比如 mTLS。让 Stargate 和 Warden 之间通过客户端证书互相确认身份。
这类方案部署成本会高一些,不是第一天必须要做。但当 Warden 开始被多个服务依赖,或者运行在更复杂的网络环境里,就值得考虑。
我更建议的顺序是:
第一阶段:内部网络 + API Key
第二阶段:反向代理访问控制 + IP 白名单 + TLS
第三阶段:HMAC 或 mTLS
第四阶段:审计日志、指标和告警
不要第一天就把链路弄得太复杂,但也不要长期停留在“只要能访问到内网就能调用”的状态。
后端服务不要绕过入口
这一点更多和 Stargate 有关,但接入 Warden 后也一样重要。
如果后端服务可以绕过 Traefik、Nginx 或 Stargate 直接访问,那么前面的认证链路就不完整。
比如你让 Stargate 查询 Warden,Warden 返回用户身份,Stargate 再把用户信息写进请求头转给后端:
X-Forwarded-User-Id: admin-001
X-Forwarded-Email: admin@example.com
X-Forwarded-Role: admin
这时后端服务会把这些请求头当成可信身份信息。但前提是:这些请求只能来自反向代理或 Stargate。
如果用户可以直接访问后端服务,他就有机会自己伪造这些请求头。
所以后端服务应该只暴露在内部网络里,外部用户只能走统一入口。
这和 Stargate 的原则一样:
门装上了,后门也要关掉。
否则 Warden 再认真维护名单,入口链路也可能被绕开。
日志里不要留下太多敏感信息
Warden 排查问题时,日志很有用。但日志也很容易变成另一个数据泄露点。
尤其是用户查询接口,可能会带邮箱、手机号、user_id。如果日志里完整记录了请求参数、返回结果、Header 和 Token,就要小心了。
至少不要把这些东西原样打进日志:
API_KEY
Authorization
远程数据源 Token
完整手机号
完整邮箱
验证码相关字段
内部备注字段
调试环境可以稍微放开一点,生产环境里最好做脱敏。比如手机号只保留后四位,邮箱只保留部分字符,Token 只显示前后几位,或者完全不显示。
日志是为了排查问题。
不是为了复制一份用户数据。
一个最小但比较稳的部署形态
如果只是内部使用,我觉得比较稳的起步方式大概是这样:
Warden 只在内部网络里
↓
查询接口必须带 API Key
↓
健康检查只给监控或内网访问
↓
返回字段用 RESPONSE_FIELDS 收紧
↓
密钥通过环境变量或 Secret 注入
↓
后端服务不能绕过 Stargate
对应到 Compose,大概像这样:
services:
warden:
image: ghcr.io/soulteary/warden:latest
container_name: warden
restart: unless-stopped
environment:
- PORT=8081
- MODE=ONLY_LOCAL
- DATA_FILE=/app/data.json
- REDIS_ENABLED=false
- API_KEY=${WARDEN_API_KEY}
- RESPONSE_FIELDS=user_id,mail,phone,status,scope,role,name
- HEALTH_CHECK_IP_WHITELIST=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
volumes:
- ./data.json:/app/data.json:ro
- /etc/localtime:/etc/localtime:ro
networks:
- auth-internal
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:8081/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
networks:
auth-internal:
internal: true
这里没有 ports。这意味着 Warden 不会直接暴露到宿主机端口。
同一个内部网络里的 Stargate 可以访问它,外部用户不应该直接访问它。
如果你需要本地调试,可以临时加上:
ports:
- "8081:8081"
调试完成后再去掉。不要把调试配置直接带进生产环境。 上线前,可以简单过一遍:
API_KEY 是否已经设置?
示例密钥是否已经替换?
Warden 是否只在内部网络里?
是否去掉了不必要的 ports?
健康检查是否限制访问范围?
RESPONSE_FIELDS 是否收紧?
远程数据源是否使用 HTTPS?
远程 API Token 是否通过 Secret 注入?
Redis 密码是否没有写进仓库?
后端服务是否只能从统一入口访问?
日志里是否避免记录 Token 和完整用户信息?
这不是为了把 Warden 做成一个很重的系统,而是因为它在认证链路里位置比较敏感。它管的不是普通配置,它管的是:
- 谁还能进。
- 谁不该进。
- 后续验证应该发给谁。
这几件事值得稍微认真一点。
最后
很多内部服务的认证问题,并不是一开始就很复杂。
最开始只是一个密码、一个测试面板、一个临时后台、一个内部文档站。几个人知道地址,几个人知道口令,这样先跑起来,没什么问题。
真正的问题通常是后来出现的。
服务慢慢变多,使用的人慢慢变多,入口也慢慢变多。这时候,原来那把共享钥匙就开始不够用了。
你会开始关心更多问题:
- 这个人是谁;
- 他现在还在不在项目里;
- 他是不是还应该继续登录;
- 验证码应该发到哪里;
- 下游服务能不能拿到他的
user_id; - 有没有必要区分
role和scope。
如果这些问题全部塞进认证入口里,Stargate 会越来越重。
如果每个服务都自己维护一份名单,用户数据又会散得到处都是。
如果直接上完整 IAM,对很多 HomeLab、小团队内部工具、测试环境和临时服务来说,又可能太早、太重。
所以这里还是那个中间地带。
我们不是不知道完整身份系统更强,只是很多时候,眼前的问题没到那一步。我们需要的可能只是:先把裸奔的服务挡住,再把共享口令换成名单,然后让验证码、会话、用户状态、下游身份信息这些东西,一步一步接上来。
Stargate 解决的是第一步:先把门装上。
Warden 解决的是第二步:让这道门不要只认一把共享钥匙,而是开始认人。
Herald 后面要解决的是第三步:把验证码和消息送到该送的人手里。
这三个工具放在一起,并不是想重新做一套完整 IAM,它们也不应该假装自己是完整 IAM。完整 IAM 要解决的是组织、账号、协议、权限、审计、生命周期和一整套治理问题。
而 Stargate、Warden、Herald 更像是几个可以独立工作的基础设施小工具。
每个工具只先解决一个具体问题:
- Stargate 负责守入口;
- Warden 负责管名单;
- Herald 负责送验证码。
这几件事拆开之后,Stargate 不需要变成用户数据库,Warden 也不需要理解登录页和验证码流程,Herald 也不用关心谁有资格登录。
它们可以单独使用,也可以组合起来。
你可以先只用 Stargate,用共享口令把几个内部服务保护起来。等共享口令不够用了,再接 Warden,用邮箱、手机号或 user_id 判断谁还能继续登录。等登录流程需要更明确的验证,再接 Herald,把验证码发出去。
再往后,如果服务更多、要求更高,再考虑 Redis、多实例、审计日志、监控指标、HMAC、mTLS,或者直接接入更完整的身份体系。
这条路径的重点不是“功能最多”,而是每一步都能解释清楚为什么要加。
第一天不要太重,但也不要一直停在共享口令里。
Warden 想补的,就是共享口令之后、完整 IAM 之前的那一段。
它让“谁可以进”这件事,从聊天记录、表格、脚本、环境变量和各个服务自己的配置里收回来,变成一份可以被机器稳定查询的准入名册。
这份名单一开始可以只是一个 data.json,但它至少让系统开始知道:谁是这个人、他现在是什么状态、后续验证应该联系哪里,以及需要的话,下游服务能拿到哪些基础身份信息。
这不是什么宏大的系统。
但对很多内部工具、测试服务、小团队平台和 HomeLab 场景来说,已经能把一件长期别扭的事情理顺很多。
先装门,再认人,最后送信。
如果这个项目对你有帮助,欢迎到 GitHub 顺手点个 Star:https://github.com/soulteary/warden
星门启闭,守望验身,鸦使送信。
下一篇,我们继续聊“鸦使”:Herald。
–EOF