博文描述:
最近做了个基于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表中有三个用户
接下来你们就可以进行激动人心的验证了,好了,撒花~~