Laravel

Laravel Passport OAuth 2.0

配套视频地址:https://www.bilibili.com/video/av74879198


composer require laravel/passport
php artisan migrate // 创建表来存储客户端和 access_token
php artisan passport:install // 生成加密 access_token 的 key、密码授权客户端、个人访问客户端
Laravel\Passport\HasApiTokens Trait 添加到 App\User 模型中 // 提供一些辅助函数检查已认证用户的令牌和使用范围
在 AuthServiceProvider 的 boot 方法中调用 Passport::routes 函数 // 访问令牌并撤销访问令牌路由,客户端和个人访问令牌相关路由
config/auth.php 中 api 的 driver 选项改为 passport

自定义 passport migration

php artisan vendor:publish --tag=passport-migrations

生成加密 access_token 的 key

php artisan passport:keys

AuthServiceProvider 中指定 passport key 加载路径

Passport::loadKeysFrom('/secret-keys/oauth');

PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"

PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"

配置

过期时间

Passport::tokensExpireIn(now()->addDays(15)); // access_token
Passport::refreshTokensExpireIn(now()->addDays(30));// refresh_token
Passport::personalAccessTokensExpireIn(now()->addMonths(6)); // personal access_token

重写模型

use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\PersonalAccessClient;
use App\Models\Passport\Token;

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

    Passport::routes();

    Passport::useTokenModel(Token::class);
    Passport::useClientModel(Client::class);
    Passport::useAuthCodeModel(AuthCode::class);
    Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
}

管理客户端

命令行创建客户端
php artisan passport:client
// 设置回调地址白名单的格式:http://example.com/callback,http://examplefoo.com/callback (逗号隔开)
api 管理客户端
axios.get('/oauth/clients')
    .then(response => {
        console.log(response.data);
    });
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...
    });
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...
    });
axios.delete('/oauth/clients/' + clientId)
    .then(response => {
        //
    });

授权码模式

请求 token

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'code',
        'scope' => '',
        'state' => $state,
    ]);

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});
自定义用户授权页面
php artisan vendor:publish --tag=passport-views
跳过用户授权页面
<?php

namespace App\Models\Passport;

use Laravel\Passport\Client as BaseClient;

class Client extends BaseClient
{
    public function skipsAuthorization()
    {
        return $this->firstParty();
    }
}
获取 access_token
Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class
    );

    $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);
});
刷新令牌
$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);

密码模式

php artisan passport:client --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);
自定义密码验证和 username 字段
public function validateForPassportPasswordGrant($password)
{
    return Hash::check($password, $this->password);
}

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

隐式模式

Passport::enableImplicitGrant();
Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'token',
        'scope' => '',
        'state' => $state,
    ]);

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});

客户端模式

php artisan passport:client --client
use Laravel\Passport\Http\Middleware\CheckClientCredentials;

protected $routeMiddleware = [
    'client' => CheckClientCredentials::class,
];
Route::get('/orders', function (Request $request) {
    ...
})->middleware('client');
Route::get('/orders', function (Request $request) {
    ...
})->middleware('client:check-status,your-scope');
$guzzle = new GuzzleHttp\Client;

$response = $guzzle->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'client_credentials',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'scope' => 'your-scope',
    ],
]);

return json_decode((string) $response->getBody(), true)['access_token'];

使用 access_token

$response = $client->request('GET', '/api/user', [
    'headers' => [
        'Accept' => 'application/json',
        'Authorization' => 'Bearer '.$accessToken,
    ],
]);

玩转 scope

# AuthServiceProvider
use Laravel\Passport\Passport;

Passport::tokensCan([
    'place-orders' => 'Place orders',
    'check-status' => 'Check order status',
]);

Passport::setDefaultScope([
    'check-status',
    'place-orders',
]);
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', // 传递 scope 格式
    ]);

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});
检验 scope
# app/Http/Kernel.php 中 $routeMiddleware
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
Route::get('/orders', function () {
    // Access token has both "check-status" and "place-orders" scopes...
})->middleware('scopes:check-status,place-orders');
Route::get('/orders', function () {
    // Access token has either "check-status" or "place-orders" scope...
})->middleware('scope:check-status,place-orders');

就算含有访问令牌验证的请求已经通过应用程序的验证,你仍然可以使用当前授权 User 实例上的 tokenCan 方法来验证令牌是否拥有指定的作用域

use Illuminate\Http\Request;

Route::get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('place-orders')) {
        //
    }
});

scopeIds 方法将返回所有已定义 ID / 名称的数组:

Laravel\Passport\Passport::scopeIds();

scopes 方法将返回一个包含所有已定义作用域数组的 Laravel\Passport\Scope 实例:

Laravel\Passport\Passport::scopes();

scopesFor 方法将返回与给定 ID / 名称匹配的 Laravel\Passport\Scope 实例数组:

Laravel\Passport\Passport::scopesFor(['place-orders', 'check-status']);

你可以使用 hasScope 方法确定是否已定义给定作用域:

Laravel\Passport\Passport::hasScope('place-orders');

事件

protected $listen = [
    'Laravel\Passport\Events\AccessTokenCreated' => [
        'App\Listeners\RevokeOldTokens',
    ],

    'Laravel\Passport\Events\RefreshTokenCreated' => [
        'App\Listeners\PruneOldTokens',
    ],
];

javascript 中使用 api

'web' => [
    // Other middleware...
    \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
// 注意:你应该确保在您的中间件堆栈中 CreateFreshApiToken 中间件之前列出了 EncryptCookies 中间件。
axios.get('/api/user')
    .then(response => {
        console.log(response.data);
    });
自定义 Cookie 名称
public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::cookie('custom_name');
}

测试

actingAs 方法可以指定当前已认证用户及其作用域 。
use App\User;
use Laravel\Passport\Passport;

public function testServerCreation()
{
    Passport::actingAs(
        factory(User::class)->create(),
        ['create-servers']
    );

    $response = $this->post('/api/create-server');

    $response->assertStatus(201);
}
actingAsClient 方法可以指定当前已认证客户端及其作用域 。
use Laravel\Passport\Client;
use Laravel\Passport\Passport;

public function testGetOrders()
{
    Passport::actingAsClient(
        factory(Client::class)->create(),
        ['check-status']
    );

    $response = $this->get('/api/orders');

    $response->assertStatus(200);
}

发表评论