使用 Laravel Gate 授權方式實現一個基於使用者角色的部落格釋出系統

zhangbao發表於2017-07-18

參考連結:https://laravel-news.com/authorization-gates

本文使用 Laravel 的 Gate 授權方式 實現一個基於使用者角色的部落格釋出系統。

在系統包含兩個使用者角色(作者編輯),它們對應的角色許可權如下:

  1. 作者能建立部落格
  2. 作者能更新自己的部落格
  3. 作者能釋出/不釋出自己的部落格
  4. 作者能刪除自己的部落格
  5. 編輯能更新所有部落格
  6. 編輯能釋出/不釋出所有部落格
  7. 編輯能刪除所有部落格

建立專案

laravel new blog

或者使用 composer create-project

composer create-project --prefer-dist laravel/laravel blog

配置資料庫連線

修改 .env 檔案中的資料庫連線資訊。

...
APP_URL=http://localhost
...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=dbname
DB_USERNAME=dbuser
DB_PASSWORD=yoursecretdbuserpassword
...

資料庫部分

建立 Post Model、posts 表的遷移檔案和 PostController。

php artisan make:model Post -m -c

開啟遷移檔案,補充 up 方法。

Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('body');
    $table->boolean('published')->default(false);
    $table->unsignedInteger('user_id');
    $table->timestamps();

    $table->foreign('user_id')->references('id')->on('users');
});

新增 roles 表和關係表 user_roles

  1. roles

建立 Role Model 和 Role 遷移檔案

php artisan make:model Role -m

開啟遷移檔案,補充 up 方法。

Schema::create('roles', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->string('slug')->unique();
    $table->jsonb('permissions'); // jsonb deletes duplicates
    $table->timestamps();
});
  1. 建立 user_roles 表遷移檔案
php artisan make:migration create_user_roles_table --create=user_roles

補充 up 方法

public function up()
{
    Schema::create('user_roles', function (Blueprint $table) {
        $table->unsignedInteger('user_id');
        $table->unsignedInteger('role_id');
        $table->timestamps();

        $table->unique(['user_id','role_id']);
        $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
    });
}

種子類

建立 RolesTableSeeder

php artisan make:seeder RolesTableSeeder

補充 run 方法。

use App\Role;

class RolesTableSeeder extends Seeder
{

    public function run()
    {
        $author = Role::create([
            'name' => '作家',
            'slug' => 'author',
            'permissions' => [
                'create-post' => true,
            ]
        ]);
        $editor = Role::create([
            'name' => '編輯',
            'slug' => 'editor',
            'permissions' => [
                'update-post' => true,
                'publish-post' => true,
                'delete-post' => true,
            ]
        ]);
    }

}

DatabaseSeeder 中註冊 RolesTableSeeder

$this->call(RolesTableSeeder::class);

UserRole Model

Role Model 裡的內容如下。

class Role extends Model
{
    protected $fillable = [
        'name', 'slug', 'permissions',
    ];
    protected $casts = [
        'permissions' => 'array',
    ];

    public function users()
    {
        return $this->belongsToMany(User::class, 'user_roles', 'role_id', 'user_id');
    }

    public function hasAccess($permission)
    {
        return $this->hasPermission($permission);
    }

    private function hasPermission($permission)
    {
        return isset($this->permissions[$permission]) ? $this->permissions[$permission] : false;
    }
}

