魔法登录邮件链接(2026):用 Rovela 做免密登录
在应用里自行生成、存储并核销一次性登录链接,再用 POST /emails 发出——与密码重置同一套模式;安全邮件建议关闭点击追踪,避免 token 被追踪跳转改写。
没有专用邮件 API,也能做免密登录
免密登录(常叫「魔法链接」)是很多 AI 应用、SaaS 原型和邮箱注册产品的第一块积木:用户输入邮箱,你发一封一次性链接,对方点开后应用创建会话。不必存密码,也不必为这类账号再维护一套「忘记密码」。
Rovela 没有单独的魔法登录产品或接口。它提供的,是和其他事务邮件一样的投递通道:POST /emails。最接近的官方示例是 密码重置链接:在应用里生成不可猜测的 token,存哈希与短过期时间,明文 token 只出现在邮件里,核销一次即失效。魔法登录只是同一套模式换了落地页——核销后创建会话,而不是改密码。
如果你已经按 验证码示例 在发 OTP,继续用在「输入六位数字」的流程。本文讲的是链接形态:一点即登录。
你负责什么,Rovela 负责什么
应用侧必须自己做:
- 用密码学安全的随机数生成 token,并做哈希
- 持久化(哈希、用户 id、过期时间、一次性核销)
- 挂在你自己域名上的绝对 URL(如
https://app.example.com/login/magic?token=…) - 校验 token 并创建会话的核销路由
- 「再发一封」的限流、无账号时返回相同文案,以及只在服务端调用发信 API
Rovela 负责:
- 接受带 Bearer
re_…的POST https://api.rovela.dev/emails(生产发信用sending_access即可) - 入队、按已验证域名做 DKIM、重试、退信/投诉处理与消息日志
- 可选模板、Webhook、抑制列表——与其他事务邮件同一套基础设施
不要把 API 密钥放进浏览器或移动客户端。额度与套餐见 定价页;本文不杜撰任何价格或配额数字。
生成并存储 token(32 字节、哈希、过期)
与 密码重置文档 同一套熵要求:
- 规范化邮箱(
trim+ 小写)。 - 查找用户。若存在,生成 32 字节随机数,编码为
base64url。 - 只存 token 的 SHA-256 十六进制哈希,绝不存明文。32 字节无法从哈希暴力还原,因此普通 SHA-256 足够(六位 OTP 不同:短数字需要带服务端 pepper 的 HMAC)。
- 过期时间约 30 分钟。
- 拼出
https://app.acme.com/login/magic?token=+ 明文 token。token 只出现在邮件 URL 里。
无论该地址有没有账号,都返回同一段话——文案、状态码一致,最好连耗时也接近(后台队列发信)。否则「发登录链接」表单会变成账号枚举工具。
用 POST /emails 发出链接(html+text、Idempotency-Key、子域发件人)
用已验证的子域发件地址隔离认证信誉(例如 security@mail.acme.com),同时发 html 和 text,并在纯文本里写上完整 URL。部分企业邮箱过滤器会剥掉 HTML 里的链接,纯文本是兜底。
为每次逻辑发送设置一次 Idempotency-Key(≤256 字符,24 小时窗口,按组织生效)。可用 token 哈希的稳定前缀派生,避免超时重试发出第二封。用户点「重新发送」是新的逻辑发送——换新 token 和新 key。
以下草图改编自密码重置示例(仅占位):
import crypto from "node:crypto"
export async function requestMagicLink(rawEmail) {
const email = rawEmail.trim().toLowerCase()
const user = await db.users.findByEmail(email)
if (user) {
const token = crypto.randomBytes(32).toString("base64url")
const tokenHash = crypto.createHash("sha256").update(token).digest("hex")
await db.magicLogins.upsert({
userId: user.id,
tokenHash,
expiresAt: new Date(Date.now() + 30 * 60 * 1000),
})
const link = "https://app.acme.com/login/magic?token=" + token
await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ROVELA_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "magic-" + tokenHash.slice(0, 32),
},
body: JSON.stringify({
from: "Acme <security@mail.acme.com>",
to: email,
subject: "你的 Acme 登录链接",
html:
"<p>使用此链接登录 Acme。</p>" +
'<p><a href="' + link + '">登录</a></p>' +
"<p>链接仅可使用一次,30 分钟后失效。" +
"若非本人操作,可忽略本邮件。</p>",
text:
"使用此链接登录 Acme。\n\n" +
"登录:" + link + "\n\n" +
"链接仅可使用一次,30 分钟后失效。",
}),
})
}
return { message: "若该地址已有账号,登录链接正在路上。" }
}
Rovela 返回 200 表示邮件已接受并入队,不等于已进收件箱。若要感知投递失败,请接 Webhook(如 email.bounced、email.complained、抑制相关事件)。字段说明见 发信 API。
一次性核销并创建会话
在数据库里一步花费 token,避免并行点击各成功一次:
export async function redeemMagicLink(token) {
const tokenHash = crypto.createHash("sha256").update(token).digest("hex")
const row = await db.query(
`DELETE FROM magic_logins
WHERE token_hash = $1 AND expires_at > NOW()
RETURNING user_id`,
[tokenHash]
)
if (row.rowCount === 0) return null
const userId = row.rows[0].user_id
const session = await db.sessions.create(userId)
return session
}
成功后写入你应用已有的会话 cookie(或 JWT)。失败则展示通用的「链接已过期或已使用」,并提供重新申请入口。不要区分「未知 / 过期 / 已用」若这会帮助攻击者。
与密码重置不同,魔法登录通常不必吊销该用户其他所有会话——用户是在登录,而不是轮换凭证。若威胁模型把每次魔法登录都视为高风险,可在核销成功后再发一封通知邮件;那是产品策略,不是 Rovela 的硬性要求。
安全邮件请关闭点击追踪
打开/点击追踪是按域名配置的。开启点击追踪时,Rovela 会把链接改写到追踪域名。登录 URL 经跳转后仍可用,但 token 会多经一跳。
对安全类邮件——魔法链接、密码重置、邀请 token——更稳妥的做法是使用单独发信域名(或子域)并关闭点击追踪,让 token 直达你的应用。这一点写在 密码重置示例 里。追踪关闭后,该域名不会产生打开/点击类 engagement Webhook,属预期行为。
失败模式(抑制 400、未验证域名、429)
- 收件人被抑制。 单次
POST /emails时,验证码文档记载以recipient is suppressed开头的400。按不可投递处理:请对方换邮箱,不要说「请查收件箱」。 from域名未验证。 会返回400并指出域名。上线认证邮件前先完成 域名验证。- 限流 /
429。 退避后用同一Idempotency-Key重试同一次逻辑发送。用户主动重发 = 新 token + 新 key。 - 密钥缺失或权限不对。 生产发信用
sending_access;密钥只放服务端(见 Authentication)。
OTP 与魔法链接:何时用哪种
| OTP(验证码) | 魔法链接 | |
|---|---|---|
| 文档示例 | 验证码 | 发信形态同 密码重置;无独立魔法链接页 |
| 用户动作 | 输入数字 | 点击一次 |
| 密钥存储 | email:code 的 HMAC + pepper;短码可被暴力尝试 | 32 字节 token 的 SHA-256 |
| 典型过期 | 文档示例约 10 分钟 | 重置示例约 30 分钟 |
| 适合 | 共享屏幕、二次确认、偏短信体验的邮箱流程 | 桌面/手机浏览器上一键完成登录 |
| 勿混用 | 验证码核销与链接核销分表、分路由 |
两条路径都调用同一个 POST /emails。Rovela 负责入队与投递;应用决定正文是六位数字还是 URL。
自己实现 token 机制,一次 HTTP 调用发出邮件,在安全发信域关闭追踪——你就有了免密登录,而无需把邮件 API 当成身份提供商。