基于 Laravel Passport API 的多用户多字段认证系统(二):多用户登录

2. 扩展多用户登录

多用户登录在Passport的 这个Issue 里面有非常多的讨论,其他网站例如stackoverflow的诸多问题最后还是回到了这个Issue。
有一位叫做 sfelix-martins 的热心群众已经开发了 扩展包passport-multiauth。实现原理为Issue里面提及的,通过增加一张 oauth_access_token_providers 的扩展表。用户第一次登录,通过 "provider"参数传递需要关联的模型。以后每次通过Token传参时,在middleware里面做一层验证,把Token对应到相应的模型。不传 "provider"则默认为User模型。
有现成的轮子省了不少事,话不多说,直接开撸。

2–1. 引入multiauth扩展包

composer require smartins/passport-multiauth

2–2. 以老师和学生为例, 创建数据表。未使用常见的username字段。

<?php

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

class CreateTeachersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('teachers', function (Blueprint $table) {
            $table->increments('id');
            $table->string('school_id');
            $table->string('teacher_name');
            $table->string('password', 60);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('teachers');
    }
}
<?php

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

class CreateStudentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('students', function (Blueprint $table) {
            $table->increments('id');
            $table->string('school_id');
            $table->string('student_no');
            $table->string('name');
            $table->string('password', 60);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('students');
    }
}

并且在项目的app\Models文件夹,参考User, 建立对应的测试模型。

<?php

namespace App\Models;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Teacher extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'school_id', 'password',
    ];

    protected $hidden = [
        'password'
    ];

}
<?php

namespace App\Models;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Student extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'school_id', 'student_no', 'password',
    ];

    protected $hidden = [
        'password'
    ];

}

2–3. 数据表迁移

php artisan migrate

2–4. 老惯例,需要增加测试数据,建立Seeder文件并且导入

<?php

use Illuminate\Database\Seeder;
use App\Models\Student;
use App\Models\Teacher;

class StudentsAndTeacherSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Student::query()->truncate();
        Student::create([
            'school_id'  => '10001',
            'student_no' => '17000003001',
            'name'       => 'Abbey',
            'password'   => bcrypt('st001')
        ]);
        Student::create([
            'school_id'  => '10002',
            'student_no' => '17000003002',
            'name'       => 'Nana',
            'password'   => bcrypt('st002')
        ]);

        Teacher::query()->truncate();
        Teacher::create([
            'school_id'    => '10001',
            'teacher_name' => 'Kathy',
            'password'     => bcrypt('tt111')
        ]);
        Teacher::create([
            'school_id'    => '10001',
            'teacher_name' => 'Jack',
            'password'     => bcrypt('tt222')
        ]);

    }
}
composer dump-autoload

php artisan db:seed --class=StudentsAndTeacherSeeder

2–5. 配置文件 config/auth.php 中 providers 数组增加对应的模型

'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

'students' => [
            'driver' => 'eloquent',
            'model' => App\Models\Student::class,
        ],

'teachers' => [
            'driver' => 'eloquent',
            'model' => App\Models\Teacher::class,
        ],

],

2–6. 在文件 app/Http/Kernelmiddlewares 的$middlewareGroups中,注册自定义的PassportCustomProvider 和PassportCustomProviderAccessToken 。

class Kernel extends HttpKernel
{
    ...

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
            \Barryvdh\Cors\HandleCors::class,
            'custom-provider',
        ],

        'custom-provider' => [
            \SMartins\PassportMultiauth\Http\Middleware\AddCustomProvider::class,
            \SMartins\PassportMultiauth\Http\Middleware\ConfigAccessTokenCustomProvider::class,
        ]
    ];

    ...
}

2–7. 在 AuthServiceProvider 增加 access token 对应的 passport routes。

use Route;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    ...

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

        Passport::routes();

        Route::group(['middleware' => 'api'], function () {
            Passport::routes(function ($router) {
                return $router->forAccessTokens();
            });
        });
    }
    ...
}

2–8. 先测试下Teacher的登录,帐号和密码参照Seeder。

这是最常见的ID和密码登录方式,用户名或手机号或邮箱登录均类似。
按照文档,需要在参数里面增加provider=teachers,对应上面2–4.

