基于七牛云 PHP SDK + Laravel 文件存储实现 Laravel 学院静态资源云存储及 CDN 加速

七牛云存储

背景

随着云服务的流行,将图片等静态文件存储到云服务提供商,然后通过CDN的方式获取成为了静态文件存储与加载的通用解决方案,这样做的好处是显而易见的,一方面可以免除在自己的系统上实现文件分布式存储(对大型系统而言),将专业的事情交给专业的人去做;另一方面,CDN获取静态文件可以极大节省服务器带宽,有效提高系统高峰期的吞吐能力,学院正是通过这种方式解决了网站访问速度的问题,用1M带宽撑起学院每日数万的访问量。

现在提供云存储的服务商也很多,阿里云、腾讯云这类整体云服务解决方案提供商自然提供了类似的服务,此外还有一些专业的云存储服务提供商,比如七牛云、又拍云等,由于老版学院使用的是七牛云,所以新版学院继续沿用,以减少新平台的学习成本。

云存储实现

基于 Laravel 实现文件云存储很简单,因为 Laravel 文件系统本身支持文件的本地存储和云存储,只不过默认支持的是 AWS 云存储,和前面一篇教程中搜索的实现思路一致,Laravel 的强大之处在于为其它驱动的存储提供了扩展支持,我们只要仿照自带的存储驱动实现为自定义的存储驱动编写一个扩展类即可,这里就是基于七牛云驱动的文件存储了。

初始化配置文件

七牛云官方提供了 PHP SDK,我们基于这个 SDK 来实现上层功能,首先安装这个 SDK:

composer require qiniu/php-sdk

为了使用这个 SDK,还需要注册一个七牛云账号,然后去 个人中心->密钥管理 页面获取 AccessKey/SecretKey 信息,编辑 .env 配置文件修改相关配置如下:

FILESYSTEM_CLOUD=qiniu
QINIU_ACCESS_KEY=你的七牛云AccessKey
QINIU_SECRET_KEY=你的七牛云SecretKey
QINIU_DEFAULT_REGION=你的默认区域,比如z0
QINIU_BUCKET=你的bucket(通过在对象存储中新增存储空间获取)
QINIU_URL=你的静态 CDN URL

然后修改配置文件 config/filesystems.php,新增七牛云存储相关配置:

'disks' => [

    ... // Laravel 自带配置信息

    'qiniu' => [
        'driver' => 'qiniu',
        'key' => env('QINIU_ACCESS_KEY'),
        'secret' => env('QINIU_SECRET_KEY'),
        'region' => env('QINIU_DEFAULT_REGION'),
        'bucket' => env('QINIU_BUCKET'),
        'url' => env('QINIU_URL')
    ],
],

编写七牛云存储适配器类

然后我们需要为七牛云存储编写一个 Laravel 文件存储驱动适配器:

<?php

namespace App\Services\FileSystem;

use Illuminate\Contracts\Filesystem\FileNotFoundException;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\Config;
use Qiniu\Auth;
use Qiniu\Storage\BucketManager;
use Qiniu\Storage\UploadManager;
use Symfony\Component\HttpFoundation\File\Exception\UploadException;

class QiniuAdapter extends AbstractAdapter
{

    protected $uploadManager;
    protected $bucketManager;
    private $accessKey;
    private $accessSecret;
    private $bucketName;
    private $token;

    public function __construct($prefix = '')
    {
        $this->uploadManager = new UploadManager();
        $this->accessKey = \config('filesystems.disks.qiniu.key');
        $this->accessSecret = \config('filesystems.disks.qiniu.secret');
        $this->bucketName = \config('filesystems.disks.qiniu.bucket');
        $auth = new Auth($this->accessKey, $this->accessSecret);
        $this->bucketManager = new BucketManager($auth);
        $this->token = $auth->uploadToken($this->bucketName);
        $this->setPathPrefix($prefix);
    }

    /**
     * Write a new file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function write($path, $contents, Config $config)
    {
        return $this->upload($path, $contents);
    }

    /**
     * Write a new file using a stream.
     *
     * @param string $path
     * @param resource $resource
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function writeStream($path, $resource, Config $config)
    {
        return $this->upload($path, $resource, true);
    }

    /**
     * Update a file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function update($path, $contents, Config $config)
    {
        return $this->upload($path, $contents);
    }

    /**
     * Update a file using a stream.
     *
     * @param string $path
     * @param resource $resource
     * @param Config $config Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function updateStream($path, $resource, Config $config)
    {
        return $this->upload($path, $resource, true);
    }

    /**
     * Rename a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function rename($path, $newpath)
    {
        $path = $this->applyPathPrefix($path);
        $newpath = $this->applyPathPrefix($newpath);
        $error = $this->bucketManager->rename($this->bucketName, $path, $newpath);
        return $error == null ? true : false;
    }

    /**
     * Copy a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function copy($path, $newpath)
    {
        $path = $this->applyPathPrefix($path);
        $newpath = $this->applyPathPrefix($newpath);
        $error = $this->bucketManager->copy($this->bucketName, $path, $this->bucketName, $newpath);
        return $error == null ? true : false;
    }

    /**
     * Delete a file.
     *
     * @param string $path
     *
     * @return bool
     */
    public function delete($path)
    {
        $this->applyPathPrefix($path);
        $error = $this->bucketManager->delete($this->bucketName, $path);
        return $error == null ? true : false;
    }

    /**
     * Delete a directory.
     *
     * @param string $dirname
     *
     * @return bool
     */
    public function deleteDir($dirname)
    {
        throw new \BadFunctionCallException('暂不支持该操作');
    }

    /**
     * Create a directory.
     *
     * @param string $dirname directory name
     * @param Config $config
     *
     * @return array|false
     */
    public function createDir($dirname, Config $config)
    {
        throw new \BadFunctionCallException('暂不支持该操作');
    }

