config = Config::get('aws.s3'); $this->bucket = $this->config['bucket']; $this->region = $this->config['region']; $this->client = new Client([ 'timeout' => 30, 'verify' => false, // 根据环境调整 ]); } /** * 生成 AWS S4 签名 */ protected function generateSignature($method, $uri, $headers, $payload = '') { $accessKey = $this->config['credentials']['key']; $secretKey = $this->config['credentials']['secret']; $service = 's3'; $algorithm = 'AWS4-HMAC-SHA256'; // 当前时间 $timestamp = time(); $longDate = gmdate('Ymd\THis\Z', $timestamp); $shortDate = substr($longDate, 0, 8); // 计算签名所需组件 $scope = $shortDate . '/' . $this->region . '/' . $service . '/aws4_request'; // 规范化请求 $canonicalHeaders = ''; $signedHeaders = ''; ksort($headers); foreach ($headers as $key => $value) { $canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n"; $signedHeaders .= strtolower($key) . ';'; } $signedHeaders = rtrim($signedHeaders, ';'); $canonicalRequest = implode("\n", [ $method, $uri, '', // 查询字符串 $canonicalHeaders, $signedHeaders, hash('sha256', $payload) ]); // 计算签名 $stringToSign = implode("\n", [ $algorithm, $longDate, $scope, hash('sha256', $canonicalRequest) ]); // 派生签名密钥 $kDate = hash_hmac('sha256', $shortDate, 'AWS4' . $secretKey, true); $kRegion = hash_hmac('sha256', $this->region, $kDate, true); $kService = hash_hmac('sha256', $service, $kRegion, true); $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); $signature = hash_hmac('sha256', $stringToSign, $kSigning); return $algorithm . ' ' . implode(', ', [ 'Credential=' . $accessKey . '/' . $scope, 'SignedHeaders=' . $signedHeaders, 'Signature=' . $signature ]); } /** * 上传文件到 S3 */ public function upload($file, $key = null, $options = [],$website) { $link ='https://cdn.alipearlhair.com'; if($website=='alipearlhair'){ $this->bucket ='alipearl-images'; $link ='https://cdn.alipearlhair.com'; } if($website=='wigginshair'){ $this->bucket ='wiggins-images'; $link ='https://cdn.wigginshair.com'; } try { // 处理文件 if ($file instanceof File) { $filePath = $file->getRealPath(); $fileContent = file_get_contents($filePath); $mimeType = $file->getMime(); } elseif (is_string($file) && file_exists($file)) { $fileContent = file_get_contents($file); $mimeType = mime_content_type($file); } else { $fileContent = $file; $mimeType = $options['mime_type'] ?? 'application/octet-stream'; } // 生成存储 key if (empty($key)) { $key = $this->generateKey($file); } // 准备请求 $method = 'PUT'; $uri = '/' . $key; $url = 'https://' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com' . $uri; $urls = $link . $uri; $headers = [ 'Host' => $this->bucket . '.s3.' . $this->region . '.amazonaws.com', 'Content-Type' => $mimeType, 'Content-Length' => strlen($fileContent), 'x-amz-content-sha256' => hash('sha256', $fileContent), 'x-amz-date' => gmdate('Ymd\THis\Z'), ]; // 添加额外头部 if (!empty($options['headers'])) { $headers = array_merge($headers, $options['headers']); } // 生成签名 $headers['Authorization'] = $this->generateSignature($method, $uri, $headers, $fileContent); // 发送请求 $response = $this->client->request($method, $url, [ 'headers' => $headers, 'body' => $fileContent, ]); if ($response->getStatusCode() == 200) { return [ 'success' => true, 'key' => $key, 'url' => $urls, 'size' => strlen($fileContent), 'type' => $mimeType ]; } else { throw new Exception('上传失败,状态码: ' . $response->getStatusCode()); } } catch (\Exception $e) { return [ 'success' => false, 'msg' => '上传失败: ' . $e->getMessage(), 'error' => $e->getMessage() ]; } } /** * 生成存储路径 key */ protected function generateKey($file) { $config = $this->config['upload']; $savePath = $config['save_path']; // 替换路径变量 $replace = [ '{year}' => date('Y'), '{mon}' => date('m'), '{day}' => date('d'), '{hour}' => date('H'), '{min}' => date('i'), '{sec}' => date('s'), '{random}' => mt_rand(1000, 9999), '{uniqid}' => uniqid(), '{timestamp}'=> time(), ]; $savePath = str_replace(array_keys($replace), array_values($replace), $savePath); // 生成文件名 // 获取原始文件名和扩展名 $originalName = $file->getInfo('name'); // 原始文件名,如 "image.jpg" $originalExtension = pathinfo($originalName, PATHINFO_EXTENSION); // 如果原始扩展名有效,使用它 if (!empty($originalExtension) && strlen($originalExtension) <= 6) { $extension = strtolower($originalExtension); } else { // 否则通过内容检测 $extension = $this->getRealExtensionByContent($file->getRealPath()); } $sha1 = sha1($file); $filename = date('YmdHis') . '_' . substr($sha1, 0, 16) . '.' . $extension; return $savePath . $filename; } /** * 删除 S3 文件 */ public function delete($key) { try { $method = 'DELETE'; $uri = '/' . $key; $url = 'https://' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com' . $uri; $headers = [ 'Host' => $this->bucket . '.s3.' . $this->region . '.amazonaws.com', 'x-amz-date' => gmdate('Ymd\THis\Z'), 'x-amz-content-sha256' => hash('sha256', ''), ]; $headers['Authorization'] = $this->generateSignature($method, $uri, $headers); $response = $this->client->request($method, $url, [ 'headers' => $headers, ]); return $response->getStatusCode() == 204; } catch (\Exception $e) { return false; } } /** * 检查文件是否存在 */ public function exists($key) { try { $method = 'HEAD'; $uri = '/' . $key; $url = 'https://' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com' . $uri; $headers = [ 'Host' => $this->bucket . '.s3.' . $this->region . '.amazonaws.com', 'x-amz-date' => gmdate('Ymd\THis\Z'), 'x-amz-content-sha256' => hash('sha256', ''), ]; $headers['Authorization'] = $this->generateSignature($method, $uri, $headers); $response = $this->client->request($method, $url, [ 'headers' => $headers, ]); return $response->getStatusCode() == 200; } catch (\Exception $e) { return false; } } /** * 获取文件 URL */ public function getUrl($key, $expires = 3600) { if ($expires === null) { // 公开读取的直接 URL $cdnUrl = rtrim($this->config['upload']['cdn_url'], '/'); return $cdnUrl . '/' . $key; } else { // 生成预签名 URL return $this->generatePresignedUrl($key, $expires); } } /** * 生成预签名 URL */ protected function generatePresignedUrl($key, $expires = 3600) { $accessKey = $this->config['credentials']['key']; $secretKey = $this->config['credentials']['secret']; $timestamp = time(); $expiresAt = $timestamp + $expires; $longDate = gmdate('Ymd\THis\Z', $timestamp); $shortDate = substr($longDate, 0, 8); $service = 's3'; $algorithm = 'AWS4-HMAC-SHA256'; $scope = $shortDate . '/' . $this->region . '/' . $service . '/aws4_request'; // 生成签名 $canonicalQueryString = implode('&', [ 'X-Amz-Algorithm=AWS4-HMAC-SHA256', 'X-Amz-Credential=' . urlencode($accessKey . '/' . $scope), 'X-Amz-Date=' . $longDate, 'X-Amz-Expires=' . $expires, 'X-Amz-SignedHeaders=host' ]); $canonicalRequest = implode("\n", [ 'GET', '/' . $key, $canonicalQueryString, 'host:' . $this->bucket . '.s3.' . $this->region . '.amazonaws.com', '', 'host', 'UNSIGNED-PAYLOAD' ]); $stringToSign = implode("\n", [ $algorithm, $longDate, $scope, hash('sha256', $canonicalRequest) ]); // 派生签名密钥 $kDate = hash_hmac('sha256', $shortDate, 'AWS4' . $secretKey, true); $kRegion = hash_hmac('sha256', $this->region, $kDate, true); $kService = hash_hmac('sha256', $service, $kRegion, true); $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true); $signature = hash_hmac('sha256', $stringToSign, $kSigning); $url = sprintf( 'https://%s.s3.%s.amazonaws.com/%s?%s&X-Amz-Signature=%s', $this->bucket, $this->region, $key, $canonicalQueryString, $signature ); return $url; } }