Listening to the Words

laravel学习之帖子的CRUD


模型观察器

Eloquent 模型会触发许多事件(Event),我们可以对模型的生命周期内多个时间点进行监控: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored。
事件让你每当有特定的模型类在数据库保存或更新时,执行代码。当一个新模型被初次保存将会触发 creating 以及 created 事件。
如果一个模型已经存在于数据库且调用了 save 方法,将会触发 updatingupdated 事件。在这两种情况下都会触发 savingsaved 事件。

  • 创建观察器文件,一个普通类,不需要继承什么
  • 针对需要的事件,编写对应的 ~ing 或 ~ed 方法,方法接收 model 作为唯一参数
  • 在 AppServiceProvider 中注册
public function boot()
    {
        \App\Models\User::observe(\App\Observers\UserObserver::class);
        \App\Models\Topic::observe(\App\Observers\TopicObserver::class);

    }
<?php

namespace App\Observers;

use App\Models\Topic;

// creating, created, updating, updated, saving,
// saved,  deleting, deleted, restoring, restored

class TopicObserver
{
    public function saving(Topic $topic)
    {
        $topic->excerpt = make_excerpt($topic->body);
    }
}


XSS安全漏洞

目前我们的项目中存在非常严重的 XSS 安全漏洞。作为一个合格的 Web 开发工程师,必须遵循一个安全原则:

永远不要信任用户提交的数据。

XSS 也称跨站脚本攻击 (Cross Site Scripting),恶意攻击者往 Web 页面里插入恶意 JavaScript 代码,当用户浏览该页之时,嵌入其中 Web 里面的 JavaScript 代码会被执行,从而达到恶意攻击用户的目的

一种比较常见的 XSS 攻击是 Cookie 窃取。我们都知道网站是通过 Cookie 来辨别用户身份的,一旦恶意攻击者能在页面中执行 JavaScript 代码,他们即可通过 JavaScript 读取并窃取你的 Cookie,拿到你的 Cookie 以后即可伪造你的身份登录网站。

有两种方法可以避免 XSS 攻击:

第一种,对用户提交的数据进行过滤;
第二种,Web 网页显示时对数据进行特殊处理,一般使用 htmlspecialchars() 输出。

Laravel 的 Blade 语法 {{ }} 会自动调用 PHP htmlspecialchars 函数来避免 XSS 攻击。但是因为我们支持 WYSIWYG 编辑器,我们使用的是 {!! !!} 来打印用户提交的话题内容:

<div class="topic-body">
    {!! $topic->body !!}
</div>

Blade 的 {!! !!} 语法是直接数据,不会对数据做任何处理。在我们这种场景下,因为业务逻辑的特殊性,第二种方法不适用,我们将使用第一种方法,对用户提交的数据进行过滤来避免 XSS 攻击。


HTMLPurifier

HTMLPurifier 本身就是一个独立的项目,运用『白名单机制』对 HTML 文本信息进行 XSS 过滤。

『白名单机制』指的是使用配置信息来定义『HTML 标签』、『标签属性』和『CSS 属性』数组,在执行 clean() 方法时,只允许配置信息『白名单』里出现的元素通过,其他都进行过滤。

如配置信息:

'HTML.Allowed' => 'div,em,a[href|title|style],ul,ol,li,p[style],br',
'CSS.AllowedProperties'    => 'font,font-size,font-weight,font-style,font-family',

当用户提交时:

<a someproperty="somevalue" href="http://example.com" style="color:#ccc;font-size:16px">
    文章内容<script>alert('Alerted')</script>
</a>

会被解析为:

<a href="http://example.com" style="font-size:16px">
    文章内容
</a>

HTMLPurifier for Laravel 5

HTMLPurifier for Laravel 是对 HTMLPurifier 针对 Laravel 框架的一个封装。本章节中,我们将使用此扩展包来对用户内容进行过滤。

链接:htmlpurifier for laravel使用教程

过滤

一切准备就绪,现在我们只需要在数据入库前进行过滤即可:

模型观察器

app/Observers/TopicObserver.php

<?php

namespace App\Observers;

use App\Models\Topic;

// creating, created, updating, updated, saving,
// saved,  deleting, deleted, restoring, restored

class TopicObserver
{
    public function saving(Topic $topic)
    {
        $topic->body = clean($topic->body, 'user_topic_body');

        $topic->excerpt = make_excerpt($topic->body);
    }
}

使用队列

配置队列

链接: 队列使用教程

一般情况下,网络请求会存在各种不确定性,如果请求 API 出现超时情况,或者发生不可预知的错误,我们的用户将无法发帖。

生成 Slug 只是一个 优化 功能,并非是发帖的 必要 功能,我们希望无论生成 Slug 的结果如何,用户都能顺利的发帖,并且完全察觉不到延迟。