    /**
     * Set the visibility for a file.
     *
     * @param string $path
     * @param string $visibility
     *
     * @return array|false file meta data
     */
    public function setVisibility($path, $visibility)
    {
        throw new \BadFunctionCallException('暂不支持该操作');
    }

    /**
     * Check whether a file exists.
     *
     * @param string $path
     *
     * @return array|bool|null
     */
    public function has($path)
    {
        $path = $this->applyPathPrefix($path);
        $stat = $this->bucketManager->stat($this->bucketName, $path);
        if ($stat[0] == null) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Read a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function read($path)
    {
        $path = $this->applyPathPrefix($path);
        list($fileInfo, $error) = $this->bucketManager->stat($this->bucketName, $path);
        if ($fileInfo) {
            return $fileInfo;
        } else {
            throw new FileNotFoundException('对应文件不存在');
        }
    }

    /**
     * Read a file as a stream.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function readStream($path)
    {
        throw new \BadFunctionCallException('暂不支持该操作');
    }

    /**
     * List contents of a directory.
     *
     * @param string $directory
     * @param bool $recursive
     *
     * @return array
     */
    public function listContents($directory = '', $recursive = false)
    {
        return $this->bucketManager->listFiles($this->bucketName);
    }

    /**
     * Get all the meta data of a file or directory.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMetadata($path)
    {
        return $this->read($path);
    }

    /**
     * Get the size of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getSize($path)
    {
        $fileInfo = $this->read($path);
        return $fileInfo['fsize'];
    }

    /**
     * Get the mimetype of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMimetype($path)
    {
        $fileInfo = $this->read($path);
        return $fileInfo['fileType'];
    }

    /**
     * Get the last modified time of a file as a timestamp.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getTimestamp($path)
    {
        $fileInfo = $this->read($path);
        return $fileInfo['putTime'];
    }

    /**
     * Get the visibility of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getVisibility($path)
    {
        throw new \BadFunctionCallException('暂不支持该操作');
    }

    protected function upload(string $path, $contents, $stream = false)
    {
        $path = $this->applyPathPrefix($path);
        try {
            if ($stream) {
                $response = $this->uploadManager->put($this->token, $path, $contents);
            } else {
                $response = $this->uploadManager->putFile($this->token, $path, $contents);
            }
        } catch (\Exception $ex) {
            throw $ex;
        }
        list($uploadResult, $error) = $response;
        if ($uploadResult) {
            return $uploadResult;
        } else {
            throw new UploadException('上传文件到七牛失败:' . $error->message());
        }
    }
}

编写好适配器后需要在 AppServiceProvider 提供的 boot 方法中注册这个适配器:

// 注册新的云存储驱动
Storage::extend('qiniu', function ($app, $config) {
    return new Filesystem(new QiniuAdapter('storage'));
});

编写好以上代码之后,就可以通过类似如下方式存储图片:

Storage::disk('qiniu')->write('test/academy/logo.png', storage_path('app/public/images/logo.png'));

我们在具体实现的过程中,可以在文件存储本地之后顺便将其存储到云存储中,在获取图片的时候通过 .env 中配置的域名加上上面保存的路径就可以获取到图片了。

镜像存储 & 融合CDN

七牛云还提供了镜像存储功能,通过这种方式我们一行代码都不用写,只需若干配置即可完成七牛云图片存储和 CDN 加速,在对象存储中选择一个存储空间:

对象存储

点击镜像存储填写镜像源(这里的镜像源即自己网站的地址):

镜像存储

然后配置绑定的融合 CDN 域名,比如 http://static.laravelacademy.org(这个域名需要在域名提供商处做 CNAME 解析),这样就完成配置了,接下来在自己的网站上将所有对静态资源的访问域名替换成融合 CDN 域名,这样当我们访问类似如下资源:

http://static.laravelacademy.org/wp-content/uploads/2017/09/logo.png

首先会去七牛 CDN 缓存中查找是否有这个资源,如果有的话直接返回,没有的话会回源到网站上查找这个资源:

http://laravelacademy.org/wp-content/uploads/2017/09/logo.png

如果这个资源不存在,返回404,如果存在则将其上传到七牛对象存储中,并保存到 CDN 缓存中以便下次获取直接返回,从而达到 CDN 加速,降低自己网站带宽负载的目的。

需要注意的是,如果静态资源有变动,比如 CSS、JavaScript 文件,需要在变动后通过刷新文件的方式刷新融合 CDN 中的缓存资源,否则不会立即生效:

刷新CDN缓存

防盗链设置

为了降低云存储CDN带宽费用,对图片进行防盗链设置很有必要,防盗链主要用于阻止第三方站点(如采集站点)对我方站点静态资源的引用(主要是图片),损耗我方宝贵的带宽资源,在七牛云中,设置图片防盗链也很简单,在融合CDN->域名管理列表页选择你要设置防盗链的域名,比如学院这边是 static.laravelacademy.org,点击配置链接,进入配置页面,下拉该页面到如下位置:

域名配置页面

点击「修改配置」按钮,参照学院配置对自己的站点进行防盗链设置:

七牛防盗链设置

主要涉及到三个配置:第一,开启防盗链,阻止第三方站点对己方站点图片的访问,第二,启用白名单功能,从自己站点访问图片是允许的,第三,允许空 Referer 对站点图片的访问,主要用于支持将图片分享到社交媒体(QQ、微信之类)。

修改完成后点击确定按钮,就配置好了,非常方便。

学院君 has written 1076 articles

Laravel学院院长,终身学习者

积分:134142 等级:P12 职业:手艺人 城市:杭州

4 条回复

登录后才能进行评论,立即登录?