Menu

24.用户授权

本节说明

  • 对应视频第 24 小节:Authorization with Policies

本节内容

上一节我们删除了话题,如果我们访问某个channel而该channel下无话题时,就会出现空白页面,对用户十分不友好:
file
我们进行下修改:
forum\resources\views\threads\index.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                @forelse ($threads as $thread)
                    <div class="panel panel-default">
                        <div class="panel-heading">
                            <div class="level">
                                <h4 class="flex">
                                    <a href="{{ $thread->path() }}">
                                        {{ $thread->title }}
                                    </a>
                                </h4>

                                <a href="{{ $thread->path() }}">
                                    {{ $thread->replies_count }} {{ str_plural('reply',$thread->replies_count) }}
                                </a>
                            </div>
                        </div>

                        <div class="panel-body">
                                <div class="body">{{ $thread->body }}</div>
                        </div>
                    </div>
                @empty
                    <p>There are no relevant results at this time.</p>
                @endforelse
            </div>
        </div>
    </div>
@endsection

我们使用了@forelse语法进行优化,现在刷新页面:
file
现在正式开始本节内容。上一节我们编写了一个测试:unauthorized_users_may_not_delete_threads,用来测试未登录用户删除话题的情形。按照正常想法,我们应该增加另一个测试,测试没有权限的用户删除话题的情形。我们将这两种逻辑放在一起,叫做:unauthorized_users_may_not_delete_threads
forum\tests\Feature\CreateThreadsTest.php

.
.
public function unauthorized_users_may_not_delete_threads()
{
    $this->withExceptionHandling();

    $thread = create('App\Thread');

    $this->delete($thread->path())->assertRedirect('/login');

    $this->signIn();
    $this->delete($thread->path())->assertRedirect('/login');
}

/** @test */
public function a_thread_can_be_deleted()
.
.

现在我们已经将两个测试放在了一起,测试一下:
file
修改控制器:
forum\app\Http\Controllers\ThreadsController.php

.
.
public function destroy($channel,Thread $thread)
{
    if($thread->user_id != auth()->id()){
        if(request()->wantsJson()){
            return response(['status' => 'Permission Denied'],403);
        }

        return redirect('/login');
    }

    $thread->delete();

    if(request()->wantsJson()){
        return response([],204);
    }

    return redirect('/threads');
}
.
.

再次测试:
file
但是如果我们运行全部测试:
file
先不要着急修改a_thread_can_be_deleted。思考一下,这样的命名有点不太好,我们改成authorized_users_can_delete_threads。这样一来,未授权和已授权用户删除话题的测试就完整了:
forum\tests\Feature\CreateThreadsTest.php

.
.
/** @test */
public function unauthorized_users_may_not_delete_threads()
{
    $this->withExceptionHandling();

    $thread = create('App\Thread');

    $this->delete($thread->path())->assertRedirect('/login');

    $this->signIn();
    $this->delete($thread->path())->assertRedirect('/login');
}

/** @test */
public function authorized_users_can_delete_threads()
{
    $this->signIn();

    $thread = create('App\Thread',['user_id' => auth()->id()]);
    $reply = create('App\Reply',['thread_id' => $thread->id]);

    $response =  $this->json('DELETE',$thread->path());

    $response->assertStatus(204);

    $this->assertDatabaseMissing('threads',['id' => $thread->id]);
    $this->assertDatabaseMissing('replies',['id' => $reply->id]);
}

public function publishThread($overrides = [])
.
.

运行全部测试:
file
这意味着即使在页面存在删除按钮,如果当前用户尝试删除其他人的话题,也不能成功,且会重定向至登录页面:
file
现在我们更改一下,尝试该动作时,不是重定向至登录页面而是抛出异常:
forum\app\Http\Controllers\ThreadsController.php

.
.
public function destroy($channel,Thread $thread)
{
    if($thread->user_id != auth()->id()){
        abort(403,"You do not have permission to do this.");
    }

    $thread->delete();

    if(request()->wantsJson()){
        return response([],204);
    }

    return redirect('/threads');
}
.
.

再次尝试删除:
file
但是我们如果运行一下全部测试,会发现有报错:
file
这是因为我们更改了控制器逻辑却没有一起修改测试逻辑,修复即可:

