說明
使用 passport 進行 admin 端和 customer 端的使用者認證。
雖然教程很多,但是我並沒有參照其他教程完整的走下來,所以記錄了自己的開發流程,希望能對其他人有所幫助。
安裝專案
laravel new passport
安裝 passport
composer require laravel/passport
資料遷移
首先我們需要建立 admins 和 customers 表,並填充假資料
php artisan make:migration create_admins_table --create=admins
php artisan make:migration create_customers_table --create=customers
php artisan make:seeder AdminsTableSeeder
php artisan make:seeder CustomersTableSeeder
執行遷移
php artisan migrate --seed
passport 初始化
php artisan passport:install
此時在 storage 下會生成 oauth-private.key 和 oauth-public.key
生成認證
目前我們只是為前後端分離的後臺使用,所以 password 模式足夠
php artisan passport:client --password --name='passport-admin'
php artisan passport:client --password --name='passport-customer'
備註
原先我以為這裡採用不一樣的資料之後下面 token 不會出現複用的情況,然而和這個沒有關係
token 複用是指 admin 端生成的 1 號使用者的 token 去請求 customer 端時,依然有效
解決方法下文會有介紹
修改路由配置
找到 app/Providers/RouteServiceProvider.php, 增加如下程式碼
public function map()
{
·
·
·
// admin 路由
$this->mapAdminRoutes();
// customer 路由
$this->mapCustomerRoutes();
}
protected function mapAdminRoutes()
{
Route::prefix('admin')
->namespace($this->namespace . '\Admin')
->group(base_path('routes/admin.php'));
}
protected function mapCustomerRoutes()
{
Route::prefix('customer')
->namespace($this->namespace . '\Customer')
->group(base_path('routes/customer.php'));
}
在 routes 下新建 admin.php 和 customer.php
分別增加如下程式碼
admin.php
<?php
Route::group([
'middleware' => 'passport-guard'
], function () {
// 登入
Route::post('login', 'AuthController@login');
// 重新整理 token
Route::put('refresh', 'AuthController@refresh');
Route::group([
'middleware' => ['auth:api', 'scopes:admin']
], function () {
// 退出
Route::delete('logout', 'AuthController@logout');
// 詳情
Route::get('admins/current', 'AdminsController@current');
});
});
customer.php
<?php
Route::group([
'middleware' => 'passport-guard'
], function () {
// 登入
Route::post('login', 'AuthController@login');
// 重新整理 token
Route::put('refresh', 'AuthController@refresh');
Route::group([
'middleware' => ['auth:api', 'scopes:customer']
], function () {
// 退出
Route::delete('logout', 'AuthController@logout');
// 詳情
Route::get('customers/current', 'CustomersController@current');
});
});
備註
此處 auth:api 是檢驗 token
scopes:admin, scopes:customer 是給 token 指定作用域,即防止上文 token 複用的情況出現
建立中介軟體
php artisan make:middleware PassportGuard
增加如下程式碼
因為 passport 預設使用的是 api 守衛,並且不支援傳參修改,所以需要通過中介軟體修改 provider
public function handle($request, Closure $next)
{
try {
if ($request->is('admin/*')) {// 如果是 admin 路由
config(['auth.guards.api.provider' => 'admins']);
} elseif ($request->is('customer/*')) { // 如果是 customer 路由
config(['auth.guards.api.provider' => 'customers']);
}
} catch (\Exception $exception) {
throw new $exception;
}
return $next($request);
}
找到 app/Http/Kernel.php,註冊中介軟體
protected $routeMiddleware = [
·
·
·
// passport 認證路由
'passport-guard' => \App\Http\Middleware\PassportGuard::class
// token 作用域
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
];
修改 Providers
找到 app/Http/Providers
增加如下程式碼
use Laravel\Passport\Passport;
use Laravel\Passport\RouteRegistrar;
public function boot()
{
·
·
·
// Passport 路由註冊
$prefix = '';
if (request()->is('admin/*')) {
$prefix = 'admin';
} elseif (request()->is('customer/*')) {
$prefix = 'customer';
}
// 我們只需要前後端分離的形式, 而不需要認證
Passport::routes(function (RouteRegistrar $router) {
$router->forAccessTokens();
}, ['prefix' => $prefix . '/oauth', 'middleware' => 'passport-guard']);
// token 作用域
Passport::tokensCan([
'admin' => 'admin',
'customer' => 'customer'
]);
// access_token 過期時間
Passport::tokensExpireIn(Carbon::now()->addDays(15));
// refreshTokens 過期時間
Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
}
此時還需修改 config/auth.php, 修改為如下程式碼
'guards' => [
·
·
·
'api' => [
'driver' => 'passport',
'provider' => 'users',
'hash' => false,
],
'admin' => [
'driver' => 'passport',
'provider' => 'admins',
],
'customer' => [
'driver' => 'passport',
'provider' => 'customers',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Admin::class,
],
'customers' => [
'driver' => 'eloquent',
'model' => App\Customer::class,
],
],
建立 Model
php artisan make:model Admin
php artisan make:model Customer
分別修改為如下程式碼
Admin.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class Admin extends Authenticatable
{
use HasApiTokens;
/**
* Passport 多認證欄位
*/
public function findForPassport($username)
{
return self::orWhere('email', $username)->orWhere('username', $username)->first();
}
}
Customer.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class Customer extends Authenticatable
{
use HasApiTokens;
/**
* Passport 多認證欄位
*/
public function findForPassport($username)
{
return self::orWhere('email', $username)->orWhere('username', $username)->first();
}
}
建立控制器
php artisan make:controller Admin/AuthController
php artisan make:controller Admin/AdminsController
php artisan make:controller Customer/AuthController
php artisan make:controller Customer/CustomersController
安裝 guzzle 擴充套件包
composer require guzzlehttp/guzzle
在 app/Http/Controllers/Admin 下新建 Traits/TokenTrait,新增如下程式碼
<?php
namespace App\Http\Controllers\Admin\Traits;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
trait TokenTrait
{
public function authenticate()
{
$client = new Client();
try {
// 請求本地的 passport token
$url = request()->root() . '/admin/oauth/token';
$password_client = \DB::table('oauth_clients')->where('name', 'passport-admin')->first();
$params = [
'grant_type' => 'password', // 認證型別 passport
'client_id' => $password_client->id,
'client_secret' => $password_client->secret,
'scope' => 'admin', // 設定 token 作用域
'username' => request('username'),
'password' => request('password'),
];
$respond = $client->request('POST', $url, ['form_params' => $params]);
} catch (RequestException $exception) {
abort(401, '系統異常');
}
if ($respond->getStatusCode() !== 401) {
return json_decode($respond->getBody()->getContents(), true);
}
abort(401, '賬號或密碼錯誤');
}
public function getRefreshToken()
{
$client = new Client();
try {
// 請求本地的 passport token
$url = request()->root() . '/admin/oauth/token';
$password_client = \DB::table('oauth_clients')->where('name', 'passport-admin')->first();
$params = [
'grant_type' => 'refresh_token',// 認證型別 refresh_token
'client_id' => $password_client->id,
'client_secret' => $password_client->secret,
'scope' => 'admin', // 設定 token 作用域
'refresh_token' => request('refresh_token')
];
$respond = $client->request('POST', $url, ['form_params' => $params]);
} catch (RequestException $exception) {
abort(401, '系統異常');
}
if ($respond->getStatusCode() !== 401) {
return json_decode($respond->getBody()->getContents(), true);
}
abort(401, 'refresh token 錯誤');
}
}
在 app/Http/Controllers/Customer 下新建 Traits/TokenTrait,新增如下程式碼
namespace App\Http\Controllers\Customer\Traits;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
trait TokenTrait
{
public function authenticate()
{
$client = new Client();
try {
$url = request()->root() . '/customer/oauth/token';
$password_client = \DB::table('oauth_clients')->where('name', 'passport-customer')->first();
$params = [
'grant_type' => 'password', // 認證型別 passport
'client_id' => $password_client->id,
'client_secret' => $password_client->secret,
'scope' => 'customer', // 設定 token 作用域
'username' => request('username'),
'password' => request('password'),
];
$respond = $client->request('POST', $url, ['form_params' => $params]);
} catch (RequestException $exception) {
abort(401, '系統異常');
}
if ($respond->getStatusCode() !== 401) {
return json_decode($respond->getBody()->getContents(), true);
}
abort(401, '賬號或密碼錯誤');
}
public function getRefreshToken()
{
$client = new Client();
try {
// 請求本地的 passport token
$url = request()->root() . '/customer/oauth/token';
$password_client = \DB::table('oauth_clients')->where('name', 'passport-customer')->first();
$params = [
'grant_type' => 'refresh_token',// 認證型別 refresh_token
'client_id' => $password_client->id,
'client_secret' => $password_client->secret,
'scope' => 'customer', // 設定 token 作用域
'refresh_token' => request('refresh_token')
];
$respond = $client->request('POST', $url, ['form_params' => $params]);
} catch (RequestException $exception) {
abort(401, '系統異常');
}
if ($respond->getStatusCode() !== 401) {
return json_decode($respond->getBody()->getContents(), true);
}
abort(401, 'refresh token 錯誤');
}
}
備註
其實此處請求的時候是有點問題的,用 guzzle 請求時如果不正確的引數是會返回 http 401 狀態碼以及報錯,然後 guzzle 如果不是 http 200 的返回,都是會丟擲異常的
所以此處丟擲的異常更準確的是 賬號密碼或 refresh_token 錯誤,最下面的 abort() 也是不會執行的。
在 app/Http/Controllers/Admin 下新建 Controller.php,新增如下程式碼
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller as BaseController;
class Controller extends BaseController
{
}
在 app/Http/Controllers/Customer 下新建 Controller.php,新增如下程式碼
<?php
namespace App.ttp.ontrollers.ustomer;
use App.ttp.ontrollers.ontroller as BaseController;
class Controller extends BaseController
{
}
修改 app/Http/Controller/Admin/AuthController.php 為如下程式碼
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
use App\Admin;
use App\Http\Controllers\Admin\Traits\TokenTrait;
use Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
use TokenTrait;
public function login(Request $request)
{
// 根據使用者名稱或者郵箱登入
$admin = Admin::orWhere('username', $request->username)
->orwhere('email', $request->username)
->firstOrFail();
// 檢驗密碼是否正確,錯誤返回 401 和報錯資訊
if (!Hash::check($request->password, $admin->password)) {
return response()->json([
'message' => '使用者名稱或密碼錯誤'
], 401);
}
$token = $this->authenticate();
return response()->json($token);
}
public function refresh()
{
// 獲取 token
$token = $this->getRefreshToken();
return response()->json($token);
}
public function logout()
{
if (Auth::guard('admin')->check()) {
Auth::guard('admin')->user()->token()->delete();
}
return response()->noContent();
}
}
修改 app/Http/Controller/Customer/AuthController.php 為如下程式碼
<?php
namespace App\Http\Controllers\Customer;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Customer;
use App\Http\Controllers\Customer\Traits\TokenTrait;
use Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
use TokenTrait;
public function login(Request $request)
{
// 根據使用者名稱或者郵箱登入
$customer = Customer::orWhere('username', $request->username)
->orwhere('email', $request->username)
->firstOrFail();
// 檢驗密碼是否正確,錯誤返回 401 和報錯資訊
if (!Hash::check($request->password, $customer->password)) {
return response()->json([
'message' => '使用者名稱或密碼錯誤'
], 401);
}
$token = $this->authenticate();
return response()->json($token);
}
public function refresh()
{
// 獲取 token
$token = $this->getRefreshToken();
return response()->json($token);
}
public function logout()
{
if (Auth::guard('customer')->check()) {
Auth::guard('customer')->user()->token()->delete();
}
return response()->noContent();
}
}
好了,激動人心的時刻到了!
開啟 postman 測試, 我分別建立了 6 個請求,具體看 url 和引數應該能明白
ok,按照預期的該返回的返回,不通過的也沒通過
接下來我們用這些 token 獲取使用者資訊
圖 5 為我用 admin1 的 token 請求 customer 的介面
圖 6 為我用 customer1 的 token 請求 admin 的介面
都是無效的。
接下來驗證重新整理 token
admin1 的 refresh_token
再請求一下
customer1 的 refersh_token
ok, 完工!
總結
-
雖然 refresh_token 不能重新刷出來,但是之前沒過期的 access_token 其實依然會有效
-
個人覺得這個並不如 dingoapi + jwt (也就是第三本 api 的教程)好用,本文只說明怎麼使用 passport 進行多端驗證。像丟擲異常,沒有自定義返回碼等還有一大堆未完善的東西。
參考資料
部落格:Laravel5.5+passport 放棄 dingo 開發 API 實戰,讓 API 開發更省心 重點感謝
部落格:Laravel Passport API 認證使用小結
部落格:Laravel Passport 多表使用者認證踩坑
部落格:Laravel 5.5 使用 Passport 實現 Auth 認證
本作品採用《CC 協議》,轉載必須註明作者和本文連結