权限系统

 

现在的应用存在两个巨大的安全隐患:

  1. 未登录用户可以访问 editupdate 动作;

  2. 已登录用户可以更新其它用户的个人信息;

接下来让我们针对这两个安全隐患进行修复。

必须先登录

Laravel 中间件 (Middleware) 为我们提供了一种非常棒的过滤机制来过滤进入应用的 HTTP 请求,例如,当我们使用 Auth 中间件来验证用户的身份时,如果用户未通过身份验证,则 Auth 中间件会把用户重定向到登录页面。如果用户通过了身份验证,则 Auth 中间件会通过此请求并接着往下执行。Laravel 框架默认为我们内置了一些中间件,例如身份验证、CSRF 保护等。所有的中间件文件都被放在项目的 app/Http/Middleware 文件夹中。
接下来让我们使用 Laravel 提供身份验证(Auth)中间件来过滤未登录用户的 edit, update 动作。
app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;
.
.
.
class UsersController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth', [            
            'except' => ['show', 'create', 'store']
        ]);
    }
    .
    .
    .
}

__construct 是 PHP 的构造器方法,当一个类对象被创建之前该方法将会被调用。我们在 __construct 方法中调用了 middleware 方法,该方法接收两个参数,第一个为中间件的名称,第二个为要进行过滤的动作。我们通过 except 方法来设定 指定动作 不使用 Auth 中间件进行过滤,意为 —— 除了此处指定的动作以外,所有其他动作都必须登录用户才能访问,类似于黑名单的过滤机制。相反的还有 only 白名单方法,将只过滤指定动作。我们提倡在控制器 Auth 中间件使用中,首选 except 方法,这样的话,当你新增一个控制器方法时,默认是安全的,此为最佳实践。
Laravel 提供的 Auth 中间件在过滤指定动作时,如该用户未通过身份验证(未登录用户),默认将会被重定向到 /login 登录页面。
此时退出登录,再次尝试访问 weibo.test/users/1/edit 页面将会被重定向到登录页面。

 

用户只能编辑自己的资料

在完成对未登录用户的限制之后,接下来我们要限制的是已登录用户的操作,当 id 为 1 的用户去尝试更新 id 为 2 的用户信息时,我们应该返回一个 403 禁止访问的异常。在 Laravel 中可以使用 授权策略 (Policy) 来对用户的操作权限进行验证,在用户未经授权进行操作时将返回 403 禁止访问的异常。

1. 创建授权策略

我们可以使用以下命令来生成一个名为 UserPolicy 的授权策略类文件,用于管理用户模型的授权。

$ php artisan make:policy UserPolicy

所有生成的授权策略文件都会被放置在 app/Policies 文件夹下。
让我们为默认生成的用户授权策略添加 update 方法,用于用户更新时的权限验证。
app/Policies/UserPolicy.php

<?php

namespace App\Policies;

use Illuminate\Auth\Access\HandlesAuthorization;
use App\Models\User;

class UserPolicy
{
    use HandlesAuthorization;

    public function update(User $currentUser, User $user)
    {
        return $currentUser->id === $user->id;
    }
}

update 方法接收两个参数,第一个参数默认为当前登录用户实例,第二个参数则为要进行授权的用户实例。当两个 id 相同时,则代表两个用户是相同用户,用户通过授权,可以接着进行下一个操作。如果 id 不相同的话,将抛出 403 异常信息来拒绝访问。
使用授权策略需要注意以下两点:

  1. 我们并不需要检查 $currentUser 是不是 NULL。未登录用户,框架会自动为其 所有权限 返回 false

  2. 调用时,默认情况下,我们 不需要 传递当前登录用户至该方法内,因为框架会自动加载当前登录用户(接着看下去,后面有例子)。

 

2. 注册授权策略

Laravel 提供两种注册授权策略的方式,第一种是手动指定,第二种是 Laravel 5.8 新增功能 —— 自动授权注册。为了方便起见,我们会使用第二种。
自动授权默认会假设 Model 模型文件直接存放在 app 目录下,鉴于我们已将模型存放目录修改为 app/Models,接下来还需自定义自动授权注册的规则,修改 boot() 方法:
app/Providers/AuthServiceProvider.php

<?php

namespace App\Providers;
.
.
.
class AuthServiceProvider extends ServiceProvider
{
    .
    .
    .
    public function boot()
    {
        $this->registerPolicies();
        // 修改策略自动发现的逻辑
        Gate::guessPolicyNamesUsing(function ($modelClass) {
            // 动态返回模型对应的策略名称,如:// 'App\Models\User' => 'App\Policies\UserPolicy',
            return 'App\Policies\\'.class_basename($modelClass).'Policy';
        });
    }
}

授权策略定义完成之后,我们便可以通过在用户控制器中使用 authorize 方法来验证用户授权策略。默认的 App\Http\Controllers\Controller 类包含了 Laravel 的 AuthorizesRequests trait。此 trait 提供了 authorize 方法,它可以被用于快速授权一个指定的行为,当无权限运行该行为时会抛出 HttpException。authorize 方法接收两个参数,第一个为授权策略的名称,第二个为进行授权验证的数据。
我们需要为 editupdate 方法加上这行:

