本文目录导读:

- 方案一:Token 替换法(最简单,推荐)
- 方案二:Token 列表 + 旧 Token 过期
- 方案三:兼容老设备的 Token 淘汰
- 方案四:JWT + Redis 黑名单(生产环境推荐)
- 方案五:使用 Session 固定 + 数据库记录
- 综合推荐方案
- 前端配合实现
在 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 替换法),原因是:
- 实现简单 - 只需要一个数据库表
- 性能好 - 一次查询即可验证
- 安全性高 - 无过期 Token 堆积
- 易于维护 - 逻辑清晰
数据库表结构优化版:
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);
}
);
这样就实现了完整的单设备登录功能,可以根据项目规模和需求选择合适的方案。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播