Write the Code. Change the World.

1月 27

完成了 web 的布局以及权限配置,现在要着手接口了。因为使用 ant design pro,进行前后端分离的项目。之前做了 User 模型的权限控制,这里想改成 Admin 模型,然后 jwt 基于 User 模型和 Admin 模型实现两套接口。这样之前的模型需要修改,以及需要创建 Admin 模型。

实现过程

# 创建模型,迁移以及控制器
php artisan make:model Models/Admin -mc

# 制作迁移文件,执行迁移,给 Admin 加白名单,并把之前 User 模型中的 hasRole 移到 Admin 模型中。最终的数据见下边

# CreateAdminsTable 迁移
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAdminsTable extends Migration
{
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id')->unique();
            $table->string('nickname', 128);
            $table->string('phone', 16);
            $table->string('address', 128)->nullable();
            $table->string('remark', 128)->nullable();
            $table->unsignedBigInteger('operator_user_id');
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->nullableTimestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('admins');
    }
}

# Admin 模型
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasRoles;

class Admin extends Model
{
    use HasRoles;

    protected $guard_name = 'admin';

    protected $fillable = ['user_id', 'nickname', 'phone', 'address', 'remark', 'operator_user_id'];

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

虽然上边这么做了。但是细节还是蛮多的。下边一一说明。