利用队列系统可以做到这点。队列允许你异步执行消耗时间的任务,比如请求一个 API 并等待返回的结果。这样可以有效的降低请求响应的时间。

队列的配置信息储存于 config/queue.php 文件中,在这个文件中你会发现框架所支持的队列驱动的配置连接示例。这些驱动包括:数据库,Beanstalkd,Amazon SQS,Redis,和一个同步(本地使用)的驱动。还有一个名为 null 的驱动表明不使用队列任务。

本项目中,我们将使用 Redis 来作为我们的队列驱动器,先使用 Composer 安装依赖:

$ composer require "predis/predis:~1.0"

接下来我们还需要修改环境变量 QUEUE_DRIVER 的值为 redis.env

QUEUE_DRIVER=redis

失败任务

有时候队列中的任务会失败。Laravel 内置了一个方便的方式来指定任务重试的最大次数。当任务超出这个重试次数后,它就会被插入到 failed_jobs 数据表里面。我们可以使用 queue:failed-table 命令来创建 failed_jobs 表的迁移文件:

$ php artisan queue:failed-table

会新建 database/migrations/{timestamp}_create_failed_jobs_table.php 文件:

《laravel学习之帖子的CRUD》

接着使用 migrate Artisan 命令生成 failed_jobs 表:

php artisan migrate

生成任务类

使用以下 Artisan 命令来生成一个新的队列任务:

$ php artisan make:job TranslateSlug

该命令会在 app/Jobs 目录下生成一个新的类:

app/Jobs/TranslateSlug

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

use App\Models\Topic;
use App\Handlers\SlugTranslateHandler;

class TranslateSlug implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $topic;

    public function __construct(Topic $topic)
    {
        // 队列任务构造器中接收了 Eloquent 模型,将会只序列化模型的 ID
        $this->topic = $topic;
    }

    public function handle()
    {
        // 请求百度 API 接口进行翻译
        $slug = app(SlugTranslateHandler::class)->translate($this->topic->title);

        // 为了避免模型监控器死循环调用,我们使用 DB 类直接对数据库进行操作
        \DB::table('topics')->where('id', $this->topic->id)->update(['slug' => $slug]);
    }
}

该类实现了 Illuminate\Contracts\Queue\ShouldQueue 接口,该接口表明 Laravel 应该将该任务添加到后台的任务队列中,而不是同步执行。

引入了 SerializesModels trait,Eloquent 模型会被优雅的序列化和反序列化。队列任务构造器中接收了 Eloquent 模型,将会只序列化模型的 ID。这样子在任务执行时,队列系统会从数据库中自动的根据 ID 检索出模型实例。这样可以避免序列化完整的模型可能在队列中出现的问题。

handle 方法会在队列任务执行时被调用。值得注意的是,我们可以在任务的 handle 方法中可以使用类型提示来进行依赖的注入。Laravel 的服务容器会自动的将这些依赖注入进去,与控制器方法类似。

还有一点需要注意,我们将会在模型监控器中分发任务,任务中要避免使用 Eloquent 模型接口调用,如:create(), update(), save() 等操作。否则会陷入调用死循环 —— 模型监控器分发任务,任务触发模型监控器,模型监控器再次分发任务,任务再次触发模型监控器…. 死循环。在这种情况下,使用 DB 类直接对数据库进行操作即可。

任务分发

接下来我们要修改 Topic 模型监控器,将 Slug 翻译的调用修改为队列执行的方式:

app/Observers/TopicObserver.php

<?php

namespace App\Observers;

use App\Models\Topic;
use App\Jobs\TranslateSlug;

// creating, created, updating, updated, saving,
// saved,  deleting, deleted, restoring, restored

class TopicObserver
{
    public function saved(Topic $topic)
    {
        // XSS 过滤
        $topic->body = clean($topic->body, 'user_topic_body');

        // 生成话题摘录
        $topic->excerpt = make_excerpt($topic->body);

        // 如 slug 字段无内容,即使用翻译器对 title 进行翻译
        if ( ! $topic->slug) {

            // 推送任务到队列
            dispatch(new TranslateSlug($topic));
        }
    }
}

总结

本章主要学习了以下知识点:

模型观察器监控 Eloquent 模型事件;
熟悉代码生成器生成的表单验证类;
集成 WYSIWYG 编辑器;
编辑器上传图片;
知悉 XSS 的机制,并使用 HTMLPurifier 作为防范;
使用 Policy 授权策略类来控制用户权限;
使用 Guzzle 调用外部 API —— 百度翻译 API;
可选路由参数的使用;
队列系统的使用,新建任务类,命令行队列监听;
Horizon 命令行和仪表板使用;

点赞