curl --request POST \
  --url http://multiauth.test/oauth/token \
  --header 'accept: application/json' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'school_id=10001&password=tt111&client_id=2&client_secret=secret&grant_type=password&scope=&provider=teachers'

不出意外,可以看见以下的结果

{"error":"invalid_request","message":"The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.","hint":"Check the `username` parameter"}

既然一定要username,那我把school_id换成username试试:

curl --request POST \
  --url http://multiauth.test/oauth/token \
  --header 'accept: application/json' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'username=10001&password=tt111&client_id=2&client_secret=secret&grant_type=password&scope=&provider=teachers'

然鹅,还是报错登陆不上,因为数据结构里面压根就没有username这个字段啊。另外冒出来的email是什么东西。

SQLSTATE[42S22]: Column not found: 1054 Unknown column ‘email’ in ‘where clause’ (SQL: select * from `teachers` where `email` = 10001 limit 1)

What the f….算了,还是先查下网上有没有类似的问题吧。
这个Issue 里面,有人提出了一个简单的解决方案,使用官方文档中并未提及的 findForPassport函数。这个函数会把username映射为需要匹配的unique字段。既然都到这一步了,那还是追一下源码吧,在vendor/laravel/passport/src/Bridge/UserRepository.php 的41行,出现了这个函数。

if (method_exists($model, 'findForPassport')) {
            $user = (new $model)->findForPassport($username);
        } else {
            $user = (new $model)->where('email', $username)->first();
        }

如果在model里面存在findForPassport这个函数,则返回对应的model。
如果不存在,则去取email字段。因为我的Teacher表也没有email字段,所以报错 Unknown column ‘email’ 。

这段代码隶属于getUserEntityByUserCredentials这个函数,那哪里又调用了这个函数呢? 继续阅读源码+1.
在 vendor/league/oauth2-server/src/Grant/PasswordGrant.php,有一个validateUserd的函数调用了getUserEntityByUserCredentials。
并且username是写死的!!!

所以如果前端不传username,直接抛出invalidRequest异常,对应前面的 ”hint”:”Check the username parameter”

protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
    {
        $username = $this->getRequestParameter('username', $request);
        if (is_null($username)) {
            throw OAuthServerException::invalidRequest('username');
        }

$password = $this->getRequestParameter('password', $request);
        if (is_null($password)) {
            throw OAuthServerException::invalidRequest('password');
        }

$user = $this->userRepository->getUserEntityByUserCredentials(
            $username,
            $password,
            $this->getIdentifier(),
            $client
        );

老老实实的按照Issues修改model,再次测试,前端必须传递username。

<?php

namespace App\Models;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Teacher extends Authenticatable
{
    use HasApiTokens, Notifiable;

    protected $fillable = [
        'school_id', 'password',
    ];

    protected $hidden = [
        'password'
    ];

    public function findForPassport($username) {
        return $this->where('school_id', $username)->first();
    }

}
curl --request POST \
  --url http://multiauth.test/oauth/token \
  --header 'accept: application/json' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'username=10001&password=tt111&client_id=2&client_secret=secret&grant_type=password&scope=&provider=teachers'

现在可以成功登录,获取Teacher 的 Token了

{“token_type”:”Bearer”,”expires_in”:31536000,”access_token”:”eyJ0e...",”refresh_token”:”def50...”}

通过Token获取模型也很简单,传递 ‘api’ guard 到 user()即可。
修改app\routes\api.php如下:

use Illuminate\Http\Request;

Route::get('/user', function (Request $request) {
    return $request->user('api');
});

curl 测试

curl -X GET -H "Accept: application/json" -H "Authorization: Bearer eyJ0eX..." http://multiauth.test/api/user

成功获取到Teacher信息

{"id":1,"school_id":"10001","teacher_name":"Kathy","created_at":"2018-01-04 21:26:44","updated_at":"2018-01-04 21:26:44"}

如果使用1–10获取到的User的Token去访问相同的 api/user,会返回User的数据。 至此多用户登录基本成功。

基于 Laravel Passport API 的多用户多字段认证系统(三):多字段登录

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 1

该方法不可用,能提供包查看吗?
ConfigAccessTokenCustomProvider不能正确引入

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!