.
.
/** @test */
public function unauthorized_users_may_not_delete_threads()
{
    $this->withExceptionHandling();

    $thread = create('App\Thread');

    $this->delete($thread->path())->assertRedirect('/login');

    $this->signIn();
    $this->delete($thread->path())->assertStatus(403);
}
.
.

再次运行全部测试,测试通过:
file
一般而言,在应用中可以通过这样的方法来进行授权。但是在 Laravel 中,我们有更好的选择。我们将使用 授权策略 来进行权限控制。首先新建一个策略类:

$ php artisan make:policy ThreadPolicy --model=Thread

修改update()方法:
forum\app\Policies\ThreadPolicy.php

<?php

namespace App\Policies;

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

class ThreadPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view the thread.
     *
     * @param  \App\User  $user
     * @param  \App\Thread  $thread
     * @return mixed
     */
    public function view(User $user, Thread $thread)
    {
        //
    }

    /**
     * Determine whether the user can create threads.
     *
     * @param  \App\User  $user
     * @return mixed
     */
    public function create(User $user)
    {
        //
    }

    /**
     * Determine whether the user can update the thread.
     *
     * @param  \App\User  $user
     * @param  \App\Thread  $thread
     * @return mixed
     */
    public function update(User $user, Thread $thread)
    {
        return $thread->user_id == $user->id;
    }

    /**
     * Determine whether the user can delete the thread.
     *
     * @param  \App\User  $user
     * @param  \App\Thread  $thread
     * @return mixed
     */
    public function delete(User $user, Thread $thread)
    {
        //
    }
}

注册策略:
forum\app\Providers\AuthServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Thread' => 'App\Policies\ThreadPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        //
    }
}

应用授权策略:
forum\app\Http\Controllers\ThreadsController.php

.
.
public function destroy($channel,Thread $thread)
{
    $this->authorize('update',$thread);  -->此处应用策略

    $thread->delete();

    if(request()->wantsJson()){
        return response([],204);
    }

    return redirect('/threads');
}
.
.

再次尝试不合法删除:
file
再次运行全部测试:
file
现在我们还需要做一件事情,前端页面无授权的用户不显示删除按钮:
forum\resources\views\threads\show.blade.php

.
.
@can('update',$thread)
    <form action="{{ $thread->path() }}" method="POST">
        {{ csrf_field() }}
        {{ method_field('DELETE') }}

        <button type="submit" class="btn btn-link">Delete Thread</button>
    </form>
@endcan
.
.

此时刷新页面:
file
在浏览页面的时候发现个人页面有需要改进的地方,我们来改进一下:
forum\resources\views\profiles\show.blade.php

.
.
@foreach($threads as $thread)
    <div class="panel panel-default">
        <div class="panel-heading">
            <div class="level">
        <span class="flex">
            <a href="{{ route('profile',$thread->creator) }}">{{ $thread->creator->name }}</a> 发表了
            <a href="{{ $thread->path() }}">{{ $thread->title }}</a>
        </span>

                <span>{{ $thread->created_at->diffForHumans() }}</span>
            </div>
        </div>

        <div class="panel-body">
            {{ $thread->body }}
        </div>
    </div>
@endforeach
.
.

可以看到我们为话题加上了链接。现在继续进行下一步。我们知道,对每一个系统而言,都有管理员的概念。在我们的系统中,我们认为用户 NoNo1 是管理员,并且他拥有所有的权限。我们将利用 Laravel 用户授权 的另外一种方式:Gates

可以把 gates 和策略类比于路由和控制器。 Gates 提供了一个简单的、基于闭包的方式来授权认证。策略则和控制器类似,在特定的模型或者资源中通过分组来实现授权认证的逻辑。

forum\app\Providers\AuthServiceProvider.php

.
.
public function boot()
{
    $this->registerPolicies();

    Gate::before(function($user){
        if ($user->name === 'NoNo1') return true;
    });
}
.
.

如果我们现在访问其他人创建的话题页面:
file
已经可以看到删除按钮,并且进行删除。

本文章首发在 Laravel China 社区
上一篇 下一篇
讨论数量: 0
发起讨论


暂无话题~
刻意练习,每日精进。
3
点赞
118
浏览
0
讨论

维护者