$this->authorize('update', $user);
这里 update 是指授权类里的 update 授权方法,$user 对应传参 update 授权方法的第二个参数。正如上面定义 update 授权方法时候提起的,调用时,默认情况下,我们 不需要 传递第一个参数,也就是当前登录用户至该方法内,因为框架会自动加载当前登录用户。

书写的位置如下:
app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;
.
.
.
class UsersController extends Controller
{
    .
    .
    .
    public function edit(User $user)
    {
        $this->authorize('update', $user);
        return view('users.edit', compact('user'));
    }

    public function update(User $user, Request $request)
    {
        $this->authorize('update', $user);
        $this->validate($request, [
            'name' => 'required|max:50',
            'password' => 'nullable|confirmed|min:6'
        ]);

        $data = [];
        $data['name'] = $request->name;
        if ($request->password) {
            $data['password'] = bcrypt($request->password);
        }
        $user->update($data);

        session()->flash('success', '个人资料更新成功!');

        return redirect()->route('users.show', $user);
    }
}

最后,我们需要创建第二个用户来测试一下授权功能,首先让我们使用此命令进入 Tinker 环境:

$ php artisan tinker

如果中途想要退出 Tinker,可使用 ctrl + c 快捷键。
通过下面命令创建 ID 为 2 的用户:

>>> App\Models\User::create(['name'=> 'Monkey', 'email'=>'monkey@example.com','password'=>bcrypt('password')])

 

现在,使用 id 为 1 的用户登录,当访问 id 为 2 的用户编辑页面 —— weibo.test/users/2/edit ,系统将会拒绝访问。
权限系统

友好的重定向

当一个未登录的用户尝试访问自己的资料编辑页面时,将会自动跳转到登录页面,这时候如果用户再进行登录,则会重定向到其个人中心页面上,这种方式的用户体验并不好。更好的做法是,将用户重定向到他之前尝试访问的页面,即自己的个人编辑页面。redirect() 实例提供了一个 intended 方法,该方法可将页面重定向到上一次请求尝试访问的页面上,并接收一个默认跳转地址参数,当上一次请求记录为空时,跳转到默认地址上。
app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;
.
.
.
class SessionsController extends Controller
{
    .
    .
    .
    public function store(Request $request)
    {
       $credentials = $this->validate($request, [
           'email' => 'required|email|max:255',
           'password' => 'required'
       ]);

       if (Auth::attempt($credentials, $request->has('remember'))) {
           session()->flash('success', '欢迎回来!');
           $fallback = route('users.show', Auth::user());
           return redirect()->intended($fallback);
       } else {
           session()->flash('danger', '很抱歉,您的邮箱和密码不匹配');
           return redirect()->back()->withInput();
       }
    }
    .
    .
    .
}

现在尝试退出登录,并访问 weibo.test/users/1/edit 页面,页面将重定向到登录页面,这时候接着使用 id 为 1 的用户进行登录,在登录成功后页面将重定向到用户编辑页面上:

 

注册与登录页面访问限制

现在我们的应用还有一个小问题,即已登录用户还能够对注册页面和登录页面进行访问:

 

这明显不符合常规逻辑。
我们除了可通过 Auth 中间件的 auth 属性来对控制器的一些动作进行过滤,只允许已登录用户访问之外。还可以使用 Auth 中间件提供的 guest 选项,用于指定一些只允许未登录用户访问的动作,因此我们需要通过对 guest 属性进行设置,只让未登录用户访问登录页面和注册页面。
只让未登录用户访问登录页面:
app/Http/Controllers/SessionsController.php

<?php

namespace App\Http\Controllers;
.
.
.
class SessionsController extends Controller
{
    public function __construct()
    {
        $this->middleware('guest', [
            'only' => ['create']
        ]);
    }
    .
    .
    .
}

只让未登录用户访问注册页面:
app/Http/Controllers/UsersController.php

<?php

namespace App\Http\Controllers;
.
.
.
class UsersController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth', [
            'except' => ['show', 'create', 'store']
        ]);

        $this->middleware('guest', [
            'only' => ['create']
        ]);
    }
    .
    .
    .
}    

这时候访问 weibo.test/login
权限系统
会被跳转到 Laravel 默认指定的页面 /home ,因我们并没有此页面,所以会报错 404 找不到页面。我们需要修改下中间件里的 redirect() 方法调用,并加上友好的消息提醒:
app/Http/Middleware/RedirectIfAuthenticated.php

<?php
.
.
.
class RedirectIfAuthenticated
{
    .
    .
    .
    public function handle(Request $request, Closure $next, ...$guards)
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if (Auth::guard($guard)->check()) {
                session()->flash('info', '您已登录,无需再次操作。');
                return redirect(RouteServiceProvider::HOME);
            }
        }

        return $next($request);
    }
    .
    .
    .
    }
}

然后再修改下 RouteServiceProvider 类里的 HOME 属性:
app/Providers/RouteServiceProvider.php

.
.
.
class RouteServiceProvider extends ServiceProvider
{
    /**
     * The path to the "home" route for your application.
     *
     * This is used by Laravel authentication to redirect users after login.
     *
     * @var string
     */
    public const HOME = '/';
    .
    .
    .

这时候当你再次访问 weibo.test/login