  1. 在 Admin 模型中,加入 `protected $guard_name = 'admin';

  2. 在 config/permission.php 中加入 'guard_name' => 'admin',。seeder 的时候会从这里取值。

  3. 在 config/auth.php 中 加入 guard 配置。

# 复制一份 web,修改 provider
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],

# 定义 provider
'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],

# 暂时先这样,然后执行迁移回滚再seeder。调用 tinker 看 role 就晓得了。

php artisan migrate:refresh --seed

php artisan tinker

$user = User::first();
# 这里就可以看到全部权限了
$user->admin->getAllPermissions ();

# 在 seeder 文件中,我们创建 role 以及 permision 的时候,都指定了具体的 guard_name 为 admin。

$guard_name = config('permission.guard_name') ?? 'web';

# 先这样吧,下一步, 我们安装 jwt,然后修改 guard。 这个时候 jwt 后台会使用到,将来接口也使用到。想要两者兼容,可以看下边的链接。

https://learnku.com/articles/28881

安装 jwt

这里操作。

# 安装 jwt
composer require tymon/jwt-auth:1.0.0-rc.5

# 生成 jwt secret
php artisan jwt:secret

# 修改 config/auth.php 中的 guard,指定 admin 的 driver 为jwt

        'admin' => [
            'driver' => 'jwt',
            'provider' => 'admins',
        ],

# 然后修改 Admin 模型,实现 JWTSubject

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasRoles;
use Tymon\JWTAuth\Contracts\JWTSubject;

class Admin extends Model implements JWTSubject
{
    use HasRoles;

    protected $guard_name = 'admin';

    protected $fillable = ['user_id', 'nickname', 'phone', 'address', 'remark', 'operator_user_id'];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return ['role' => 'admin'];
    }

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

# 将来用到接口的时候 User 模型一样要这波操作。

看起来好像好了,那么我们到 tinker 中操作一波试试。

php artisan tinker;

use App\Models\Admin;
use Auth;

$admin = Admin::first();
$token = Auth::guard('admin')->login($admin);

# 这个时候,发现报错了。原来是 Admin 这个模型模型没有继承 Auth,我们复制 User 模型这的 Auth 过来,然后继承一下。完整如下。

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;
use Tymon\JWTAuth\Contracts\JWTSubject;

class Admin extends Authenticatable implements JWTSubject
{
    use HasRoles;

    protected $guard_name = 'admin';

    protected $fillable = ['user_id', 'nickname', 'phone', 'address', 'remark', 'operator_user_id'];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return ['role' => 'admin'];
    }

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


# 修改好之后,我们再一波操作。发现生成的 token 是 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9hZG1pbi5jb20iLCJpYXQiOjE1ODAzMTI1MTUsImV4cCI6MTU4MDMxNjExNSwibmJmIjoxNTgwMzEyNTE1LCJqdGkiOiJsR25sZmlmcW9iSWFwQnc5Iiwic3ViIjoxLCJwcnYiOiJkZjg4M2RiOTdiZDA1ZWY4ZmY4NTA4MmQ2ODZjNDVlODMyZTU5M2E5Iiwicm9sZSI6ImFkbWluIn0.AOU3iiIgGHE6XuaioCDo3Ce8mZmRe63WLYKL_BDj1_w

# 我们复制 token 到 https://jwt.io/ 中,进行 decode,就可以还原出具体的数据 

    {
 typ: "JWT",
 alg: "HS256"
}.
{
 iss: "http:\/\/admin.com",
 iat: 1580312515,
 exp: 1580316115,
 nbf: 1580312515,
 jti: "lGnlfifqobIapBw9",
 sub: 1,
 prv: "df883db97bd05ef8ff85082d686c45e832e593a9",
 role: "admin"
}.
[signature]

# jwt 组成的 三部分都在

到了这里,部分细节要说一遍。

  1. 建立 Admin 模型。安装 jwt ,以及之前已经安装的 laravel permission。让 Admin 模型适应 jwt 以及 laravel permission。
  2. 新建 中间件。以区分 token 是授权 api 的 还是 admin 的。后边会列出来。
  3. 建立路由。默认有 web 和 api 两个路由。可以建一个 admin 的路由。然后在 RouteServiceProvider 中复制一份 api 的修改成 admin 的即可。

列出来

# 新增路由配置
    // 新增 admin 路由
    protected function mapAdminRoutes()
    {
        Route::prefix('admin')
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/admin.php'));
    }

# 中间件
<?php

namespace App\Http\Middleware;

use Closure;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;

class JWTRoleAuth extends BaseMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param $request
     * @param Closure $next
     * @param null $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role = null)
    {
        try {
            // 解析token角色
            $token_role = $this->auth->parseToken()->getClaim('role');
        } catch (JWTException $e) {
            /**
             * token解析失败,说明请求中没有可用的token。
             * 为了可以全局使用(不需要token的请求也可通过),这里让请求继续。
             * 因为这个中间件的责职只是校验token里的角色。
             */
            return $next($request);
        }

        // 判断token角色。
        if ($token_role != $role) {
            throw new UnauthorizedHttpException('jwt-auth', 'User role error');
        }

        return $next($request);
    }
}

# 并且要在 Kernel 的 routeMiddleware 中进行注册。
'jwt.role' => \App\Http\Middleware\JWTRoleAuth::class,

上边这些弄好了,就开始处理路由。

# 路由 admin.php
<?php

Route::namespace('Admin')->group(function(){

    Route::post('authorizations', 'AuthorizationsController@store')->name('api.authorizations.store');

    Route::group([
        'middleware' => ['jwt.role:admin', 'jwt.auth'],
    ], function () {
        // 刷新token
        Route::put('authorizations/current', 'AuthorizationsController@update')->name('authorizations.update');

        // 删除token
        Route::delete('authorizations/current', 'AuthorizationsController@destroy')->name('authorizations.destroy');
    });
});

测试发现,返回的是 html代码,并非是 json 数据。这个时候需要手动添加 Head 头 指定 Accept 为 application/json 才返回 json 格式。既然这样,就加一个中间件来返回 json 数据。

中间件

<?php

namespace App\Http\Middleware;

use Closure;

class AcceptHeader
{
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');

        return $next($request);
    }
}

并添加到 api 中间件组中,

        'api' => [
            \App\Http\Middleware\AcceptHeader::class,
            'throttle:60,1',
            'bindings',
        ],

# admin 路由 和 api 都将会用到这个中间件组。

这样在请求中,就不必每次指定 Header 头的 Accept 值。

1月 27

做好之前的功能,现在开始做权限管理以及后台。权限管理这里使用第三方扩展包 laravel-permission

laravel-permission github

https://docs.spatie.be/laravel-permission/v3/installation-laravel/

按照文档,进行常规操作。

安装使用 laravel-permission

基本步骤如下

composer require spatie/laravel-permission

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

为了更好的使用权限,在 权限表中加了一些额外的字段

# permissions 迁移中加入下边字段
$table->string('display_name');
$table->string('route')->nullable();
$table->integer('icon_id')->nullable();
$table->integer('parent_id')->default(0);
$table->integer('sort')->default(0);

# roles 迁移中加入下边字段
$table->string('display_name');

因为会用到 icon,所以得额外加一个 icons 表。生成一个额外的迁移文件

php artisan make:migration create_icons_tabel --create=icons

# 填充迁移文件
        Schema::create('icons', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('unicode')->nullable();
            $table->string('class')->nullable();
            $table->string('name')->nullable();
            $table->integer('sort')->default(0);
            $table->nullableTimestamps();
        });

# 执行迁移
php artisan migrate

怎么使用呢,看 wiki https://docs.spatie.be/laravel-permission/v3/basic-usage/basic-usage/

  1. 在模型中加入 Spatie\Permission\Traits\HasRoles trait,可以是 User 模型也可以是其他,看你怎么用。
  2. 虽然可以给单独模型赋予权限,不建议这样做。建议通过 role 来实现。先给 role 赋予权限,再给用户赋予角色。这样更好管理和控制权限。

完成了上边的操作,我们还需要做两个数据填充(Seeders),一个 IconSeeder 和 PermissionSeeder 。

php artisan make:seeder IconSeeder

php artisan make:seeder PermissionSeeder

既然要整 icon ,那我们就用阿里的 iconfonts 。去 https://www.iconfont.cn/ 操作一波。

制作好了之后,先创建 json 文件:

vim public/backstage/json/icons.json

# 填充以下内容(以3个icon为例)
[{"unicode": "", "name": "控制台", "class": "icon-kongzhitai"},
 {"unicode": "", "name": "权限", "class": "icon-quanxian"},
 {"unicode": "", "name": "仪表盘", "class": "icon-yibiaopan"}]

再去编辑 IconSeeder。

# 创建 icon
php artisan make:model Models/Icon

# 白名单
protected $fillable = ['unicode','class','name','sort'];

# IconSeeder
<?php

use Illuminate\Database\Seeder;
use App\Models\Icon;

class IconSeeder extends Seeder
{
    public function run()
    {
        Icon::truncate();
        $file = file_get_contents(public_path().'/backstage/json/icons.json');
        $icons = json_decode($file, true);
        foreach ($icons as $icon){
            Icon::create($icon);
        }
    }
}

PermissionSeeder 的迁移文件可参考: https://docs.spatie.be/laravel-permission/v3/advanced-usage/seeding/

根据需求,先暂时建立几个权限以及角色。迁移文件如下:

php artisan make:model Models/Permission

php artisan make:model Models/Role

# Permission Model
<?php

namespace App\Models;

use Spatie\Permission\Models\Permission as SPermission;

class Permission extends SPermission
{
    //菜单图标
    public function icon()
    {
        return $this->belongsTo('App\Models\Icon', 'icon_id', 'id');
    }

    //子权限
    public function childs()
    {
        return $this->hasMany('App\Models\Permission', 'parent_id', 'id');
    }
}

# Role Model
<?php

namespace App\Models;

use Spatie\Permission\Models\Role as SRole;

class Role extends SRole
{

}

然后,permission seeder。

<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\PermissionRegistrar;
use App\Models\User;
use App\Models\Permission;
use App\Models\Role;
use DB;
use Hash;

class PermissionSeeder extends Seeder
{
    public function run()
    {
        app(PermissionRegistrar::class)->forgetCachedPermissions();

        $tableNames = config('permission.table_names');
        Model::unguard();
        foreach ($tableNames as $tableName) {
            DB::table($tableName)->delete();
        }
        Model::reguard();

        $user = User::where('account', '13671638524')->first();
        if (!$user) {
            $user = User::create([
                'account' => '13671638524',
                'password' => Hash::make('w123456'),
                'email' => 'lichking_lin86@qq.com',
                'phone' => '13671638524'
            ]);
        }

        $role = Role::create([
            'name' => 'root',
            'display_name' => '站长'
        ]);

        $user->assignRole($role);

        $permissions = [
            [
                'name' => 'home.manager',
                'display_name' => '主页',
                'route' => '',
                'icon_id' => '1',
                'child' => [
                    [
                        'name' => 'home',
                        'display_name' => '控制台',
                        'route' => 'admin.home',
                        'icon_id' => '12',
                        'child' => []
                    ]
                ]
            ],
            [
                'name' => 'system.manage',
                'display_name' => '系统管理',
                'route' => '',
                'icon_id' => '2',
                'child' => [
                    [
                        'name' => 'system.permission',
                        'display_name' => '权限管理',
                        'route' => 'admin.system.permission',
                        'icon_id' => '2',
                        'child' => [
                            ['name' => 'system.permission.create', 'display_name' => '添加权限', 'route' => 'admin.system.permission.create'],
                            ['name' => 'system.permission.edit', 'display_name' => '编辑权限', 'route' => 'admin.system.permission.edit'],
                            ['name' => 'system.permission.destroy', 'display_name' => '删除权限', 'route' => 'admin.system.permission.destroy'],
                        ]
                    ],
                    [
                        'name' => 'system.role',
                        'display_name' => '角色管理',
                        'route' => 'admin.system.role',
                        'icon_id' => '3',
                        'child' => [
                            ['name' => 'system.role.create', 'display_name' => '添加角色', 'route' => 'admin.system.role.create'],
                            ['name' => 'system.role.edit', 'display_name' => '编辑角色', 'route' => 'admin.system.role.edit'],
                            ['name' => 'system.role.destroy', 'display_name' => '删除角色', 'route' => 'admin.system.role.destroy'],
                            ['name' => 'system.role.permission', 'display_name' => '分配权限', 'route' => 'admin.system.role.permission'],
                        ]
                    ],
                    [
                        'name' => 'system.manager',
                        'display_name' => '管理员',
                        'route' => 'admin.system.manager',
                        'icon_id' => '123',
                        'child' => [
                            ['name' => 'system.manager.create', 'display_name' => '添加管理', 'route' => 'admin.system.manager.create'],
                            ['name' => 'system.manager.edit', 'display_name' => '编辑管理', 'route' => 'admin.system.manager.edit'],
                            ['name' => 'system.manager.destroy', 'display_name' => '删除管理', 'route' => 'admin.system.manager.destroy'],
                            ['name' => 'system.manager.role', 'display_name' => '分配角色', 'route' => 'admin.system.manager.role'],
                            ['name' => 'system.manager.permission', 'display_name' => '分配权限', 'route' => 'admin.system.manager.permission'],
                        ]
                    ]
                ]
            ]
        ];

        foreach ($permissions as $pem1) {
            //生成一级权限
            $p1 = Permission::create([
                'name' => $pem1['name'],
                'display_name' => $pem1['display_name'],
                'route' => $pem1['route'] ?? '',
                'icon_id' => $pem1['icon_id'] ?? 1,
            ]);
            //为角色添加权限
            $role->givePermissionTo($p1);
            if (isset($pem1['child'])) {
                foreach ($pem1['child'] as $pem2) {
                    //生成二级权限
                    $p2 = Permission::create([
                        'name' => $pem2['name'],
                        'display_name' => $pem2['display_name'],
                        'parent_id' => $p1->id,
                        'route' => $pem2['route'] ?? 1,
                        'icon_id' => $pem2['icon_id'] ?? 1,
                    ]);
                    //为角色添加权限
                    $role->givePermissionTo($p2);
                    if (isset($pem2['child'])) {
                        foreach ($pem2['child'] as $pem3) {
                            //生成三级权限
                            $p3 = \App\Models\Permission::create([
                                'name' => $pem3['name'],
                                'display_name' => $pem3['display_name'],
                                'parent_id' => $p2->id,
                                'route' => $pem3['route'] ?? '',
                                'icon_id' => $pem3['icon_id'] ?? 1,
                            ]);
                            //为角色添加权限
                            $role->givePermissionTo($p3);
                        }
                    }
                }
            }
        }

        //初始化的角色
        $roles = [
            ['name' => 'manager', 'display_name' => '管理员']
        ];

        foreach ($roles as $role) {
            Role::create($role);
        }
    }
}

再在 DatabaseSeeder 中加入:

        $this->call(IconSeeder::class);
        $this->call(PermissionSeeder::class);

# 然后迁移回滚操作
php artisan migrate:refresh --seed -v

添加中间件。在 app/Http/Kernel.php 中的 routeMiddleware 增加映射

        'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
        'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,