path($kind, $value), 'x'); if ($handle === false) { throw new RuntimeException('IAM transaction creation failed'); } try { chmod($this->path($kind, $value), 0600); $data = json_encode(['browser' => hash('sha256', $browser), 'expires' => time() + $ttl, 'payload' => $payload], JSON_THROW_ON_ERROR); if (fwrite($handle, $data) !== strlen($data)) { throw new RuntimeException('IAM transaction write failed'); } } finally { fclose($handle); } // Only expired private IAM files are collected, and never a file held by another request. if (random_int(1, 20) === 1) { foreach (array_slice(glob($this->directory . '/*.json') ?: [], 0, 200) as $file) { if (filemtime($file) >= time() - 1200) { continue; } $lock = @fopen($file, 'r+'); if ($lock !== false) { if (flock($lock, LOCK_EX | LOCK_NB)) { @unlink($file); flock($lock, LOCK_UN); } fclose($lock); } } } } public function consume(string $kind, string $value, string $browser): array { $handle = @fopen($this->path($kind, $value), 'r+'); if ($handle === false) { throw new RuntimeException('登录状态已失效,请重新发起快捷登录'); } try { if (!flock($handle, LOCK_EX)) { throw new RuntimeException('IAM transaction lock failed'); } $data = json_decode(stream_get_contents($handle), true); if (!is_array($data) || ($data['expires'] ?? 0) <= time() || !isset($data['browser']) || !hash_equals($data['browser'], hash('sha256', $browser)) || !is_array($data['payload'] ?? null)) { throw new RuntimeException('登录状态已失效,请重新发起快捷登录'); } // Truncate under the lock before returning, so previously opened file descriptors cannot replay. if (!ftruncate($handle, 0) || !fflush($handle)) { throw new RuntimeException('IAM transaction consumption failed'); } return $data['payload']; } finally { flock($handle, LOCK_UN); fclose($handle); } } public function allowStart(string $ip): bool { $file = $this->directory . '/limit-' . hash('sha256', $ip) . '.json'; $handle = fopen($file, 'c+'); if ($handle === false) { return false; } try { chmod($file, 0600); if (!flock($handle, LOCK_EX)) { return false; } $data = json_decode(stream_get_contents($handle), true); if (!is_array($data) || ($data['expires'] ?? 0) <= time()) { $data = ['count' => 0, 'expires' => time() + 600]; } if ($data['count'] >= 20) { return false; } ++$data['count']; rewind($handle); ftruncate($handle, 0); $encoded = json_encode($data, JSON_THROW_ON_ERROR); return fwrite($handle, $encoded) === strlen($encoded) && fflush($handle); } finally { flock($handle, LOCK_UN); fclose($handle); } } private function path(string $kind, string $value): string { if (!in_array($kind, ['state', 'ticket'], true) || !preg_match('/^[a-f0-9]{64}$/D', $value)) { throw new RuntimeException('登录状态无效,请重新发起快捷登录'); } return $this->directory . '/' . $kind . '-' . hash('sha256', $value) . '.json'; } }