Laravel – API – Oauth2 架設
安裝認證
php artisan make:auth php artisan migrate
接著透過 Laravel 生出來的使用者功能,前往 http://domain/register 註冊兩組帳號作為交互運用練習:
- 一組是開發者帳號,用這個帳號來建立一個應用程式,也就是 client 端,例如這個應用程式是 Mobile APP 會需要連接的 Oauth。
- 一組帳號是使用者帳號,會使用開發者所建立的 Mobile APP。開發者的 Client 端,會向使用者索取授權與 token。
安裝 Passport
composer require laravel/passport
下 artisan 指令,建立資料表。
php artisan migrate
接著下 artisan 指令,預設會建立「個人權限 personal access」與「密碼許可 password grant」
php artisan passport:install
在 App\User 添加 Laravel\Passport\HasApiTokens 特徵,如果你的 model 有需要使用 api token 的輔助方法,都可以加入這項特徵
<?php namespace App; use Laravel\Passport\HasApiTokens; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use HasApiTokens, Notifiable; }
接著將路由 Passport::routes() 添加到 AuthServiceProvider 的 boot() 方法
<?php namespace App\Providers; use Laravel\Passport\Passport; use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; 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(); Passport::routes(); } }
最後修改 config/auth.php 的 api driver 為 passport
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], ],
部屬 Passport
建立鑰匙
php artisan passport:keys
設定
令牌壽命
在 AuthServiceProvider 指定令牌過期時間 tokensExpireIn() 與重取令牌過期時間 refreshTokensExpireIn()
/** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); Passport::routes(); Passport::tokensExpireIn(now()->addDays(15)); Passport::refreshTokensExpireIn(now()->addDays(30)); }
發行訪問令牌
管理客戶端
首先,當開發者建立應用程式時,會需要與你的應用程式API交互溝通;而你的應用程式 API ,會需要透過建立一個稱作 “client” 的方式,來註冊開發者的應用程式。
通常,這包括由應用程式提供的「名稱 name」以及使用者批准請求授權後,應用程式所需「返回的 網址 url」。
JSON API
這四種 axios 方法請記得先登入開發者帳號,可以讓開發者對自己開發的 client 做管理。但目前我們僅要用剛剛建立的開發者登入,透過 POST 新增客戶端,記得 GET 之外的方法要加入 CSRF token
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> <meta name="csrf-token" content="{{ csrf_token() }}">
若是 jQuery 加入方式。axios 請改成特定方式
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } });
- GET /oauth/clients 查詢自己擁有的客戶端
axios.get('/oauth/clients') .then(response => { console.log(response.data); });
- POST /oauth/clients 新增客戶端
const data = { name: 'Client Name', redirect: 'http://example.com/callback' }; axios.post('/oauth/clients', data) .then(response => { console.log(response.data); }) .catch (response => { // List errors on response... });
- PUT /oauth/clients 修改自己的某個客戶端
const data = { name: 'New Client Name', redirect: 'http://example.com/callback' }; axios.put('/oauth/clients/' + clientId, data) .then(response => { console.log(response.data); }) .catch (response => { // List errors on response... });
- DELETE /oauth/clients 刪除自己的某個客戶端
axios.delete('/oauth/clients/' + clientId) .then(response => { // });
請求令牌
當開發者建立一個用戶端以後,這個用戶端 Client 要向使用者請求令牌。下方的用戶端會帶領使用者,導向到授權頁面,使用者允許授權給用戶端,那會返回指定的 redirect_uri:http://example.com/callback 以取得授權碼。
Route::get('/redirect', function () { $query = http_build_query([ 'client_id' => 'client-id', 'redirect_uri' => 'http://example.com/callback', 'response_type' => 'code', 'scope' => '', ]); return redirect('http://your-app.com/oauth/authorize?'.$query); });
另外,我們可以下 artisan 建立 view 來讓我們自行修改版面
php artisan vendor:publish --tag=passport-views
授權碼轉換為訪問令牌
開發者的客戶端 client 取得了授權碼 code,我們要 POST /oauth/token 來轉換為訪問令牌。client_id 與 client_secret 我們可以從資料表中看到,或是發 GET 到 oauth/clients 找到屬於開發者剛建立的 client。
use Illuminate\Http\Request; Route::get('/callback', function (Request $request) { $http = new GuzzleHttp\Client; $response = $http->post('http://your-app.com/oauth/token', [ 'form_params' => [ 'grant_type' => 'authorization_code', 'client_id' => 'client-id', 'client_secret' => 'client-secret', 'redirect_uri' => 'http://example.com/callback', 'code' => $request->code, ], ]); return json_decode((string) $response->getBody(), true); });
返回參數包含訪問令牌 access_token, 重取令牌 refresh_token, 令牌過期時間 expires_in。如果要重取令牌可以這樣
$http = new GuzzleHttp\Client; $response = $http->post('http://your-app.com/oauth/token', [ 'form_params' => [ 'grant_type' => 'refresh_token', 'refresh_token' => 'the-refresh-token', 'client_id' => 'client-id', 'client_secret' => 'client-secret', 'scope' => '', ], ]); return json_decode((string) $response->getBody(), true);
密碼許可令牌 Password Grant Tokens
若之前已經使用 php artisan passport:client 那就不需要下以下指令
php artisan passport:client --password
請求令牌
另外透過這個方法,可以用 password_client = 1 的系統身分,來取得會員自己的 access token 與 refresh token。與上述 授權碼轉換為訪問令牌 的差異在於參數 grant_type 使用 password,改用會員的密碼拋送,取得的結果是一樣的。
- client_id 與 client_secret:注意填入的是資料表 oauth_clients 中 password_client = 1 的編號
- username 與 password:注意填入的是使用者的 email 與 password(未加密的值不是資料表欄位的值)
$http = new GuzzleHttp\Client; $response = $http->post('http://your-app.com/oauth/token', [ 'form_params' => [ 'grant_type' => 'password', 'client_id' => 'client-id', 'client_secret' => 'client-secret', 'username' => 'taylor@laravel.com', 'password' => 'my-password', 'scope' => '', ], ]); return json_decode((string) $response->getBody(), true);
請求所有範圍
在 scope 添加萬用字元「*」。注意,這樣的指定只能在 /oauth/token 方法,且指定 grant_type 為 password 或 client_credentials。
$response = $http->post('http://your-app.com/oauth/token', [ 'form_params' => [ 'grant_type' => 'password', 'client_id' => 'client-id', 'client_secret' => 'client-secret', 'username' => 'taylor@laravel.com', 'password' => 'my-password', 'scope' => '*', ], ]);
隱藏式許可令牌
(待寫)
客戶端許可證授權令牌
(待寫)
個人權限令牌
(待寫)
保護路由
通過中介層
Passport 只要在路由配合使用 auth:api 就可以包含驗證權限令牌 access token 了
Route::get('/user', function () { // })->middleware('auth:api');
傳遞訪問令牌
這個很常用,當我們透過如 JavaScript 發送請求到 Server 必須要夾帶 Access Token,注意 Authorization 參數值 Bearer 後方要保留一個空白,範例如下
const accessToken = '取得的 access token'; // 設置道 header $.ajaxSetup({ headers: { 'Accept' : 'application/json', 'Authorization' : 'Bearer ' + accessToken } }); // 開始做任何的請求 $.get('/api/user', function (data){ console.log(data) });
若用 php 的話
$accessToken = '取得的 access token'; $client = new GuzzleHttp\Client; $response = $client->request('GET', '/api/user', [ 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer '.$accessToken, ], ]); print_r(json_decode((string) $response->getBody(), true));
而 Laravel 的路由 /api/user 也務必使用上述的中介層 middleware(‘auth:api’)。
令牌範圍
定義範圍
在 AuthServiceProvider 的方法 boot() 添加 Passport::tokensCan(),value 的部分請填上給使用者看得好懂語言
use Laravel\Passport\Passport; Passport::tokensCan([ 'place-orders' => 'Place orders', 'check-status' => 'Check order status', ]);
預設範圍
這種方式會在當我們在 redirect 需要 scope 參數,設定空白的時候的預設值。
use Laravel\Passport\Passport; Passport::setDefaultScope([ 'check-status', 'place-orders', ]);
分配範圍到令牌
參數 scope 值使用上述定義的名稱,要用空白分開,例如
Route::get('/redirect', function () { $query = http_build_query([ 'client_id' => 'client-id', 'redirect_uri' => 'http://example.com/callback', 'response_type' => 'code', 'scope' => 'place-orders check-status', ]); return redirect('http://your-app.com/oauth/authorize?'.$query); });
檢查範圍
在 app/Http/Kernel.php 的屬性 $routeMiddleware 添加兩個中介層
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class, 'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
若要檢查符合權限範圍,中介層的 scope 或 scopes 之間的逗號不可以有空白
Route::get('/orders', function () { // 符合全部 })->middleware('scopes:check-status,place-orders');
或
Route::get('/orders', function () { // 符合其中一項 })->middleware('scope:check-status,place-orders');
或是手動判斷
use Illuminate\Http\Request; Route::get('/orders', function (Request $request) { if ($request->user()->tokenCan('place-orders')) { // } });