做了几年后端,最让我头疼的不是业务逻辑,而是“账号体系”这四个字。密码怎么存、会话怎么管、CSRF怎么防、邮件验证怎么发、被恶意注册怎么办……一套东西全堆在自己身上,又累又容易出漏洞。后来我接触了 Ory 这套开源身份生态,才意识到很多自建账号系统的痛点是可以被标准组件替代掉的。这篇快速开始(一)先聚焦最基础也最关键的环节:Ory Kratos 的邮箱注册和登录流程,同时也把Ory Hydra在整套体系里的位置讲清楚,方便后续继续拼图。
如果你和我一样,是独立开发者、小团队,或者正在做微服务拆分、不想在每个服务里重复写登录逻辑,这篇内容应该对你有用。我会从环境搭建讲到注册登录的完整流程,再讲几个我实际踩过的坑。先提醒一句:搜“Hydra”的时候要留意,网上很多同名工具是下载器、爆破工具之类,跟 Ory 生态里的 Hydra 完全不是一回事,别搞混了。
1. 为什么我把账号体系交给 Ory 而不是自己写
1.1 自己写认证系统的隐藏成本
很多人觉得写个登录不过就是“比对一下密码,然后存个 session”。真做起来完全不是这么回事。密码哈希要选对算法和参数,还要处理时序攻击;session 要解决存储、过期、续期、并发登录限制;邮件验证要防止被刷接口、防止验证链接被猜出来;再加上找回密码、修改邮箱、多设备管理、风控审计……这些和业务逻辑一点关系没有,却要消耗大量排期。
我见过不少项目,业务还没跑起来,先花了两三周在账号系统上,结果写出来的东西还不敢上线。用生活里的例子来说,自己从零写认证,就像自己开餐厅还非要自己砌灶台、打家具、做收银系统。不是不能做,而是这些活儿应该有成熟的供应商。Ory Kratos 就是把“身份管理”这件事抽出来做成了标准组件,我只需要在它周边写配置和业务回调。
1.2 Kratos 与 Hydra 的分工
Ory 生态里有两个经常一起出现的组件:Kratos负责身份认证,解决的是“你是谁、能不能登进来”的问题;Hydra负责授权,解决的是“你的应用能不能代替你去做某些操作”的问题。说得再直白一点:Kratos 是前台登记系统,核对你身份证;Hydra 是门禁授权中心,根据你已经登记过的身份,给第三方应用发放临时通行证。
很多教程把两者混在一起讲,容易把人绕晕。我的建议是先分清层次:认证在前,授权在后。用户要先用邮箱和密码在 Kratos 里完成注册登录,拿到自己的登录态;之后如果我们需要让第三方应用访问用户的资源,才轮到 Hydra 出场发 token。所以这篇快速开始(一)先把 Kratos 的邮箱注册登录流程跑通,给 Hydra 的后续接入打好地基。标题里把 Kratos、Hydra 放一起,其实代表了 Ory 这套体系的完整形态,但步子要一步一步走。
2. 快速开始前的环境搭建与项目结构
2.1 用 Docker Compose 把整套服务拉起来
Ory 官方仓库里带了完整的快速开始配置,最省事的方式是直接用它。我本地推荐的做法是:
git clone https://github.com/ory/kratos.git cd kratos然后进入 Docker 快速启动目录,直接起服务:
cd docker/quickstart docker compose up第一次启动会拉几个镜像,包含 Kratos 本体、PostgreSQL 数据库、以及一个叫MailSlurper的开发用邮件服务器。这套组合跑起来之后,你本地就有了:
- Kratos Public API:
http://localhost:4433 - Kratos Admin API:
http://localhost:4434 - 自带的示例前端 UI:
http://localhost:4455 - MailSlurper 邮件测试后台:
http://localhost:8085
端口号比较多,我第一次跑的时候也觉得乱。后来我整理成了一张表,每次都对着看,少走很多弯路。
| 服务 | 地址 | 作用 |
|---|---|---|
| Kratos Public API | http://localhost:4433 | 对上,给浏览器和前端应用调用 |
| Kratos Admin API | http://localhost:4434 | 对内,管理员操作、导入数据、维护 |
| 示例 UI(Kratos SelfService UI) | http://localhost:4455 | 现成的注册登录页面,调试用 |
| MailSlurper | http://localhost:8085 | 接收开发环境的邮件,查看验证链接 |
如果本地 5432 端口已经被其他 PostgreSQL 占用了,docker compose up会直接报错。这时候不要慌,去docker-compose.yml里改一下映射端口,把5432:5432改成类似5433:5432就行。修改后记得重建容器:
docker compose down -v docker compose up-v会清掉旧的数据卷,确保你用的是干净状态。这个操作我后面还会提,因为开发过程中反复踩坑,最终我发现最有效的排障方式就是“全部删掉重来”。
2.2 Kratos 的 Public / Admin API 为什么拆开
Kratos 把接口分成 Public 和 Admin 两组,这个设计我一开始不理解,觉得多此一举。后来在预发环境里做权限控制时才体会到好处:Public API 要暴露给浏览器端,里面跑的可能是注册、登录、找回密码这些用户自服务流程;Admin API 则完全不应该被外部摸到,里面是身份导入、批量操作、配置管理这些敏感能力。
类比一下,Public API 是餐厅前台,顾客可以直接接触;Admin API 是后厨和财务室,只有内部工作人员能进。生产环境里,Admin API 必须放在内网,或者至少加严格的网络策略,绝不能跟着 Public API 一起裸奔到公网。Kratos 在架构层面就把这两个口子分开,逼着你从一开始就养成好的网络隔离习惯。
示例 UI 这边,默认配置会通过环境变量指定 Kratos 的 SDK 地址,比如ORY_SDK_URL=http://kratos:4433。因为容器内部通信走的是 Docker 网络,容器里访问 Kratos 会用内部服务名,而你本地浏览器访问则直接用 localhost。这个细节很容易忽略,我在刚开始折腾自建 UI 时经常搞混,后来才明白容器内外的网络命名空间不一样。
3. 邮箱注册登录的核心流程拆解
3.1 身份模型:schema 决定了你能存什么
Kratos 把“用户”抽象成Identity(身份)。每个身份有几个关键部分:
- ID:系统内部唯一标识,类似主键,通常用 UUID。
- Traits:面向业务的数据,比如邮箱、昵称、头像。用户本人可以查看和修改。
- Credentials:认证凭据,比如邮箱密码、OIDC 关联信息。这部分由 Kratos 管理,不暴露给前端。
- Metadata:管理员侧的元数据,用户不可见。
这里我觉得最关键的一个设计是:邮箱你要放在 traits 里,而不是 metadata 里。因为注册登录流程里,Kratos 需要根据用户提交的邮箱去索引 identity;而能够被外部提交、被用户编辑的数据,必然会走 traits 的 schema 校验。密码则不行,密码属于 credentials,必须由 Kratos 内部的密码凭据模块管理,前端永远只提交明文给 Kratos 的接口,永远不能自己往数据库里塞哈希。
实际项目中我用的 schema 长这样(简化版):
{ "$id": "https://example.com/identity.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Person", "type": "object", "properties": { "traits": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "title": "E-Mail" }, "name": { "type": "string", "title": "Name" } }, "required": ["email"], "additionalProperties": false } } }traits.email是必填项,而且 schema 里限制了格式。这样前端就算不认真做前端校验,Kratos 也会在服务端拦住格式错误的邮箱,防止脏数据入库。additionalProperties: false意味着你只能提交 schema 里定义过的字段,并不是前端传什么 Kratos 都收。这个限制帮我挡掉过不少“想偷偷塞点东西到 traits 里”的不合理需求,设计上非常省心。
3.2 注册流程:从填写表单到拿到登录态
Kratos 的浏览器注册流程,和传统后端写模板渲染的注册流程不太一样,它用的是flow(流程)机制。你可以把 flow 理解成一次有状态的会话过程:前端先向后端申请一个“注册任务”,Kratos 返回一个 flow ID 和 CSRF token;前端把用户填好的数据连同 flow ID 一起提交;Kratos 校验通过后创建 identity,并写入登录 session。
第一步,申请注册流程:
curl -v \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ http://localhost:4433/self-service/registration/browser返回里会包含一个id字段,比如054d6f26-2b25-4c9c-b074-1a59272f4c53。接着前端带着这个 flow id 提交邮箱和密码:
curl -v \ -X POST \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ --cookie "csrf_0=xxx" \ -d '{ "traits": { "email": "user@example.com", "name": "张三" }, "password": "AveryStrongPassword!123", "method": "password" }' \ http://localhost:4433/self-service/registration?flow=054d6f26-2b25-4c9c-b074-1a59272f4c53没有报错的话,Kratos 会创建 identity,并且在响应头里返回Set-Cookie,把登录会话写入浏览器。这里有两个点我当初困惑了很久:
一是为什么注册成功后就直接登录了?很多传统系统注册完会跳到“登录页”,让你重新输一遍密码。Kratos 默认逻辑是注册成功即视为登录成功,直接建立会话。你可以在配置里关掉,但我建议保留默认,因为这是主流产品的体验。
二是为什么注册接口要有 CSRF token?因为这个接口是靠 Cookie 维持状态的,而 Cookie 天然有被跨站请求劫持的风险。Kratos 在GET注册页时返回的 Cookie 里带有csrf_0,表单提交时必须把它原样带回。如果你用 UI 组件库自己渲染表单,记得从 flow 的响应里取csrf_token嵌入到表单隐藏域,否则提交会一直 400。这个坑我帮同事排查了很久,最后发现就是少了个隐藏字段。
3.3 登录流程与密码校验
登录流程跟注册流程很像,也是先申请 flow,再提交凭据。
curl -v \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ http://localhost:4433/self-service/login/browser拿到新的 flow id 之后,提交邮箱和密码:
curl -v \ -X POST \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ --cookie "csrf_0=xxx" \ -d '{ "identifier": "user@example.com", "password": "AveryStrongPassword!123", "method": "password" }' \ http://localhost:4433/self-service/login?flow=刚才的flow_id注意登录和注册提交的字段名不一样,注册用traits.email,登录用identifier。这个细节在对接前端时非常容易搞错,我见过不少前端同事把注册的 JSON 结构直接套到登录接口上,结果 Kratos 一直提示找不到字段。
登录成功后的返回同样会设置会话 Cookie。浏览器后面的请求带上这个 Cookie,Kratos 就知道你是谁。Kratos 在浏览器场景下默认使用Cookie Session,而不是 JWT。这跟很多人习惯的“API 返回 token”思维不一样。选择 Cookie 是因为浏览器端天然支持 Cookie 的 HttpOnly、SameSite、Secure 等属性,XSS 捞不走,CSRF 有专门防护,比前端把 token 存在 localStorage 里要安全得多。如果你做的是纯 API 服务,Kratos 也支持 API 场景下的 token 方式,但那是另一个话题,本篇先不展开。
3.4 忘记密码和邮箱验证那点事
一篇“邮箱注册和登录流程”的教程,如果不提邮件验证和找回密码,后面必然会卡壳。Kratos 里有个组件叫Courier,它负责把邮件消息投递出去。开发环境配置的是 MailSlurper,所以注册之后你看到的“验证邮箱”“重置密码”邮件,都会先落到 MailSlurper 里。
邮箱验证的逻辑是:identity 的 traits.email 旁边会维护一个verifiable_addresses列表,里面记录了邮箱是否已验证。如果项目配置了要求验证邮箱才能登录,那么未验证用户在登录时会被引导到 verification 流程。你可以通过配置放行未验证用户,但一般建议验证,尤其是面向公众的产品,能有效减少垃圾注册。
恢复密码流程也类似,用户发起 recovery flow,Kratos 生成一个一次性链接发到邮箱,用户打开链接后设置新密码。整个过程里,Kratos 承担了令牌生成、过期时间管理、邮件模板渲染等脏活。我在代码层面几乎不需要写任何邮件逻辑,只需要在配置里写好 SMTP 连接信息,把模板改成自己的品牌样式。这套流程内部细节颇多,但对外表现就是“点开邮件链接,设置新密码”,非常干净。
4. 实操过程实录:从启动到第一次登录成功
4.1 完整跑一遍官方示例
如果只推一个最省心的路径,我会建议你先别改任何配置,直接用官方镜像把整套东西跑起来,亲眼看到“注册 -> 收邮件 -> 验证 -> 登录”这个过程,再回去读文档会容易得多。
第一步,克隆仓库并启动:
git clone https://github.com/ory/kratos.git cd kratos/docker/quickstart docker compose up第二次启动时我习惯加-d,让容器在后台跑:
docker compose up -d然后打开http://localhost:4455。你会看到示例 UI 的首页,点注册,填一个真实可用的邮箱(开发环境不需要真能收到,MailSlurper 会替你收),再设置密码。提交之后,Kratos 日志里会出现类似这样的记录:
INFO[0015] A registration was successfully completed这时候到http://localhost:8085打开 MailSlurper 后台,找到刚才发给你的邮件。如果邮件里包含验证链接,点开它。然后回到http://localhost:4455,用刚才的邮箱密码登录,就能进入受保护页面了。
这个过程你可能 5 分钟就能跑通。但我强烈建议你不要看完“成功了”就关掉页面,而是继续往下做两件事:第一,把容器日志打开,观察注册和登录发生时 Kratos 打了哪些日志,初步建立“正常日志长什么样”的概念;第二,把 MailSlurper 里的邮件原文打开,研究一下验证链接的格式,后面排查问题会用到。
4.2 用 API 手工复现同一套流程
UI 跑通之后,我建议再用 curl 走一遍接口,这一步对后面集成前端、写自动化测试非常重要。Curl 不需要浏览器,能直接暴露请求和响应的真实面貌。具体命令在上一章已经列过,这里我只补充几个容易踩细节:
- 请求
/self-service/registration/browser时,注意保存返回的 Cookie。这个 Cookie 里包含 CSRF token,是一个名为csrf_0的 Cookie。 - 提交注册信息时,表单里要把
csrf_token带上,而且这个值必须和 Cookie 里的值一致。 - 建议全程用一个 cookie jar 文件保存 Cookie,避免多个请求之间 Cookie 不一致:
curl -v \ -c /tmp/kratos-cookie.txt \ -b /tmp/kratos-cookie.txt \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ http://localhost:4433/self-service/registration/browser后续带 Cookie 的请求都用-b /tmp/kratos-cookie.txt,Kratos 就不会一直认为你没有 CSRF 上下文。我见过很多第一次接触 Kratos 的人在这一步被绕晕,觉得“为什么我照着文档写还是 400”,多半就是没有严格维持 Cookie 会话。
手工调通 API 之后,你对 Kratos 的信心会完全不一样。你会知道每个 flow 返回什么、错误响应长什么样、session cookie 叫ory_kratos_session之类,这些信息在写前端对接或者排查用户反馈时会非常有用。
4.3 我踩过的几个坑
我在把 Kratos 接到真实项目之前,前前后后踩了不少坑,这里挑几个最有代表性的整理成表格。如果你跑流程时遇到问题,可以对照着看。
| 现象 | 原因 | 解决办法 |
|---|---|---|
docker compose up报端口占用 | 本机已有 PostgreSQL 或 Web 服务占用 5432/4433 | 修改docker-compose.yml端口映射,再用down -v重建 |
注册提交后返回 422,提示email格式异常 | schema 里format: email校验失败,或提交字段嵌套层级不对 | 检查提交 JSON 是否包含traits.email,并确认真实邮箱格式 |
| 注册成功但收不到验证邮件 | Courier 循环周期未到,或 SMTP 配置错误,或邮件卡在队列 | 看 Kratos 日志里的courier相关输出;开发环境检查 MailSlurper 是否在 8085 正常服务 |
| 登录一直提示账号密码错误,但数据库里明明有记录 | 邮箱验证未通过,身份处于未激活状态;或者提交字段用了email而不是identifier | 先完成邮箱验证;登录接口字段名确认为identifier |
| 浏览器里能打开页面,但 API 请求一直 400/CSRF 错误 | Cookie 没有维持好,或前端没有提交csrf_token隐藏域 | 用 curl cookie jar 验证接口本身;前端从 flow 响应取csrf_token并嵌入表单 |
| 改完配置重启后老配置还在 | Docker 数据卷缓存旧配置 | docker compose down -v清理数据卷,再docker compose up |
这中间我最想单独拎出来说的是“先看日志,再去猜配置”。Kratos 的日志写得算清晰,如果你加一段DEBUG级别日志,几乎能看到每一步决策理由。比起反复看配置文档,日志给出的信息量更大。开发阶段我会把日志级别调低,多看输出,尤其是courier、selfservice这两个模块的日志;生产环境再调高,避免打印敏感信息。
另外,如果不想让验证流程卡住正常的登录体验,你可以临时在配置里把selfservice.verification.enabled关掉测试,先跑通纯密码登录。这算是个小技巧,能帮你快速定位问题出在“验证”还是“登录”本身。
5. Hydra 在整套体系中的位置:下一步怎么接
5.1 Hydra 到底解决什么问题
前面提过 Hydra 是授权服务,这里我再展开一点。当你的业务开始有多个客户端应用,或者有第三方合作方要读取用户数据时,就会遇到“用户同意授权”的场景。Hydra 负责颁发 access token、refresh token、ID token,实现标准的 OAuth2/OIDC 协议。Kratos 跟 Hydra 不冲突,一个管身份,一个管令牌。
用更直观的说法:Kratos 相当于公司的员工花名册,确认“这个人是我们公司的”;Hydra 相当于门禁系统,根据花名册和访客规则,给外部系统发“临时通行证”。很多微服务自己实现 token 逻辑,换来换去很容易出现签名算法不统一、过期策略不一致的问题。Hydra 把这些协议层的东西标准化,我们可以直接用标准 OIDC 客户端库去对接它。
举个常见的场景:用户已经通过 Kratos 登录了业务网站,现在业务系统想让自己的移动 App 访问用户资料。App 不能拿用户的密码,而是需要引导用户到 Hydra 完成授权,Hydra 签发的 access token 由后端验证。Kratos 在整个链条里,只负责确认“当前登录的人到底是谁”,身份确认完,授权动作交给 Hydra。分层清晰,各干各的,这也是我推荐 Ory 这套组合的原因。
5.2 下一期快速开始的预期路径
既然标题是“Ory kratos、Hydra快速开始(一)”,后面大概率会有系列文章继续展开。按照我自己的学习路径,下一步建议按这个顺序来:
- 先了解 OAuth2 的授权码模式,知道
client_id、client_secret、redirect_uri、authorization_code这几个核心参数分别指什么。 - 用 Docker 把 Ory Hydra 也跑起来,设置好客户端 ID 和回调地址。
- 用 Hydra 的登录接口配合 Kratos 的登录会话,实现“点第三方登录 -> 跳转到 Hydra -> Hydra 校验会话 -> 回跳应用”的完整链路。
- 后端拿到 token 后,用 Hydra 提供的信息校验用户身份,再结合 Kratos 的 identity 信息做业务数据映射。
我自己的体会是,Kratos 和 Hydra 单独看都不算太难,难的是理解它们之间“谁先谁后”的协作关系。这个道理想清楚了,后续配置和调试快很多。也因为这个系列是分步走的,所以我建议你现在先把 Kratos 注册登录的肌肉记忆练扎实,下一期讲 Hydra 的时候就不会前后打架。
最后再补充一个小经验:开发阶段把 MailSlurper 留着挺有用的,它不只能收邮件,还能让你在调试时看到邮件模板渲染后的效果。等上生产再换成真正的 SMTP 服务,切换成本很低。我个人在实际操作中最深刻的体会是,Kratos 这套东西值得你花一两个小时把官方示例完整跑一遍,而不是直接跳到“写代码对接”。流程理解透了,后面接 Hydra、做二次开发都会顺很多。希望这篇快速开始能帮你少踩几个坑。