Laravel + JWT多模型用户认证(解决同一模型下只能一个用户进行所有接口的操作,其他用户只能访问登录接口,其他接口返回401的问题)

博文描述:

最近做了个基于laravel7 + jwt的一个小项目,给两个端提供服务,一个管理系统,一个小程序,使用jwt做认证服务,然后就想用jwt做多模型的认证


问题描述:

做完后发现一个问题,就是不管哪一个表中的第一个用户是可以操作所有接口的,但是其他的用户只能通过登录的接口,再用登录后的token去请求其他接口的时候,就会报401 user not found的错误

laravel + jwt实现多模型认证的操作步骤如下

这里laravel和jwt的安装配置默认都已安装好,还没安装的,可以看其他博主的文章,或直接跟着laravel中文文档JWT安装步骤去安装

先生成两个模型文件
使用命令 php artisan make:model Models/Admin -m生成管理员表


Schema::create('admins', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->string('mobile');
    $table->string('email');
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});

小程序模型的操作步骤都是一样的,你可以直接使用自带的User模型

接下来进行多模型认证配置
config\auth.php文件下

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt',  // 默认是 token
            'provider' => 'users',
        ],
        // 管理系统的看守器
        'admin' => [
            'driver' => 'jwt',
            'provider' => 'admins',
        ],
        // 小程序的看守器
        'qwtkd' => [
            'driver' => 'jwt',
            'provider' => 'qwtkds',
        ]
    ],


    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],
        'admins' => [    // 这里对应好上面看守器配置的服务名称
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],
        'qwtkds' => [
            'driver' => 'eloquent',
            'model' => App\Models\Qwtkd::class,
        ]
    ],

上面配置好后,接着创建好服务对应的模型

  • 注意,下面的操作我只操作了一个端,其他端的操作步骤都是一样的,我就不多废话了

依次在模型中配置好一下代码

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class Admin extends Authenticatable implements JWTSubject
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * 获取会储存到 jwt 声明中的标识
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * 返回包含要添加到 jwt 声明中的自定义键值对数组
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return ['role' => 'admin'];
    }

    /**
     * 获取用户信息
     * @param $id
     * @return Builder|Builder[]|Collection|Model|null
     */
    public function getUserInfo($id)
    {
        return self::query()->find($id);
    }
}

JWT的具体构成、签名结构、原理等问题可以点这里
以上代码中的getJWTCustomClaims()的作用就是用来自定义私有字段区分使用不同的模型的

再创建一个中间件用来验证

php artisan make:middleware JWTRoleAuth

<?php

namespace App\Http\Middleware;

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

class JWTRoleAuth extends BaseMiddleware
{
    /**
     * JWT 检测当前登录的平台
     *
     * @param Request $request
     * @param Closure $next
     * @param null $role
     * @return mixed
     */
    public function handle(Request $request, Closure $next, $role = null)
    {
        try {

            // 解析token角色
            $tokenRole = $this->auth->parseToken()->getClaim('role');
        } catch (JWTException $e) {
            /**
             * token解析失败,说明请求中没有可用的token。
             * 为了可以全局使用(不需要token的请求也可通过),这里让请求继续。
             * 因为这个中间件的责职只是校验token里的角色。
             */
            return $next($request);
        }
        // 判断token角色。
        if ($tokenRole != $role) {
            throw new UnauthorizedHttpException('jwt-auth', 'User role error');
        }
        return $next($request);
    }
}

中间件创建好后,不要忘记在 Http\Kernel.php文件下配置一下你创建好的路由,要在路由中间件组中配置哦,不要写错位置
在这里插入图片描述

然后再创建路由

# 普通用户登录
Route::group(['prefix' => 'auth'], function () {
    Route::post('login', 'AuthController@login');
    Route::post('logout', 'AuthController@logout');
    Route::post('refresh', 'AuthController@refresh');
    Route::post('me', 'AuthController@me')->name('me')->middleware(['jwt.role:user', 'jwt.auth']);
});

# 后台用户登录
Route::group(['prefix' => 'admin', 'namespace' => 'Admin'], function () {
    Route::post('login', 'LoginController@login');
    Route::post('logout', 'LoginController@logout');
    Route::post('refresh', 'LoginController@refresh');
    Route::get('me', 'LoginController@me')->middleware(['jwt.role:admin', 'jwt.auth'])->name('me');
});

这个时候就可以去测试一下了,如果猜的没错的话第一个用户的所有操作都是通畅无阻的,但是用数据库中第二个及其他用户操作的话,就只能使用登录接口,token也是正常返回的,但就是没办法使用其他接口

↓↓↓↓↓↓↓↓接下来开始找原因了↓↓↓↓↓↓↓

原因分析:

