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 協議》,轉載必須註明作者和本文連結