在 PHP 中实现基于 Token 的认证机制,并在用户关闭页面后使 Token 失效,可以通过以下步骤实现:

1. 生成 Token 并存储

在登录时生成 Token 并存储到数据库:

// 登录成功后的处理
function generateAndStoreToken($userId) {
    // 生成唯一Token
    $token = bin2hex(random_bytes(32)); // 生成64字符的随机字符串
    
    // 计算过期时间(例如1小时后过期)
    $expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
    
    // 存储到数据库
    $db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    $stmt = $db->prepare("INSERT INTO user_tokens (user_id, token, expires_at) VALUES (?, ?, ?)");
    $stmt->execute([$userId, $token, $expires]);
    
    // 返回给客户端
    return $token;
}

2. 客户端存储 Token

将 Token 返回给客户端,通常存储在:

  • localStorage (持久化存储,直到明确删除)

  • sessionStorage (标签页关闭后自动删除)

  • Cookie (可设置过期时间)

// 返回Token给客户端(JSON响应示例)
header('Content-Type: application/json');
echo json_encode([
    'token' => $token,
    'user_id' => $userId
]);

 

3. 校验 Token 的中间件

创建用于校验 Token 的中间件:

function validateToken($token) {
    if (empty($token)) {
        return false;
    }
    
    $db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    
    // 查询有效且未过期的Token
    $stmt = $db->prepare("SELECT user_id FROM user_tokens WHERE token = ? AND expires_at > NOW()");
    $stmt->execute([$token]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if ($result) {
        return $result['user_id']; // 返回用户ID表示验证通过
    }
    
    return false;
}

4. 使 Token 在关闭页面后失效的实现方案

方案1: 使用 sessionStorage + 短期过期时间

// 客户端存储Token到sessionStorage
sessionStorage.setItem('auth_token', token);

// 这样在关闭页面/标签后会自动清除

方案2: 服务器端短期过期 + 客户端心跳检测

// 客户端定期发送心跳请求
setInterval(function() {
    fetch('/api/heartbeat', {
        headers: {
            'Authorization': 'Bearer ' + sessionStorage.getItem('auth_token')
        }
    }).then(response => {
        if (!response.ok) {
            // Token可能已失效,跳转到登录页
            window.location.href = '/login';
        }
    });
}, 300000); // 每5分钟一次

方案3: 结合数据库清理

在数据库层面定期清理过期Token:

// 定时任务或每次Token验证时清理过期Token
function cleanupExpiredTokens() {
    $db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    $db->exec("DELETE FROM user_tokens WHERE expires_at <= NOW()");
}

5. 完整使用示例

在需要验证的页面

// 获取Token(假设通过Authorization头传递)
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = str_replace('Bearer ', '', $token);

// 验证Token
$userId = validateToken($token);
if (!$userId) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// Token有效,继续处理请求

Token 过期后的更新机制

在 Token 认证系统中,当 Token 超过有效期后,可以通过以下几种方式实现 Token 更新:

1. 使用 Refresh Token 机制

这是最常用的 Token 更新方案,采用双 Token 机制:

// 登录时生成两种Token
function generateTokens($userId) {
    // 访问Token (短期有效)
    $accessToken = bin2hex(random_bytes(32));
    $accessExpires = date('Y-m-d H:i:s', strtotime('+1 hour'));
    
    // 刷新Token (长期有效)
    $refreshToken = bin2hex(random_bytes(32));
    $refreshExpires = date('Y-m-d H:i:s', strtotime('+30 days'));
    
    // 存储到数据库
    $db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    $stmt = $db->prepare("INSERT INTO user_tokens (user_id, access_token, refresh_token, access_expires, refresh_expires) VALUES (?, ?, ?, ?, ?)");
    $stmt->execute([$userId, $accessToken, $refreshToken, $accessExpires, $refreshExpires]);
    
    return [
        'access_token' => $accessToken,
        'refresh_token' => $refreshToken,
        'expires_in' => 3600 // 1小时
    ];
}

// Token刷新接口
function refreshToken($refreshToken) {
    $db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    
    // 验证refresh token是否有效
    $stmt = $db->prepare("SELECT user_id FROM user_tokens WHERE refresh_token = ? AND refresh_expires > NOW()");
    $stmt->execute([$refreshToken]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$result) {
        return false;
    }
    
    $userId = $result['user_id'];
    
    // 生成新的access token
    $newAccessToken = bin2hex(random_bytes(32));
    $newAccessExpires = date('Y-m-d H:i:s', strtotime('+1 hour'));
    
    // 更新数据库
    $stmt = $db->prepare("UPDATE user_tokens SET access_token = ?, access_expires = ? WHERE refresh_token = ?");
    $stmt->execute([$newAccessToken, $newAccessExpires, $refreshToken]);
    
    return [
        'access_token' => $newAccessToken,
        'expires_in' => 3600
    ];
}

2. 自动检测并刷新 Token(前端实现)

前端可以在检测到 Token 即将过期时自动刷新:

let authToken = localStorage.getItem('auth_token');
let refreshToken = localStorage.getItem('refresh_token');

function checkTokenExpiry() {
    // 在实际应用中,你可能需要解码JWT或从API获取过期时间
    // 这里假设我们有一个expiry时间存储在本地
    const expiryTime = localStorage.getItem('token_expiry');
    
    if (expiryTime && new Date(expiryTime) < new Date()) {
        // Token已过期,使用refresh token获取新token
        fetch('/api/auth/refresh', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ refresh_token: refreshToken })
        })
        .then(response => response.json())
        .then(data => {
            localStorage.setItem('auth_token', data.access_token);
            localStorage.setItem('token_expiry', new Date(Date.now() + data.expires_in * 1000));
        })
        .catch(error => {
            // 刷新失败,跳转到登录页
            window.location.href = '/login';
        });
    }
}

