本文目录导读:

告别密码:PHP 实现 Passwordless 认证的完整实战指南(WebAuthn & Magic Link)
目录导读(Table of Contents)
- 为什么需要 Passwordless? — 从安全痛点与用户体验说起
- Passwordless 的核心技术原理 — 公钥加密与令牌的博弈
- 方案A:Magic Link(魔法链接) — 基于邮箱的免密登录实战
- 方案B:WebAuthn(设备密钥) — 基于生物识别/安全密钥的进阶实现
- PHP 代码实战:手写一个极简 Passwordless 类
- 安全坑位与规避指南 — 防止令牌泄露与重放攻击
- 常见问题解答(FAQ) — 开发者的 5 个高频疑问
- 总结与选型建议
为什么需要 Passwordless?
传统密码认证(如 password_hash + password_verify)面临着三大顽疾:弱口令猜测、撞库攻击 以及 用户流失(记不住密码),根据 2024 年 Verizon 数据泄露报告,81% 的入侵事件与弱口令有关,而 Passwordless(无密码)认证,正试图通过消除共享密钥来彻底瓦解这些攻击向量——即便数据库被拖库,攻击者拿到的也是一堆无法逆向的“公钥”或“一次性令牌”。
核心价值:将“你知道什么”(密码)转变为“你拥有什么”(邮件/手机)或“你是什么”(生物特征),这不仅降低了钓鱼风险,还简化了登录流程。
核心技术原理:公钥与令牌的博弈
目前主流的 PHP Passwordless 实现,底层无非两种逻辑:
- 魔法链接 (Magic Link):服务端生成一个带有加密签名(HMAC)的唯一 URL,发送到用户邮箱,用户点击 URL 后,服务端验证签名与有效期,即视为登录成功,本质上是“短时有效的 Bearer Token”。
- WebAuthn (FIDO2):基于非对称加密,用户注册时,设备(如手机指纹)生成一对密钥(私钥留在设备安全区,公钥上传服务器),登录时,服务器下发挑战码(Nonce),设备用私钥签名,服务器用公钥验签,这是目前安全等级最高的方案,且无需输入任何内容。
方案A:Magic Link(魔法链接)实战
这是 PHP 项目中最容易落地的方案,以 Laravel 为例,只需在 User 模型中添加 sendMagicLink 方法,利用 URL::temporarySignedRoute 生成带 15 分钟时效的签名 URL:
public function sendMagicLink()
{
$url = URL::temporarySignedRoute('auth.magic.verify', now()->addMinutes(15), ['user' => $this->id]);
Mail::to($this->email)->send(new LoginLinkMail($url));
}
验证逻辑:在 auth.magic.verify 路由中调用 $request->hasValidSignature(),若通过,直接用 Auth::login($user)。
方案B:WebAuthn 进阶实现
由于 PHP 后端主要处理验证,前端需配合浏览器 Web Authentication API,PHP 侧建议使用 web-auth/webauthn-lib 库。
核心流程:
- 注册阶段:服务端生成
PublicKeyCredentialCreationOptions,前端调用navigator.credentials.create()生成凭证,将id和rawId及公钥(PEM格式)提交回 PHP 存储。 - 登录阶段:服务端生成
PublicKeyCredentialRequestOptions(包含挑战码),前端调用navigator.credentials.get()进行指纹/人脸验证,将签名信息回传,PHP 端调用PublicKeyCredentialLoader验签。
小编提醒:除非你的目标用户全是极客或内部系统,否则 WebAuthn 需要处理兼容性(老旧浏览器退回 OTP),开发难度较高。
极简 PHP 手写类(无框架依赖)
如果你不想引入庞大包,以下这个纯原生 PHP 类展示了 Magic Link 的核心签名生成与校验:
class Passwordless {
private string $key;
public function __construct(string $secretKey) { $this->key = $secretKey; }
// 生成带过期时间的令牌(防篡改)
public function generateToken(int $userId): string {
$expires = time() + 600; // 10分钟
$payload = "{$userId}.{$expires}";
$signature = hash_hmac('sha256', $payload, $this->key);
return base64_encode("{$payload}.{$signature}");
}
// 校验令牌
public function verifyToken(string $token): ?int {
$data = base64_decode($token);
[$userId, $expires, $signature] = explode('.', $data);
$expected = hash_hmac('sha256', "{$userId}.{$expires}", $this->key);
if (!hash_equals($expected, $signature)) return null; // 签名不符
if (time() > (int)$expires) return null; // 已过期
return (int)$userId;
}
}
注意:生产环境务必结合 HTTPS 使用,防止中间人截取令牌。
安全坑位与规避指南
- 令牌泄漏:日志中禁止打印
magic_linkURL,邮件链接必须使用https。 - 重放攻击:令牌绑定
IP(不推荐,会误伤移动网络用户),或采用“一次性”机制:验证成功后立刻从数据库删除该令牌记录。 - 用户枚举:不论邮箱是否存在,统一返回“邮件已发送”,避免攻击者探测注册邮箱。
- CSRF:Magic Link 的
GET请求会改变状态,可将链接改为POST表单提交,或利用 SameSite Cookie 防护。
常见问题解答(FAQ)
问:PHP 密码less 认证安全吗? 答:比传统密码更安全,Magic Link 依赖邮箱安全,WebAuthn 依赖设备安全,前者需防钓鱼邮箱,后者无密码可被窃取。
问:用户在无密码系统里如何退出登录? 答:依然通过 Session 或 JWT 管理会话,退出登录即销毁服务端会话标记,无需撤销密钥。
问:WebAuthn 必须用 HTTPS 吗? 答:是的,浏览器强制要求安全上下文(HTTPS 或 localhost)才能调用 WebAuthn API。
问:多个设备如何登录? 答:每个设备注册时可上传各自的公钥,形成一对多的关系,也可以引导用户使用 Passkey(云同步密钥)。
问:如果用户的手机丢了怎么办? 答:提供备用的恢复码或预留邮箱回退方案,否则会永久失去账户访问权。
问:能不能做到“无感”登录? 答:可以,登录后下发持久 Cookie(Remember Me),并设置较长的过期时间,实现“打开网站即已登录”。
总结与选型建议
选型速查:
- 追求快速开发:选 Magic Link,只需处理邮件发送与签名校验,半小时可完成。
- 高安全/金融项目:选 WebAuthn,推荐使用
web-auth/webauthn-lib。 - 折中方案:Magic Link + TOTP(谷歌验证器)双因素。
Passwordless 并非银弹,但它减少了攻击面,在 PHP 生态中,Symfony 和 Laravel 均已内置或支持包来简化此过程,建议从 Magic Link 入手,感受无密码的魅力,再逐步演进到生物识别。
最后忠告:无论采用哪种方案,务必在代码层面加入速率限制(如 throttle 中间件),防止黑客暴力请求生成邮件,造成服务器资源耗尽或邮箱轰炸。