PHP 怎么实现单设备登录

wen PHP项目 6

本文目录导读:

PHP 怎么实现单设备登录

  1. 方案一:Token 替换法(最简单,推荐)
  2. 方案二:Token 列表 + 旧 Token 过期
  3. 方案三:兼容老设备的 Token 淘汰
  4. 方案四:JWT + Redis 黑名单(生产环境推荐)
  5. 方案五:使用 Session 固定 + 数据库记录
  6. 综合推荐方案
  7. 前端配合实现

在 PHP 中实现单设备登录,核心思路是让同一账号的新登录使旧登录失效,主要有以下几种实现方案,按推荐程度排序:

Token 替换法(最简单,推荐)

用户登录时生成新 Token,旧 Token 自动失效。

<?php
// 1. 登录时生成唯一 Token
function login($userId) {
    $token = md5(uniqid($userId, true) . time());
    // 存入数据库(只有一个字段,天然覆盖)
    $pdo = new PDO("mysql:host=localhost;dbname=test", "root", "");
    $stmt = $pdo->prepare("INSERT INTO user_tokens (user_id, token, expire_time) 
                           VALUES (?, ?, ?) 
                           ON DUPLICATE KEY UPDATE token = ?, expire_time = ?");
    $expire = time() + 3600 * 24; // 24小时
    $stmt->execute([$userId, $token, $expire, $token, $expire]);
    return $token;
}
// 2. 验证 Token 是否有效
function verifyToken($userId, $token) {
    $pdo = new PDO("mysql:host=localhost;dbname=test", "root", "");
    $stmt = $pdo->prepare("SELECT token FROM user_tokens WHERE user_id = ?");
    $stmt->execute([$userId]);
    $dbToken = $stmt->fetchColumn();
    return $token === $dbToken; // 只有最新 Token 有效
}
// 使用示例
session_start();
$token = login(123);
$_SESSION['token'] = $token; // 或存到 Cookie
?>

数据库表设计:

CREATE TABLE user_tokens (
    user_id INT PRIMARY KEY,
    token VARCHAR(64) NOT NULL,
    expire_time INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Token 列表 + 旧 Token 过期

允许一个用户有多个 Token,但只保留最新的 N 个。

<?php
function generateToken() {
    return bin2hex(random_bytes(16));
}
function validateToken($userId, $token) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    $key = "user_token:{$userId}";
    $tokens = $redis->lRange($key, 0, -1);
    // Token 存在则有效
    return in_array($token, $tokens);
}
// 登录时
$token = generateToken();
$redis->lPush("user_token:{$userId}", $token);
$redis->lTrim("user_token:{$userId}", 0, 9); // 允许保留10个 Token
// 验证时只需检查 Token 是否在列表中
?>

兼容老设备的 Token 淘汰

当检测到新设备登录时,通知旧设备。

<?php
class SingleLoginManager {
    private $pdo;
    public function __construct($pdo) {
        $this->pdo = $pdo;
    }
    // 登录,返回新 Token
    public function login($userId, $deviceId) {
        // 1. 将旧 Token 设为无效
        $stmt = $this->pdo->prepare("UPDATE user_tokens 
                                     SET status = 'expired' 
                                     WHERE user_id = ? AND status = 'active'");
        $stmt->execute([$userId]);
        // 2. 生成新 Token
        $token = bin2hex(random_bytes(32));
        $expire = time() + 3600 * 24;
        // 3. 保存新 Token
        $stmt = $this->pdo->prepare("INSERT INTO user_tokens 
                                     (user_id, token, device_id, expire_time, status) 
                                     VALUES (?, ?, ?, ?, 'active')");
        $stmt->execute([$userId, $token, $deviceId, $expire]);
        return $token;
    }
    // 验证 Token
    public function validate($userId, $token) {
        $stmt = $this->pdo->prepare("SELECT status FROM user_tokens 
                                     WHERE user_id = ? AND token = ? 
                                     LIMIT 1");
        $stmt->execute([$userId, $token]);
        $status = $stmt->fetchColumn();
        return $status === 'active';
    }
    // 统计在线设备数
    public function getOnlineDevices($userId) {
        $stmt = $this->pdo->prepare("SELECT device_id, login_time 
                                     FROM user_tokens 
                                     WHERE user_id = ? AND status = 'active'");
        $stmt->execute([$userId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}
?>

JWT + Redis 黑名单(生产环境推荐)

<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JWTLoginManager {
    private $secretKey;
    private $redis;
    public function __construct($secretKey, $redis) {
        $this->secretKey = $secretKey;
        $this->redis = $redis;
    }
    // 登录生成 JWT
    public function login($userId) {
        $payload = [
            'user_id' => $userId,
            'iat' => time(),
            'exp' => time() + 3600 * 24, // 24小时有效
            'jti' => bin2hex(random_bytes(16)) // 唯一标识
        ];
        return JWT::encode($payload, $this->secretKey, 'HS256');
    }
    // 验证 Token,同时检查是否是最新的
    public function validate($token) {
        try {
            $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
            // 检查当前 Token 是否为最新
            $latestTokenId = $this->redis->get("latest_token:{$decoded->user_id}");
            if ($decoded->jti !== $latestTokenId) {
                return false; // 已被新登录替代
            }
            // 检查是否在黑名单(可以用于登出)
            if ($this->redis->sIsMember('token_blacklist', $decoded->jti)) {
                return false;
            }
            return $decoded;
        } catch (Exception $e) {
            return false;
        }
    }
    // 登出(可以取消单设备限制)
    public function logout($userId) {
        $this->redis->del("latest_token:{$userId}");
    }
}
?>

使用 Session 固定 + 数据库记录

<?php
// MySQL 实现,适合中小型项目
$pdo = new PDO("mysql:host=localhost;dbname=test", "root", "");
// 登录时
session_start();
session_regenerate_id(true); // 防 Session 固定
$sessionId = session_id();
// 更新该用户的 Session
$stmt = $pdo->prepare("UPDATE users SET session_id = ? WHERE id = ?");
$stmt->execute([$sessionId, $userId]);
// 每次请求验证
function checkSession($userId) {
    global $pdo;
    $stmt = $pdo->prepare("SELECT session_id FROM users WHERE id = ?");
    $stmt->execute([$userId]);
    $dbSession = $stmt->fetchColumn();
    return $dbSession === session_id();
}
?>

综合推荐方案

对于大多数 Web 应用,推荐方案一(Token 替换法),原因是:

  1. 实现简单 - 只需要一个数据库表
  2. 性能好 - 一次查询即可验证
  3. 安全性高 - 无过期 Token 堆积
  4. 易于维护 - 逻辑清晰

数据库表结构优化版:

CREATE TABLE user_sessions (
    user_id INT PRIMARY KEY,
    token VARCHAR(64) NOT NULL,
    device_info VARCHAR(255),
    ip_address VARCHAR(45),
    login_time INT,
    expire_time INT,
    last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_token (token)
) ENGINE=InnoDB;

前端配合实现

// Axios 拦截器 - 每次请求携带 Token
axios.interceptors.request.use(config => {
    const token = localStorage.getItem('auth_token');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});
// 后端返回 401 时,提示重新登录
axios.interceptors.response.use(
    response => response,
    error => {
        if (error.response?.status === 401) {
            alert('您的账号在其他设备登录');
            localStorage.removeItem('auth_token');
            window.location.href = '/login';
        }
        return Promise.reject(error);
    }
);

这样就实现了完整的单设备登录功能,可以根据项目规模和需求选择合适的方案。

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