User Model 修改內容如下。

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    public function roles()
    {
        return $this->belongsToMany(Role::class, 'user_roles', 'user_id', 'role_id');
    }

    /**
     * Checks if User has access to $permission.
     */
    public function hasAccess($permission)
    {
        // check if the permission is available in any role
        foreach ($this->roles as $role) {
            if($role->hasAccess($permission)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if the user belongs to role.
     */
    public function inRole($roleSlug)
    {
        return $this->roles()->where('slug', $roleSlug)->count() == 1;
    }
}

執行遷移並注入資料

php artisan migrate --seed

認證系統

建立認證系統。

php artisan make:auth

改寫註冊

RegisterController 中寫 showRegistrationForm 方法(覆蓋掉在 RegistersUsers trait 中定義的同名方法)。

Use App/Role;

...

public function showRegistrationForm()
{
    $roles = Role::orderBy('name')->pluck('name', 'id');
    return view('auth.register', compact('roles'));
}

編輯 resources/views/auth/register.blade.php,新增角色選擇項。

...

<div class="form-group{{ $errors->has('role') ? ' has-error' : '' }}">
    <label for="role" class="col-md-4 control-label">User role</label>

    <div class="col-md-6">
        <select id="role" class="form-control" name="role" required>
            @foreach($roles as $id => $role)
                <option value="{{ $id }}">{{ $role }}</option>
            @endforeach
        </select>

        @if ($errors->has('role'))
            <span class="help-block">
                <strong>{{ $errors->first('role') }}</strong>
            </span>
        @endif
    </div>
</div>

...

更新 RegisterController 中的 validator方法。

...

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => 'required|max:255',
        'email' => 'required|email|max:255|unique:users',
        'password' => 'required|min:6|confirmed',
        'role' => 'required|exists:roles,id', // validating role
    ]);
}

...

修改 create 方法,加入儲存使用者角色的業務邏輯。

...

protected function create(array $data)
{
    $user = User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => bcrypt($data['password']),
    ]);
    $user->roles()->attach($data['role']);
    return $user;
}

...

定義策略

修改 AuthServiceProvider,新增、註冊與操作播客相關的策略。

use App\Post;

...

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

public function registerPostPolicies()
{
    Gate::define('create-post', function ($user) {
        return $user->hasAccess('create-post');
    });
    Gate::define('update-post', function ($user, Post $post) {
        return $user->hasAccess('update-post') or $user->id == $post->user_id;
    });
    Gate::define('publish-post', function ($user, Post $post) {
        return $user->hasAccess('publish-post') or $user->id == $post->user_id;;
    });
    Gate::define('delete-post', function ($user, Post $post) {
        return $user->hasAccess('delete-post') or $user->id == $post->user_id;
    });
    Gate::define('see-all-drafts', function ($user) {
        return $user->inRole('editor');
    });
}

定義路由

修改 routes/web.php,新增下面的路由。

Route::get('/posts', 'PostController@index')->name('list_posts');
Route::group(['prefix' => 'posts'], function () {
    Route::get('/drafts', 'PostController@drafts')
        ->name('list_drafts')
        ->middleware('auth');
    Route::get('/show/{id}', 'PostController@show')
        ->name('show_post');
    Route::get('/create', 'PostController@create')
        ->name('create_post')
        ->middleware('can:create-post');
    Route::post('/create', 'PostController@store')
        ->name('store_post')
        ->middleware('can:create-post');
    Route::get('/edit/{post}', 'PostController@edit')
        ->name('edit_post')
        ->middleware('can:update-post,post');
    Route::post('/edit/{post}', 'PostController@update')
        ->name('update_post')
        ->middleware('can:update-post,post');
    Route::post('/delete/{post}', 'PostController@destory')
        ->name('delete_post')
        ->middleware('can:delete-post,post');
    // using get to simplify
    Route::get('/publish/{post}', 'PostController@publish')
        ->name('publish_post,post')
        ->middleware('can:publish-post');
    Route::get('/unpublish/{post}', 'PostController@unpublish')
        ->name('unpublish_post')
        ->middleware('can:publish-post,post');
});

部落格

Post Model

...

class Post extends Model
{
    protected $fillable = [
        'title', 'slug', 'body', 'user_id',
    ];

    public function owner()
    {
        return $this->belongsTo(User::class);
    }

    public function scopePublished($query)
    {
        return $query->where('published', true);
    }

    public function scopeUnpublished($query)
    {
        return $query->where('published', false);
    }
}

新增 PostController 裡的方法和相關檢視檔案

部落格列表

use App\Post;

...

public function index()
{
    $posts = Post::published()->latest()->paginate();
    return view('posts.index', compact('posts'));
}

...

