PHP 怎么实现免密登录

wen PHP项目 5

PHP免密登录实战:从Token签发到安全校验的完整指南(附代码)


📚 目录导读

  1. 免密登录的核心逻辑:为什么需要它?与Session登录的区别
  2. 技术方案选型:临时令牌(Token) vs 签名URL vs OAuth隐式授权
  3. 手把手实现步骤:数据库设计 → 生成令牌 → 校验并建立会话
  4. 安全加固策略:防止令牌泄露、重放攻击和时序攻击
  5. 常见问题FAQ:令牌有效期多少?刷新机制怎么设计?
  6. 完整代码示例:可直接复制运行的PHP类

免密登录的核心逻辑

免密登录(Passwordless Login)本质是用“持有凭证”代替“知道秘密”,用户通过邮件/短信收到的临时链接,或App中保存的长期令牌,向服务器证明身份,与常规Session不同,它不依赖用户输入密码,而是通过一次性或短期有效的Token完成身份映射。

PHP 怎么实现免密登录

核心流程
用户请求 → 生成唯一Token并存储 → 发送给用户 → 用户携带Token访问 → 校验Token并创建会话 → 删除或作废Token


技术方案选型:三种主流实现

方案 适用场景 安全性 实现复杂度
临时Token 邮件/短信验证码 高(短期+一次性)
签名URL 微信/钉钉OAuth跳转 中(依赖签名算法)
长期刷新Token 移动App自动登录 高(需配合HTTPS)

本文重点讲解方案一,因为它最贴合“免密登录”字面需求,且容易扩展。


手把手实现步骤(附带数据库设计)

第一步:创建数据表

CREATE TABLE login_tokens (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  token CHAR(64) NOT NULL UNIQUE,
  expires_at DATETIME NOT NULL,
  used TINYINT(1) DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

第二步:生成并发送令牌

// 生成高强度随机Token(防猜测)
$token = bin2hex(random_bytes(32)); // 64位十六进制字符
// 存入数据库(有效期设为10分钟)
$expires = date('Y-m-d H:i:s', strtotime('+10 minutes'));
$pdo->prepare("INSERT INTO login_tokens (user_id, token, expires_at) VALUES (?,?,?)")
    ->execute([$userId, $token, $expires]);
// 发送到用户邮箱(短信同理)
$magicLink = "https://yourdomain.com/login.php?token={$token}";
// mail($userEmail, "登录链接", $magicLink);

第三步:校验并完成登录

// 接收Token
$token = $_GET['token'] ?? '';
// 查询并检查有效性
$stmt = $pdo->prepare("SELECT user_id FROM login_tokens 
                       WHERE token = ? AND used = 0 AND expires_at > NOW()");
$stmt->execute([$token]);
$user = $stmt->fetch();
if ($user) {
    // 作废令牌(防止二次使用)
    $pdo->prepare("UPDATE login_tokens SET used = 1 WHERE token = ?")->execute([$token]);
    // 创建常规会话
    session_start();
    $_SESSION['user_id'] = $user['user_id'];
    header('Location: /dashboard.php');
    exit;
} else {
    die("链接无效或已过期,请重新申请");
}

安全加固策略(重要!)

  • HTTPS强制:令牌必须通过加密传输,否则会明文暴露。
  • 令牌熵值:使用random_bytes()而非uniqid()md5(time()),后者可被预测。
  • 过期时间:建议5-10分钟,过长增加泄露风险。
  • 一次性使用:校验后立即标记used=1,防重放。
  • 速率限制:同一用户每小时申请次数≤5,防恶意刷邮件。
  • 时序攻击防护:令牌不存在或过期时,返回相同的模糊错误提示(如"链接无效"),不要区分具体原因。

常见问题FAQ(Q&A)

Q1:免密登录的Token有效期应该设置多久?
答:建议5-15分钟,如果太短(<1分钟)用户可能来不及打开邮件;太长(>30分钟)则增加了数据库泄漏后暴力破解的窗口期,对于“记住我”场景,可额外签发一个7天有效的刷新Token,但需结合设备指纹绑定。

Q2:如果用户反复请求,如何防止数据库膨胀?
答:每次新请求生成Token前,先删除该用户所有过期(expires_at < NOW())的旧记录,也可以加一个定时任务清理,或直接使用Redis等缓存并设置TTL,天然自动过期。

Q3:免密登录和OAuth2.0授权码模式的区别是什么?
答:OAuth用于第三方授权,用户跳转到服务商(如微信)确认后回调;而免密登录是本站自签,令牌由站点自己生成和校验,前者规范更复杂,后者实现更轻量。

Q4:能否用JWT代替数据库存储Token?
答:可以,JWT无状态,无需存储,但无法主动作废(除非引入黑名单),如果追求服务器无状态,可签发短时效JWT(如5分钟),并在JWT中自定义jti字段,配合Redis做黑名单。

Q5:在API接口中如何实现免密登录?
答:前端携带Authorization: Bearer <token>头,后端直接解析,推荐使用JWT+签名,因为API通常无Session概念,但注意:JWT令牌要保存在客户端内存或Secure Cookie中,不要放localStorage(防XSS窃取)。


完整代码示例:免密登录核心类

class PasswordlessLogin {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    // 生成令牌并返回链接
    public function generateToken(int $userId, int $ttlMinutes = 10): string {
        $token = bin2hex(random_bytes(32));
        $expires = date('Y-m-d H:i:s', strtotime("+{$ttlMinutes} minutes"));
        $stmt = $this->pdo->prepare(
            "INSERT INTO login_tokens (user_id, token, expires_at) VALUES (?,?,?)"
        );
        $stmt->execute([$userId, $token, $expires]);
        return "https://yourdomain.com/login.php?token={$token}";
    }
    // 校验令牌,成功则返回用户ID,失败返回null
    public function validateToken(string $token): ?int {
        // 先清理过期记录
        $this->pdo->exec("DELETE FROM login_tokens WHERE expires_at < NOW()");
        $stmt = $this->pdo->prepare(
            "SELECT user_id FROM login_tokens 
             WHERE token = ? AND used = 0 AND expires_at > NOW()"
        );
        $stmt->execute([$token]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($result) {
            // 立即作废
            $this->pdo->prepare("UPDATE login_tokens SET used = 1 WHERE token = ?")
                      ->execute([$token]);
            return (int)$result['user_id'];
        }
        return null;
    }
}
// 使用示例
$login = new PasswordlessLogin($pdo);
$magicLink = $login->generateToken(42); // 发送给用户


免密登录不是银弹,它牺牲了部分便利性换取了更高的安全性,核心在于短期、一次性、不可预测的令牌设计,建议结合业务场景,为高风险操作(如支付)强制要求二次验证,务必在生产环境开启HTTPS,并做好日志监控异常登录行为。

抱歉,评论功能暂时关闭!