24.用户授权
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频第 24 小节:Authorization with Policies
本节内容
上一节我们删除了话题,如果我们访问某个channel
而该channel
下无话题时,就会出现空白页面,对用户十分不友好:
我们进行下修改:
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
语法进行优化,现在刷新页面:
现在正式开始本节内容。上一节我们编写了一个测试:guests_cannot_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()
.
.
现在我们已经将两个测试放在了一起,测试一下:
修改控制器:
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');
}
.
.
再次测试:
但是如果我们运行全部测试:
先不要着急修改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 = [])
.
.
运行全部测试:
这意味着即使在页面存在删除按钮,如果当前用户尝试删除其他人的话题,也不能成功,且会重定向至登录页面:
现在我们更改一下,尝试该动作时,不是重定向至登录页面而是抛出异常:
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');
}
.
.
再次尝试删除:
但是我们如果运行一下全部测试,会发现有报错:
这是因为我们更改了控制器逻辑却没有一起修改测试逻辑,修复即可:
.
.
/** @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);
}
.
.
再次运行全部测试,测试通过:
一般而言,在应用中可以通过这样的方法来进行授权。但是在 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');
}
.
.
再次尝试不合法删除:
再次运行全部测试:
现在我们还需要做一件事情,前端页面无授权的用户不显示删除按钮:
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
.
.
此时刷新页面:
在浏览页面的时候发现个人页面有需要改进的地方,我们来改进一下:
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;
});
}
.
.
如果我们现在访问其他人创建的话题页面:
已经可以看到删除按钮,并且进行删除。