7、基础 —— HTTP 路由

1、基本路由

所有应用路由都定义在 App\Providers\RouteServiceProvider 类载入的 app/Http/routes.php 文件中。
最基本的 Laravel 路由接收一个 URI 和一个闭包:

Route::get('foo', function () {
    return 'Hello World';
});

Route::post('foo', function () {
    //
});

默认情况下,routes.php 文件包含单个路由和一个路由群组,该路由群组包含的所有路由都使用了中间件组 web,而这个中间件组为路由提供了 Session 状态和  CSRF 保护功能。通常,我们会将所有路由定义在这个路由组中。

有效的路由方法

我们可以注册路由来响应任何 HTTP 请求:

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);

有时候还需要注册路由响应多个 HTTP 请求——这可以通过 match 方法来实现。或者,甚至可以使用 any 方法注册一个路由来响应所有 HTTP 请求:

Route::match(['get', 'post'], '/', function () {
    //
});

Route::any('foo', function () {
    //
});

2、路由参数

必选参数

有时我们需要在路由中捕获 URI 片段。比如,要从 URL 中捕获用户 ID,需要通过如下方式定义路由参数:

Route::get('user/{id}', function ($id) {
    return 'User '.$id;
});

可以按需要在路由中定义多个路由参数:

Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
    //
});

路由参数总是通过花括号进行包裹,这些参数在路由被执行时会被传递到路由的闭包。

注意:路由参数不能包含 - 字符,需要的话可以使用 _ 替代。

可选参数

有时候可能需要指定可选的路由参数,这可以通过在参数名后加一个 ? 标记来实现,这种情况下需要给相应的变量指定默认值:

Route::get('user/{name?}', function ($name = null) {
    return $name;
});

Route::get('user/{name?}', function ($name = 'John') {
    return $name;
});

3、命名路由

命名路由为生成 URL 或重定向提供了便利。实现也很简单,在定义路由时使用数组键 as 指定路由名称:

Route::get('user/profile', ['as' => 'profile', function () {
    //
}]);

此外,还可以为控制器动作指定路由名称:

Route::get('user/profile', [
    'as' => 'profile', 'uses' => 'UserController@showProfile'
]);

此外,除了在路由数组定义中指定路由名称外,还可以通过在路由定义之后使用 name 方法链的方式来实现:

Route::get('user/profile', 'UserController@showProfile')->name('profile');

路由群组 & 命名路由

如果你在使用路由群组,可以通过在路由群组的属性数组中指定 as 关键字来为群组中的路由设置一个共用的路由名前缀:

Route::group(['as' => 'admin::'], function () {
    Route::get('dashboard', ['as' => 'dashboard', function () {
        // 路由被命名为 "admin::dashboard"
    }]);
});

为命名路由生成URL

如果你为给定路由进行了命名,就可以通过 route 函数为该命名路由生成对应 URL:

$url = route('profile');
$redirect = redirect()->route('profile');

如果命名路由定义了参数,可以将该参数作为第二个参数传递给 route 函数。给定的路由参数将会自动插入 URL 中:

Route::get('user/{id}/profile', ['as' => 'profile', function ($id) {
     //
}]);
$url = route('profile', ['id' => 1]);

4、路由群组

路由群组允许我们在多个路由中共享路由属性,比如中间件和命名空间等,这样的话我们就不必为每一个路由单独定义属性。共享属性以数组的形式作为第一个参数被传递给 Route::group 方法。

下面我们通过几个简单的应用实例来演示路由群组。

中间件

要给路由群组中定义的所有路由分配中间件,可以在群组属性数组中使用 middleware。中间件将会按照数组中定义的顺序依次执行:

Route::group(['middleware' => 'auth'], function () {
    Route::get('/', function () {
        // 使用 Auth 中间件
    });

    Route::get('user/profile', function () {
        // 使用 Auth 中间件
    });
});

命名空间

另一个通用的例子是路由群组分配同一个 PHP 命名空间给其下的多个控制器,可以在分组属性数组中使用 namespace来指定群组中所有控制器的公共命名空间:

Route::group(['namespace' => 'Admin'], function(){
    // 控制器在 "App\Http\Controllers\Admin" 命名空间下

    Route::group(['namespace' => 'User'], function(){
        // 控制器在 "App\Http\Controllers\Admin\User" 命名空间下
    });
});

默认情况下,RouteServiceProvider 引入 routes.php 并指定其下所有控制器类所在的默认命名空间App\Http\Controllers,因此,我们在定义的时候只需要指定命名空间 App\Http\Controllers 之后的部分即可。

子域名路由

路由群组还可以被用于子域名路由通配符,子域名可以像 URI 一样被分配给路由参数,从而允许捕获子域名的部分用于路由或者控制器,子域名可以通过群组属性数组中的 domain 来指定:

Route::group(['domain' => '{account}.myapp.com'], function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

路由前缀

群组属性 prefix 可以用来为群组中每个路由添加一个给定 URI 前缀,比如,你可以为所有路由 URI 添加 admin 前缀 :

Route::group(['prefix' => 'admin'], function () {
    Route::get('users', function () {
        // 匹配 "/admin/users" URL
    });
});

你还可以使用 prefix 参数为路由群组指定公共路由参数:

Route::group(['prefix' => 'accounts/{account_id}'], function () {
    Route::get('detail', function ($account_id) {
        // 匹配 accounts/{account_id}/detail URL
    });
});

5、CSRF 攻击

简介

跨站请求伪造是一种通过伪装授权用户的请求来利用授信网站的恶意漏洞。Laravel 使得防止应用遭到跨站请求伪造攻击变得简单。

Laravel 自动为每一个被应用管理的有效用户会话生成一个 CSRF “令牌”,该令牌用于验证授权用户和发起请求者是否是同一个人。想要生成包含 CSRF 令牌的隐藏输入字段,可以使用帮助函数 csrf_field 来实现:

<?php echo csrf_field(); ?>

辅助函数 csrf_field 会生成如下 HTML:

<input type="hidden" name="_token" value="<?php echo csrf_token(); ?>">

当然还可以使用 Blade 模板引擎提供的方式:

{!! csrf_field() !!}

你不需要自己编写代码去验证 POST、PUT 或者 DELETE 请求的 CSRF 令牌,因为 Laravel 自带的 HTTP 中间件VerifyCsrfToken 会为我们做这项工作:将请求中输入的 token 值和 Session 中的存储的 token 作对比来进行验证。

从 CSRF 保护中排除指定 URL

有时候我们需要从 CSRF 保护中排除一些 URL,比如,如果你使用了 Stripe 来处理支付并用到他们的 webhook 系统,这时候就需要从 Laravel 的 CSRF 保护中排除  webhook 处理器路由。

要实现这一目的,你需要在 VerifyCsrfToken 中间件中将要排除的 URL 添加到 $except 属性:

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;

class VerifyCsrfToken extends BaseVerifier
{
    /**
     *从CSRF验证中排除的URL
     *
     * @var array
     */
    protected $except = [
        'stripe/*',
    ];
}

X-CSRF-Token

除了将 CSRF 令牌作为 POST 参数进行验证外,还可以通过设置 X-CSRF-Token 请求头来实现验证,VerifyCsrfToken 中间件会检查 X-CSRF-TOKEN 请求头,首先创建一个 meta 标签并将令牌保存到该 meta 标签:

<meta name="csrf-token" content="{{ csrf_token() }}">

然后在 js 库(如 jQuery)中添加该令牌到所有请求头,这为基于 AJAX 的应用提供了简单、方便的方式来避免 CSRF 攻击:

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});

X-XSRF-Token

Laravel 还会将 CSRF 令牌保存到了名为 XSRF-TOKEN 的 Cookie 中,你可以使用该 Cookie 值来设置 X-XSRF-TOKEN请求头。一些 JavaScript 框架,比如  Angular,会为你自动进行设置,基本上你不太需要手动设置这个值。

6、路由模型绑定

Laravel 路由模型绑定为注入类实例到路由提供了方便,例如,你可以将匹配给定 ID 的整个 User 类实例注入到路由中,而不是直接注入用户 ID。

隐式绑定

Laravel 会自动解析定义在路由或控制器动作(变量名匹配路由片段)中的 Eloquent 模型类型声明,例如:

Route::get('api/users/{user}', function (App\User $user) {
    return $user->email;
});

在这个例子中,由于类型声明了 Eloquent 模型 App\User,对应的变量名 $user 会匹配路由片段中的{user},这样,Laravel 会自动注入与请求 URI 中传入的 ID 对应的用户模型实例。

如果数据库中找不到对应的模型实例,会会自动生成 HTTP 404 响应。

自定义键名

如果你想要隐式模型绑定使用数据表的其它字段,可以重写 Eloquent 模型类的 getRouteKeyName 方法:

/**
 * Get the route key for the model.
 *
 * @return string
 */
public function getRouteKeyName()
{
    return 'slug';
}

显式绑定

要注册显式绑定,需要使用路由的 model 方法来为给定参数指定绑定类。应该在 RouteServiceProvider::boot 方法中定义模型绑定:

绑定参数到模型

public function boot(Router $router)
{
    parent::boot($router);
    $router->model('user', 'App\User');
}

接下来,定义一个包含 {user} 参数的路由:

$router->get('profile/{user}', function(App\User $user) {
     //
});

由于我们已经绑定 {user} 参数到 App\User 模型,User 实例会被注入到该路由。因此,如果请求 URL 是profile/1,就会注入一个用户 ID 为 1 的 User 实例。

如果匹配的模型实例在数据库不存在,会自动生成并返回 HTTP 404 响应。

自定义解析逻辑

如果你想要使用自定义的解析逻辑,需要使用 Route::bind 方法,传递到 bind 方法的闭包会获取到 URI 请求参数中的值,并且返回你想要在该路由中注入的类实例:

$router->bind('user', function($value) {
    return App\User::where('name', $value)->first();
});

自定义“Not Found”

如果你想要指定自己的“Not Found”行为,将封装该行为的闭包作为第三个参数传递给 model 方法:

$router->model('user', 'App\User', function() {
    throw new NotFoundHttpException;
});

7、表单方法伪造

HTML 表单不支持 PUT、PATCH 或者 DELETE 请求方法,因此,当 PUT、PATCH 或 DELETE 路由时,需要添加一个隐藏的 _method 字段到表单中,其值被用作该表单的 HTTP 请求方法:

<form action="/foo/bar" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
</form>

还可以使用辅助函数 method_field 来实现这一目的:

<?php echo method_field('PUT'); ?>

当然,也支持 Blade 模板引擎:

{{ method_field('PUT') }}