// 定期检查Token状态
setInterval(checkTokenExpiry, 60000); // 每分钟检查一次

3. 被动刷新方式(后端检测并返回新Token)

在每次API请求时,后端可以检测Token状态并返回新Token:

function validateTokenWithRefresh($token) {
    $db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    
    // 检查Token是否有效
    $stmt = $db->prepare("SELECT user_id, refresh_token, expires_at FROM user_tokens WHERE access_token = ?");
    $stmt->execute([$token]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$result) {
        return ['valid' => false];
    }
    
    $expiresAt = new DateTime($result['expires_at']);
    $now = new DateTime();
    
    if ($expiresAt > $now) {
        // Token仍然有效
        return ['valid' => true, 'user_id' => $result['user_id']];
    } else {
        // Token已过期,但可以刷新
        $refreshResult = refreshToken($result['refresh_token']);
        if ($refreshResult) {
            return [
                'valid' => false,
                'should_refresh' => true,
                'new_token' => $refreshResult['access_token']
            ];
        } else {
            return ['valid' => false];
        }
    }
}

// 在API中使用
$validation = validateTokenWithRefresh($token);
if (!$validation['valid']) {
    if (isset($validation['should_refresh'])) {
        // 返回新Token给客户端
        header('X-New-Access-Token: ' . $validation['new_token']);
        http_response_code(401); // 或考虑使用特殊状态码如 428 Precondition Required
        echo json_encode(['error' => 'Token expired', 'new_token' => $validation['new_token']]);
    } else {
        http_response_code(401);
        echo json_encode(['error' => 'Unauthorized']);
    }
    exit;
}

4. 滑动过期时间机制

每次使用Token时延长其有效期:

function validateAndExtendToken($token) {
    $db = new PDO('mysql:host=localhost;dbname=yourdb', 'username', 'password');
    
    // 检查Token是否有效
    $stmt = $db->prepare("SELECT user_id FROM user_tokens WHERE token = ? AND expires_at > NOW()");
    $stmt->execute([$token]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if ($result) {
        // 延长Token有效期
        $newExpires = date('Y-m-d H:i:s', strtotime('+1 hour'));
        $stmt = $db->prepare("UPDATE user_tokens SET expires_at = ? WHERE token = ?");
        $stmt->execute([$newExpires, $token]);
        
        return $result['user_id'];
    }
    
    return false;
}

最佳实践建议

  1. 合理设置Token有效期

    • Access Token: 短(15分钟-2小时)

    • Refresh Token: 长(7-30天)

  2. 安全存储Refresh Token

    • 使用HttpOnly、Secure Cookie存储

    • 或在移动端使用安全存储机制

  3. Refresh Token轮换

    • 每次使用Refresh Token获取新Access Token时,也生成新的Refresh Token

    • 使旧的Refresh Token失效,增强安全性

  4. 实现Token撤销机制

    • 提供接口让用户可以主动撤销所有Token

    • 在密码更改等敏感操作后自动撤销所有Token

  5. 限制Refresh Token使用频率

    • 防止暴力攻击

    • 记录Refresh Token使用日志

通过以上机制,可以确保用户在保持活动状态时无需重新登录,同时在适当的时候更新Token保证安全性。