建立 resources/views/posts/index.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">
                    Posts
                    @can('create-post')
                    <a class="pull-right btn btn-sm btn-primary" href="{{ route('create_post') }}">New</a>
                    @endcan
                </div>

                <div class="panel-body">
                    <div class="row">
                    @foreach($posts as $post)
                        <div class="col-sm-6 col-md-4">
                            <div class="thumbnail">
                            <div class="caption">
                                <h3><a href="{{ route('show_post', ['id' => $post->id]) }}">{{ $post->title }}</a></h3>
                                <p>{{ str_limit($post->body, 50) }}</p>
                                @can('update-post', $post)
                                <p>
                                    <a href="{{ route('edit_post', ['id' => $post->id]) }}" class="btn btn-sm btn-default" role="button">Edit</a> 
                                </p>
                                @endcan
                            </div>
                            </div>
                        </div>
                    @endforeach
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

建立部落格

...

public function create()
{
    return view('posts.create');
}

...

建立 posts\create.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">New Post</div>

                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ route('store_post') }}">
                        {{ csrf_field() }}

                        <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}">
                            <label for="title" class="col-md-2 control-label">Title</label>

                            <div class="col-md-9">
                                <input id="title" type="text" class="form-control" name="title" value="{{ old('title') }}" required autofocus>
                                @if ($errors->has('title'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('title') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}">
                            <label for="body" class="col-md-2 control-label">Body</label>

                            <div class="col-md-9">
                                <textarea name="body" id="body" cols="30" rows="10" class="form-control" required>{{ old('body') }}</textarea>
                                @if ($errors->has('body'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('body') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-2">
                                <button type="submit" class="btn btn-primary">
                                    Create
                                </button>
                                <a href="{{ route('list_posts') }}" class="btn btn-primary">
                                    Cancel
                                </a>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

儲存部落格

use Auth;
use App\Http\Requests\StorePost as StorePostRequest;

...

public function store(StorePostRequest $request)
{
    $data = $request->only('title', 'body');
    $data['slug'] = str_slug($data['title']);
    $data['user_id'] = Auth::user()->id;
    $post = Post::create($data);
    return redirect()->route('edit_post', ['id' => $post->id]);
}

...

建立處理儲存部落格時使用的請求類 StorePostRequest

php artisan make:request StorePostRequest

編輯 app/Http/Requests/StorePostRequest.php

public function authorize()
{
    return true; // gate will be responsible for access
}

public function rules()
{
    return [
        'title' => 'required|unique:posts',
        'body' => 'required',
    ];
}

部落格草稿列表

use Gate;

...

public function drafts()
{
    $postsQuery = Post::unpublished();
    if(Gate::denies('see-all-drafts')) {
        $postsQuery = $postsQuery->where('user_id', Auth::user()->id);
    }
    $posts = $postsQuery->paginate();
    return view('posts.drafts', compact('posts'));
}

...

建立 posts/drafts.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">
                    Drafts <a class="btn btn-sm btn-default pull-right" href="{{ route('list_posts') }}">Return</a>
                </div>

                <div class="panel-body">
                    <div class="row">
                    @foreach($posts as $post)
                        <div class="col-sm-6 col-md-4">
                            <div class="thumbnail">
                            <div class="caption">
                                <h3><a href="{{ route('show_post', ['id' => $post->id]) }}">{{ $post->title }}</a></h3>
                                <p>{{ str_limit($post->body, 50) }}</p>
                                <p>
                                @can('publish-post', $post)
                                    <a href="{{ route('publish_post', ['id' => $post->id]) }}" class="btn btn-default" role="button">Publish</a> 
                                @endcan
                                    <a href="{{ route('edit_post', ['id' => $post->id]) }}" class="btn btn-default" role="button">Edit</a> 
                                </p>
                            </div>
                            </div>
                        </div>
                    @endforeach
                        <div class="col-sm-12 col-md-12">
                            {{ $posts->links() }}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

修改 layouts/app.blade.php,新增“草稿”選單。

...

<ul class="dropdown-menu" role="menu">
    <li>
        <a href="{{ route('list_drafts') }}">Drafts</a>

...

編輯部落格

新增編輯部落格的方法 update

use App\Http\Requests\UpdatePostRequest as UpdatePostRequest;

...

public function edit(Post $post)
{
    return view('posts.edit', compact('post'));
}

public function update(Post $post, UpdatePostRequest $request)
{
    $data = $request->only('title', 'body');
    $data['slug'] = str_slug($data['title']);
    $post->fill($data)->save();
    return back();
}

建立處理更新部落格時使用的請求類 UpdatePostRequest

php artisan make:request UpdatePostRequest

編輯 app/Http/Requests/UpdatePostRequest.php

use Illuminate\Validation\Rule;

...

public function authorize()
{
    return true;
}

public function rules()
{
    $id = $this->route('post')->id;
    return [
        'title' => [
            'required',
            Rule::unique('posts')->where('id', '<>', $id),
        ],
        'body' => 'required',
    ];
}

建立檢視 posts/edit.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Update Post</div>

                <div class="panel-body">
                    <form class="form-horizontal" role="form" method="POST" action="{{ route('update_post', ['post' => $post->id]) }}">
                        {{ csrf_field() }}

                        <div class="form-group{{ $errors->has('title') ? ' has-error' : '' }}">
                            <label for="title" class="col-md-2 control-label">Title</label>

                            <div class="col-md-9">
                                <input id="title" type="text" class="form-control" name="title" value="{{ old('title', $post->title) }}" required autofocus>

                                @if ($errors->has('title'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('title') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group{{ $errors->has('body') ? ' has-error' : '' }}">
                            <label for="body" class="col-md-2 control-label">Body</label>

                            <div class="col-md-9">
                                <textarea name="body" id="body" cols="30" rows="10" class="form-control" required>{{ old('body', $post->body) }}</textarea>
                                @if ($errors->has('body'))
                                    <span class="help-block">
                                        <strong>{{ $errors->first('body') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-2">
                                <button type="submit" class="btn btn-primary">
                                    Update
                                </button>
                                @can('publish-post', $post)
                                    @if (!$post->published)
                                        <a href="{{ route('publish_post', ['post' => $post->id]) }}" class="btn btn-primary">
                                            Publish
                                        </a>
                                    @else
                                        <a href="{{ route('unpublish_post', ['post' => $post->id]) }}" class="btn btn-primary">
                                            Unpublish
                                        </a>
                                    @endif
                                @endcan
                                <a href="{{ route('list_posts') }}" class="btn btn-primary">
                                    Cancel
                                </a>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

釋出草稿/不釋出草稿

PostController 新增方法 publishunpublish

...

public function publish(Post $post)
{
    $post->published = true;
    $post->save();
    return redirect()->route('show_post', ['post' => $post->id]);
}

public function unpublish(Post $post)
{
    $post->published = false;
    $post->save();
    return back();
}

...

展示部落格

public function show(Post $post)
{
    $post = Post::findOrFail($id);
    if ($post->published || $post->user_id == Auth::user()->id) {
        return view('posts.show', compact('post'));
    }
    abort(403, 'Unauthorized.');
}

建立 posts/show.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">
                    {{ $post->title }}
                    <a class="btn btn-sm btn-default pull-right" href="{{ route('list_posts') }}">Return</a>
                </div>

                <div class="panel-body">
                    {{ $post->body }}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

刪除部落格

修改 PostController,新增 destory 方法。

public function destory(Post $post)
{
    $post->delete();
    return redirect()->route('list_post');;
}

修改 posts/edit.blade.php,新增刪除按鈕。

...

<div class="panel-heading">Update Post
    @can('delete-post', $post)
        <a class="pull-right btn btn-sm btn-danger" href="{{ route('delete_post', ['id' => $post->id]) }}"
           onclick="if(confirm('確定刪除嗎?') === false) { return false; } else {
           event.preventDefault(); document.getElementById('delete-post-form').submit();}">
            刪除
        </a>
        <form id="delete-post-form" action="{{ route('delete_post', ['post' => $post->id]) }}" method="POST" style="display: none;">
            {{ csrf_field() }}
        </form>
    @endcan
</div>

...

404 & 403

resources/views 下新建 errors 目錄,再在該目錄下新建 404.blade.php403.blade.php 頁面。

404

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">404</div>

                <div class="panel-body">
                    <h2>Not Found</h2>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

403

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">403</div>

                <div class="panel-body">
                    <h2>This Action is Unauthorized!</h2>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

使用 Markdown

這裡使用了依賴包 erusev/parsedown,使用它將 Markdown 文字裝換為 HTML。

composer require erusev/parsedown

修改 posts/show.blade.php

<div class="panel-body">
    {!! Parsedown::instance()->text($post->body) !!}
</div>

此刻,你就可以使用 Markdown 寫部落格了。

相關文章