既然报错是user not found,那就找到报这个错误的位置
Tymon\JWTAuth\Http\Middleware;

    /**
     * Attempt to authenticate a user via the token in the request.
     *
     * @param  \Illuminate\Http\Request  $request
     *
     * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
     *
     * @return void
     */
    public function authenticate(Request $request)
    {
        $this->checkForToken($request);

        try {
            if (! $this->auth->parseToken()->authenticate()) {
            	// 这里出的错误
                throw new UnauthorizedHttpException('jwt-auth', 'User not found');
            }
        } catch (JWTException $e) {
            throw new UnauthorizedHttpException('jwt-auth', $e->getMessage(), $e, $e->getCode());
        }
    }

查看一下authenticate()方法
Tymon\JWTAuth;

    /**
     * Authenticate a user via a token.
     *
     * @return \Tymon\JWTAuth\Contracts\JWTSubject|false
     */
    public function authenticate()
    {
        $id = $this->getPayload()->get('sub');

        if (! $this->auth->byId($id)) {
            return false;
        }

        return $this->user();
    }

此处 $this->auth->byId($id)返回了false,真的是很费解啊

再往下找

Tymon\JWTAuth\Providers\Auth\Illuminate


use Illuminate\Contracts\Auth\Guard as GuardContract;
use Tymon\JWTAuth\Contracts\Providers\Auth;

    /**
     * The authentication guard.
     *
     * @var \Illuminate\Contracts\Auth\Guard
     */
    protected $auth;

    /**
     * Constructor.
     *
     * @param  \Illuminate\Contracts\Auth\Guard  $auth
     *
     * @return void
     */
    public function __construct(GuardContract $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Authenticate a user via the id.
     *
     * @param  mixed  $id
     *
     * @return bool
     */
    public function byId($id)
    {
        return $this->auth->onceUsingId($id);
    }

很明显,这里的看守器并不是我们期望的在config/auth.php文件里配置的看守器,用的是SessionGuard


解决方案:

既然他验证所用的看守器不是我们期望的,那我们就自己定义一个服务来验证吧

使用命令php artisan make:provider AdminProvider, 然后填充内容

<?php

namespace App\Providers;

use App\Models\Admin;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class AdminProvider extends EloquentUserProvider
{
    public function __construct($hasher, $model)
    {
        parent::__construct($hasher, $model);
    }

    public function retrieveById($identifier)     {
        // 减少每次认证请求数据库,直接请求缓存
         if ($model = Admin::getUserInfo($identifier)) {
             if ($model->state == 1) {
                 throw new UnauthorizedHttpException('jwt-auth');
             }
             if (!$model->openid) {
                 throw new UnauthorizedHttpException('jwt-auth');
             }
         }
         return $model;
    }

    public function retrieveByCredentials(array $credentials) {
        if ( empty($credentials) || (count($credentials) === 1 && Str::contains($this->firstCredentialKey($credentials), 'password'))) return false;

        // 首先,我们将每个凭证元素作为 where 子句添加到查询中
        // 然后我们可以执行查询,如果我们找到了一个用户,则将它返回到一个 Eloquent User “模型”中,该模型将被 Guard 实例使用。
        $query = $this->newModelQuery();
        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }
            if (is_array($value) || $value instanceof Arrayable)
            {
                $query->whereIn($key, $value);
            } else {
                $query->where($key, $value);
            }
        }
        if ($model = $query->first()) {
            Cache::forget('admin:info:' . $model->id);
            
            if ($model->is_lock == 1) {
                throw new UnauthorizedHttpException('jwt-auth', '账号被禁用', null, 1002);
            }
        }
        return $model;
    }
}

注意,这里我继承的不是基类的Provider,而是EloquentUserProvider,注意好引用

然后再App\Providers\AuthServiceProvider中写以下代码

<?php

namespace App\Providers;

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

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

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

        //Admin   注册好admin服务
        Auth::provider('admin', function ($app, $config) {
           return new AdminProvider($app['hash'], $config['model']);
        });
    }
}

其他端的验证顺序和这个是一样的,只需要创建好自定义的服务提供者,
App\Providers\AuthServiceProvider注册一下就好了

最后一个步骤

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Repositories\AdminRepositoris;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;

class AdminController extends Controller
{

    protected $admin_repositories;

    public function __construct(AdminRepositoris $admin_repositories)
    {
        $this->admin_repositories = $admin_repositories;
        // 比较麻烦的地方,就是每个控制器的构造函数都要加上这段代码来设置默认看守器
        Config::set('auth.defaults.guard', 'admin');
    }

    /**
     * 管理员列表
     * @return JsonResponse
     */
    public function getAdminList(): JsonResponse
    {
        $adminList = $this->admin_repositories->adminList();

        return response()->json([
            'code' => 200,
            'data' => $adminList,
            'message' => '获取成功'
        ]);
    }
}

比较麻烦的地方就在这里了,虽然能实现,但是必须在每个对应的控制器的构造函数中,添加一下默认的看守器

当前我的admin表中有三个用户

接下来你们就可以进行激动人心的验证了,好了,撒花~~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值