Laravel Passport
- 简介
- 安装
- 配置
- 颁发访问令牌
- 带有 PKCE 的授权码许可
- 密码许可令牌
- 隐式许可令牌
- 客户端凭据许可令牌
- 个人访问令牌
- 保护路由
- 令牌作用域
- 使用 JavaScript 消费您的 API
- 事件
- 测试
简介
Laravel Passport 在几分钟内为您的 Laravel 应用程序提供完整的 OAuth2 服务器实现。Passport 构建于 Andy Millington 和 Simon Hamp 维护的 League OAuth2 server 之上。
本文档假定您已经熟悉 OAuth2。如果您对 OAuth2 一无所知,请考虑在继续之前熟悉 OAuth2 的通用术语和功能。
Passport 还是 Sanctum?
在开始之前,您可能希望确定您的应用程序使用 Laravel Passport 还是 Laravel Sanctum 会更好。如果您的应用程序绝对需要支持 OAuth2,那么您应该使用 Laravel Passport。
但是,如果您尝试验证单页应用程序、移动应用程序或颁发 API 令牌,则应使用 Laravel Sanctum。Laravel Sanctum 不支持 OAuth2;但是,它提供了更简单的 API 身份验证开发体验。
安装
您可以通过 install:api
Artisan 命令安装 Laravel Passport
1php artisan install:api --passport
此命令将发布并运行数据库迁移,这些迁移对于创建应用程序存储 OAuth2 客户端和访问令牌所需的表是必要的。该命令还将创建生成安全访问令牌所需的加密密钥。
此外,此命令会询问您是否希望使用 UUID 作为 Passport Client
模型的主键值,而不是自动递增的整数。
运行 install:api
命令后,将 Laravel\Passport\HasApiTokens
trait 添加到您的 App\Models\User
模型。此 trait 将为您的模型提供一些辅助方法,使您可以检查经过身份验证的用户的令牌和作用域
1<?php 2 3namespace App\Models; 4 5use Illuminate\Database\Eloquent\Factories\HasFactory; 6use Illuminate\Foundation\Auth\User as Authenticatable; 7use Illuminate\Notifications\Notifiable; 8use Laravel\Passport\HasApiTokens; 9 10class User extends Authenticatable11{12 use HasApiTokens, HasFactory, Notifiable;13}
最后,在应用程序的 config/auth.php
配置文件中,您应该定义一个 api
身份验证守卫,并将 driver
选项设置为 passport
。这将指示您的应用程序在验证传入的 API 请求时使用 Passport 的 TokenGuard
1'guards' => [ 2 'web' => [ 3 'driver' => 'session', 4 'provider' => 'users', 5 ], 6 7 'api' => [ 8 'driver' => 'passport', 9 'provider' => 'users',10 ],11],
部署 Passport
首次将 Passport 部署到应用程序的服务器时,您可能需要运行 passport:keys
命令。此命令生成 Passport 生成访问令牌所需的加密密钥。生成的密钥通常不保存在源代码控制中
1php artisan passport:keys
如有必要,您可以定义应从中加载 Passport 密钥的路径。您可以使用 Passport::loadKeysFrom
方法来完成此操作。通常,此方法应从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');7}
从环境变量加载密钥
或者,您可以使用 vendor:publish
Artisan 命令发布 Passport 的配置文件
1php artisan vendor:publish --tag=passport-config
发布配置文件后,您可以通过将应用程序的加密密钥定义为环境变量来加载它们
1PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----2<private key here>3-----END RSA PRIVATE KEY-----"4 5PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----6<public key here>7-----END PUBLIC KEY-----"
升级 Passport
升级到新主要版本的 Passport 时,请务必仔细查看升级指南。
配置
客户端密钥哈希
如果您希望在数据库中存储客户端密钥时对其进行哈希处理,则应在 App\Providers\AppServiceProvider
类的 boot
方法中调用 Passport::hashClientSecrets
方法
1use Laravel\Passport\Passport;2 3Passport::hashClientSecrets();
启用后,您的所有客户端密钥将仅在创建后立即向用户显示。由于纯文本客户端密钥值永远不会存储在数据库中,因此如果密钥值丢失,则无法恢复密钥值。
令牌有效期
默认情况下,Passport 颁发有效期为一年的长期访问令牌。如果您想配置更长/更短的令牌有效期,可以使用 tokensExpireIn
、refreshTokensExpireIn
和 personalAccessTokensExpireIn
方法。这些方法应从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Passport::tokensExpireIn(now()->addDays(15));7 Passport::refreshTokensExpireIn(now()->addDays(30));8 Passport::personalAccessTokensExpireIn(now()->addMonths(6));9}
Passport 数据库表上的 expires_at
列是只读的,仅用于显示目的。颁发令牌时,Passport 会将过期信息存储在签名和加密的令牌中。如果您需要使令牌无效,则应撤销它。
覆盖默认模型
您可以自由扩展 Passport 内部使用的模型,方法是定义自己的模型并扩展相应的 Passport 模型
1use Laravel\Passport\Client as PassportClient;2 3class Client extends PassportClient4{5 // ...6}
定义模型后,您可以指示 Passport 通过 Laravel\Passport\Passport
类使用您的自定义模型。通常,您应该在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中告知 Passport 您的自定义模型
1use App\Models\Passport\AuthCode; 2use App\Models\Passport\Client; 3use App\Models\Passport\PersonalAccessClient; 4use App\Models\Passport\RefreshToken; 5use App\Models\Passport\Token; 6 7/** 8 * Bootstrap any application services. 9 */10public function boot(): void11{12 Passport::useTokenModel(Token::class);13 Passport::useRefreshTokenModel(RefreshToken::class);14 Passport::useAuthCodeModel(AuthCode::class);15 Passport::useClientModel(Client::class);16 Passport::usePersonalAccessClientModel(PersonalAccessClient::class);17}
覆盖路由
有时您可能希望自定义 Passport 定义的路由。为了实现这一点,您首先需要通过将 Passport::ignoreRoutes
添加到应用程序的 AppServiceProvider
的 register
方法来忽略 Passport 注册的路由
1use Laravel\Passport\Passport;2 3/**4 * Register any application services.5 */6public function register(): void7{8 Passport::ignoreRoutes();9}
然后,您可以将 Passport 在 其路由文件 中定义的路由复制到应用程序的 routes/web.php
文件中,并根据您的喜好进行修改
1Route::group([2 'as' => 'passport.',3 'prefix' => config('passport.path', 'oauth'),4 'namespace' => '\Laravel\Passport\Http\Controllers',5], function () {6 // Passport routes...7});
颁发访问令牌
通过授权码使用 OAuth2 是大多数开发人员熟悉的 OAuth2 使用方式。使用授权码时,客户端应用程序会将用户重定向到您的服务器,用户将在服务器上批准或拒绝向客户端颁发访问令牌的请求。
管理客户端
首先,构建需要与您的应用程序 API 交互的应用程序的开发人员需要通过创建“客户端”在您的应用程序中注册他们的应用程序。通常,这包括提供他们的应用程序的名称和 URL,您的应用程序可以在用户批准他们的授权请求后重定向到该 URL。
passport:client
命令
创建客户端的最简单方法是使用 passport:client
Artisan 命令。此命令可用于创建您自己的客户端来测试 OAuth2 功能。当您运行 client
命令时,Passport 会提示您提供有关客户端的更多信息,并将为您提供客户端 ID 和密钥
1php artisan passport:client
重定向 URL
如果您想允许客户端使用多个重定向 URL,您可以在 passport:client
命令提示您输入 URL 时使用逗号分隔的列表指定它们。任何包含逗号的 URL 都应进行 URL 编码
1http://example.com/callback,http://examplefoo.com/callback
JSON API
由于您的应用程序用户将无法使用 client
命令,因此 Passport 提供了 JSON API,您可以使用它来创建客户端。这为您省去了手动编写控制器来创建、更新和删除客户端的麻烦。
但是,您需要将 Passport 的 JSON API 与您自己的前端配对,以便为用户提供仪表板来管理他们的客户端。下面,我们将回顾所有用于管理客户端的 API 端点。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。
JSON API 由 web
和 auth
中间件保护;因此,它只能从您自己的应用程序中调用。它无法从外部来源调用。
GET /oauth/clients
此路由返回经过身份验证的用户的全部客户端。这主要用于列出用户的所有客户端,以便他们可以编辑或删除它们
1axios.get('/oauth/clients')2 .then(response => {3 console.log(response.data);4 });
POST /oauth/clients
此路由用于创建新客户端。它需要两个数据:客户端的 name
和 redirect
URL。redirect
URL 是在用户批准或拒绝授权请求后将被重定向到的位置。
创建客户端时,将为其颁发客户端 ID 和客户端密钥。在从您的应用程序请求访问令牌时将使用这些值。客户端创建路由将返回新的客户端实例
1const data = { 2 name: 'Client Name', 3 redirect: 'http://example.com/callback' 4}; 5 6axios.post('/oauth/clients', data) 7 .then(response => { 8 console.log(response.data); 9 })10 .catch (response => {11 // List errors on response...12 });
PUT /oauth/clients/{client-id}
此路由用于更新客户端。它需要两个数据:客户端的 name
和 redirect
URL。redirect
URL 是在用户批准或拒绝授权请求后将被重定向到的位置。该路由将返回更新后的客户端实例
1const data = { 2 name: 'New Client Name', 3 redirect: 'http://example.com/callback' 4}; 5 6axios.put('/oauth/clients/' + clientId, data) 7 .then(response => { 8 console.log(response.data); 9 })10 .catch (response => {11 // List errors on response...12 });
DELETE /oauth/clients/{client-id}
此路由用于删除客户端
1axios.delete('/oauth/clients/' + clientId)2 .then(response => {3 // ...4 });
请求令牌
重定向以进行授权
创建客户端后,开发人员可以使用其客户端 ID 和密钥从您的应用程序请求授权码和访问令牌。首先,消费应用程序应像这样向您的应用程序的 /oauth/authorize
路由发出重定向请求
1use Illuminate\Http\Request; 2use Illuminate\Support\Str; 3 4Route::get('/redirect', function (Request $request) { 5 $request->session()->put('state', $state = Str::random(40)); 6 7 $query = http_build_query([ 8 'client_id' => 'client-id', 9 'redirect_uri' => 'http://third-party-app.com/callback',10 'response_type' => 'code',11 'scope' => '',12 'state' => $state,13 // 'prompt' => '', // "none", "consent", or "login"14 ]);15 16 return redirect('http://passport-app.test/oauth/authorize?'.$query);17});
prompt
参数可用于指定 Passport 应用程序的身份验证行为。
如果 prompt
值为 none
,则如果用户尚未通过 Passport 应用程序进行身份验证,Passport 将始终抛出身份验证错误。如果值为 consent
,则 Passport 将始终显示授权批准屏幕,即使之前已向消费应用程序授予了所有作用域。当值为 login
时,Passport 应用程序将始终提示用户重新登录到应用程序,即使他们已经有现有的会话。
如果未提供 prompt
值,则仅当用户之前未授权消费应用程序访问请求的作用域时,才会提示用户进行授权。
请记住,/oauth/authorize
路由已由 Passport 定义。您无需手动定义此路由。
批准请求
收到授权请求时,Passport 将根据 prompt
参数的值(如果存在)自动响应,并可能向用户显示一个模板,允许他们批准或拒绝授权请求。如果他们批准请求,他们将被重定向回消费应用程序指定的 redirect_uri
。redirect_uri
必须与创建客户端时指定的 redirect
URL 匹配。
如果您想自定义授权批准屏幕,可以使用 vendor:publish
Artisan 命令发布 Passport 的视图。发布的视图将放置在 resources/views/vendor/passport
目录中
1php artisan vendor:publish --tag=passport-views
有时您可能希望跳过授权提示,例如在授权第一方客户端时。您可以通过扩展 Client
模型并定义 skipsAuthorization
方法来实现此目的。如果 skipsAuthorization
返回 true
,则客户端将被批准,用户将立即重定向回 redirect_uri
,除非消费应用程序在重定向进行授权时显式设置了 prompt
参数
1<?php 2 3namespace App\Models\Passport; 4 5use Laravel\Passport\Client as BaseClient; 6 7class Client extends BaseClient 8{ 9 /**10 * Determine if the client should skip the authorization prompt.11 */12 public function skipsAuthorization(): bool13 {14 return $this->firstParty();15 }16}
将授权码转换为访问令牌
如果用户批准授权请求,他们将被重定向回消费应用程序。消费者应首先根据重定向之前存储的值验证 state
参数。如果 state 参数匹配,则消费者应向您的应用程序发出 POST
请求以请求访问令牌。该请求应包括您的应用程序在用户批准授权请求时颁发的授权码
1use Illuminate\Http\Request; 2use Illuminate\Support\Facades\Http; 3 4Route::get('/callback', function (Request $request) { 5 $state = $request->session()->pull('state'); 6 7 throw_unless( 8 strlen($state) > 0 && $state === $request->state, 9 InvalidArgumentException::class,10 'Invalid state value.'11 );12 13 $response = Http::asForm()->post('http://passport-app.test/oauth/token', [14 'grant_type' => 'authorization_code',15 'client_id' => 'client-id',16 'client_secret' => 'client-secret',17 'redirect_uri' => 'http://third-party-app.com/callback',18 'code' => $request->code,19 ]);20 21 return $response->json();22});
此 /oauth/token
路由将返回一个 JSON 响应,其中包含 access_token
、refresh_token
和 expires_in
属性。expires_in
属性包含访问令牌过期前的秒数。
与 /oauth/authorize
路由一样,/oauth/token
路由由 Passport 为您定义。无需手动定义此路由。
JSON API
Passport 还包括一个 JSON API,用于管理授权的访问令牌。您可以将其与您自己的前端配对,为用户提供一个仪表板来管理访问令牌。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。JSON API 由 web
和 auth
中间件保护;因此,它只能从您自己的应用程序中调用。
GET /oauth/tokens
此路由返回经过身份验证的用户创建的所有授权访问令牌。这主要用于列出用户的所有令牌,以便他们可以撤销它们
1axios.get('/oauth/tokens')2 .then(response => {3 console.log(response.data);4 });
DELETE /oauth/tokens/{token-id}
此路由可用于撤销授权的访问令牌及其相关的刷新令牌
1axios.delete('/oauth/tokens/' + tokenId);
刷新令牌
如果您的应用程序颁发有效期较短的访问令牌,则用户需要通过访问令牌颁发时提供给他们的刷新令牌来刷新其访问令牌
1use Illuminate\Support\Facades\Http; 2 3$response = Http::asForm()->post('http://passport-app.test/oauth/token', [ 4 'grant_type' => 'refresh_token', 5 'refresh_token' => 'the-refresh-token', 6 'client_id' => 'client-id', 7 'client_secret' => 'client-secret', 8 'scope' => '', 9]);10 11return $response->json();
此 /oauth/token
路由将返回一个 JSON 响应,其中包含 access_token
、refresh_token
和 expires_in
属性。expires_in
属性包含访问令牌过期前的秒数。
撤销令牌
您可以使用 Laravel\Passport\TokenRepository
上的 revokeAccessToken
方法撤销令牌。您可以使用 Laravel\Passport\RefreshTokenRepository
上的 revokeRefreshTokensByAccessTokenId
方法撤销令牌的刷新令牌。可以使用 Laravel 的服务容器解析这些类
1use Laravel\Passport\TokenRepository; 2use Laravel\Passport\RefreshTokenRepository; 3 4$tokenRepository = app(TokenRepository::class); 5$refreshTokenRepository = app(RefreshTokenRepository::class); 6 7// Revoke an access token... 8$tokenRepository->revokeAccessToken($tokenId); 9 10// Revoke all of the token's refresh tokens...11$refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId);
清除令牌
当令牌被撤销或过期时,您可能希望从数据库中清除它们。Passport 包含的 passport:purge
Artisan 命令可以为您执行此操作
1# Purge revoked and expired tokens and auth codes... 2php artisan passport:purge 3 4# Only purge tokens expired for more than 6 hours... 5php artisan passport:purge --hours=6 6 7# Only purge revoked tokens and auth codes... 8php artisan passport:purge --revoked 9 10# Only purge expired tokens and auth codes...11php artisan passport:purge --expired
您还可以在应用程序的 routes/console.php
文件中配置一个计划任务,以按计划自动修剪您的令牌
1use Illuminate\Support\Facades\Schedule;2 3Schedule::command('passport:purge')->hourly();
带有 PKCE 的授权码许可
带有“代码交换证明密钥”(PKCE)的授权码许可是一种安全的方式,用于验证单页应用程序或本机应用程序以访问您的 API。当您无法保证客户端密钥将被保密存储时,或者为了减轻授权码被攻击者拦截的威胁时,应使用此许可。当交换授权码以获取访问令牌时,“代码验证器”和“代码质询”的组合取代了客户端密钥。
创建客户端
在您的应用程序可以通过带有 PKCE 的授权码许可颁发令牌之前,您需要创建一个启用 PKCE 的客户端。您可以使用带有 --public
选项的 passport:client
Artisan 命令来执行此操作
1php artisan passport:client --public
请求令牌
代码验证器和代码质询
由于此授权许可不提供客户端密钥,因此开发人员需要生成代码验证器和代码质询的组合才能请求令牌。
代码验证器应为 43 到 128 个字符之间的随机字符串,其中包含字母、数字和 "-"
、"."
、"_"
、"~"
字符,如 RFC 7636 规范中所定义。
代码质询应为带有 URL 和文件名安全字符的 Base64 编码字符串。应删除尾随的 '='
字符,并且不应存在换行符、空格或其他附加字符。
1$encoded = base64_encode(hash('sha256', $code_verifier, true));2 3$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');
重定向以进行授权
创建客户端后,您可以使用客户端 ID 和生成的代码验证器和代码质询从您的应用程序请求授权码和访问令牌。首先,消费应用程序应向您的应用程序的 /oauth/authorize
路由发出重定向请求
1use Illuminate\Http\Request; 2use Illuminate\Support\Str; 3 4Route::get('/redirect', function (Request $request) { 5 $request->session()->put('state', $state = Str::random(40)); 6 7 $request->session()->put( 8 'code_verifier', $code_verifier = Str::random(128) 9 );10 11 $codeChallenge = strtr(rtrim(12 base64_encode(hash('sha256', $code_verifier, true))13 , '='), '+/', '-_');14 15 $query = http_build_query([16 'client_id' => 'client-id',17 'redirect_uri' => 'http://third-party-app.com/callback',18 'response_type' => 'code',19 'scope' => '',20 'state' => $state,21 'code_challenge' => $codeChallenge,22 'code_challenge_method' => 'S256',23 // 'prompt' => '', // "none", "consent", or "login"24 ]);25 26 return redirect('http://passport-app.test/oauth/authorize?'.$query);27});
将授权码转换为访问令牌
如果用户批准授权请求,他们将被重定向回消费应用程序。消费者应根据重定向之前存储的值验证 state
参数,就像标准授权码许可中一样。
如果 state 参数匹配,则消费者应向您的应用程序发出 POST
请求以请求访问令牌。该请求应包括您的应用程序在用户批准授权请求时颁发的授权码以及最初生成的代码验证器
1use Illuminate\Http\Request; 2use Illuminate\Support\Facades\Http; 3 4Route::get('/callback', function (Request $request) { 5 $state = $request->session()->pull('state'); 6 7 $codeVerifier = $request->session()->pull('code_verifier'); 8 9 throw_unless(10 strlen($state) > 0 && $state === $request->state,11 InvalidArgumentException::class12 );13 14 $response = Http::asForm()->post('http://passport-app.test/oauth/token', [15 'grant_type' => 'authorization_code',16 'client_id' => 'client-id',17 'redirect_uri' => 'http://third-party-app.com/callback',18 'code_verifier' => $codeVerifier,19 'code' => $request->code,20 ]);21 22 return $response->json();23});
密码许可令牌
我们不再建议使用密码许可令牌。相反,您应该选择 OAuth2 服务器当前推荐的许可类型。
OAuth2 密码许可允许您的其他第一方客户端(例如移动应用程序)使用电子邮件地址/用户名和密码获取访问令牌。这允许您安全地向您的第一方客户端颁发访问令牌,而无需用户完成整个 OAuth2 授权码重定向流程。
要启用密码许可,请在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用 enablePasswordGrant
方法
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Passport::enablePasswordGrant();7}
创建密码许可客户端
在您的应用程序可以通过密码许可颁发令牌之前,您需要创建一个密码许可客户端。您可以使用带有 --password
选项的 passport:client
Artisan 命令来执行此操作。如果您已运行 passport:install
命令,则无需运行此命令:
1php artisan passport:client --password
请求令牌
创建密码许可客户端后,您可以通过向 /oauth/token
路由发出 POST
请求并提供用户的电子邮件地址和密码来请求访问令牌。请记住,此路由已由 Passport 注册,因此无需手动定义它。如果请求成功,您将在服务器的 JSON 响应中收到 access_token
和 refresh_token
1use Illuminate\Support\Facades\Http; 2 3$response = Http::asForm()->post('http://passport-app.test/oauth/token', [ 4 'grant_type' => 'password', 5 'client_id' => 'client-id', 6 'client_secret' => 'client-secret', 8 'password' => 'my-password', 9 'scope' => '',10]);11 12return $response->json();
请记住,默认情况下访问令牌是长期有效的。但是,您可以根据需要配置您的最大访问令牌有效期。
请求所有作用域
当使用密码许可或客户端凭据许可时,您可能希望授权令牌访问您的应用程序支持的所有作用域。您可以通过请求 *
作用域来实现此目的。如果您请求 *
作用域,则令牌实例上的 can
方法将始终返回 true
。此作用域只能分配给使用 password
或 client_credentials
许可颁发的令牌
1use Illuminate\Support\Facades\Http; 2 3$response = Http::asForm()->post('http://passport-app.test/oauth/token', [ 4 'grant_type' => 'password', 5 'client_id' => 'client-id', 6 'client_secret' => 'client-secret', 8 'password' => 'my-password', 9 'scope' => '*',10]);
自定义用户提供器
如果您的应用程序使用多个身份验证用户提供器,您可以通过在通过 artisan passport:client --password
命令创建客户端时提供 --provider
选项来指定密码许可客户端使用的用户提供器。给定的提供器名称应与应用程序的 config/auth.php
配置文件中定义的有效提供器匹配。然后,您可以使用中间件保护您的路由,以确保只有来自守卫指定提供器的用户才能获得授权。
自定义用户名段
当使用密码许可进行身份验证时,Passport 将使用您的可身份验证模型的 email
属性作为“用户名”。但是,您可以通过在您的模型上定义 findForPassport
方法来自定义此行为
1<?php 2 3namespace App\Models; 4 5use Illuminate\Foundation\Auth\User as Authenticatable; 6use Illuminate\Notifications\Notifiable; 7use Laravel\Passport\HasApiTokens; 8 9class User extends Authenticatable10{11 use HasApiTokens, Notifiable;12 13 /**14 * Find the user instance for the given username.15 */16 public function findForPassport(string $username): User17 {18 return $this->where('username', $username)->first();19 }20}
自定义密码验证
当使用密码许可进行身份验证时,Passport 将使用您的模型的 password
属性来验证给定的密码。如果您的模型没有 password
属性,或者您希望自定义密码验证逻辑,您可以在您的模型上定义 validateForPassportPasswordGrant
方法
1<?php 2 3namespace App\Models; 4 5use Illuminate\Foundation\Auth\User as Authenticatable; 6use Illuminate\Notifications\Notifiable; 7use Illuminate\Support\Facades\Hash; 8use Laravel\Passport\HasApiTokens; 9 10class User extends Authenticatable11{12 use HasApiTokens, Notifiable;13 14 /**15 * Validate the password of the user for the Passport password grant.16 */17 public function validateForPassportPasswordGrant(string $password): bool18 {19 return Hash::check($password, $this->password);20 }21}
隐式许可令牌
我们不再建议使用隐式许可令牌。相反,您应该选择 OAuth2 服务器当前推荐的许可类型。
隐式许可类似于授权码许可;但是,令牌在不交换授权码的情况下返回给客户端。此许可最常用于客户端凭据无法安全存储的 JavaScript 或移动应用程序。要启用许可,请在应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用 enableImplicitGrant
方法
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Passport::enableImplicitGrant();7}
启用许可后,开发人员可以使用其客户端 ID 从您的应用程序请求访问令牌。消费应用程序应像这样向您的应用程序的 /oauth/authorize
路由发出重定向请求
1use Illuminate\Http\Request; 2 3Route::get('/redirect', function (Request $request) { 4 $request->session()->put('state', $state = Str::random(40)); 5 6 $query = http_build_query([ 7 'client_id' => 'client-id', 8 'redirect_uri' => 'http://third-party-app.com/callback', 9 'response_type' => 'token',10 'scope' => '',11 'state' => $state,12 // 'prompt' => '', // "none", "consent", or "login"13 ]);14 15 return redirect('http://passport-app.test/oauth/authorize?'.$query);16});
请记住,/oauth/authorize
路由已由 Passport 定义。您无需手动定义此路由。
客户端凭据许可令牌
客户端凭据许可适用于机器对机器的身份验证。例如,您可以在计划任务中使用此许可,该计划任务通过 API 执行维护任务。
在您的应用程序可以通过客户端凭据许可颁发令牌之前,您需要创建一个客户端凭据许可客户端。您可以使用 passport:client
Artisan 命令的 --client
选项来执行此操作
1php artisan passport:client --client
接下来,要使用此许可类型,请为 CheckClientCredentials
中间件注册一个中间件别名。您可以在应用程序的 bootstrap/app.php
文件中定义中间件别名
1use Laravel\Passport\Http\Middleware\CheckClientCredentials;2 3->withMiddleware(function (Middleware $middleware) {4 $middleware->alias([5 'client' => CheckClientCredentials::class6 ]);7})
然后,将中间件附加到路由
1Route::get('/orders', function (Request $request) {2 // ...3})->middleware('client');
要将对路由的访问限制为特定作用域,您可以在将 client
中间件附加到路由时提供以逗号分隔的所需作用域列表
1Route::get('/orders', function (Request $request) {2 // ...3})->middleware('client:check-status,your-scope');
检索令牌
要使用此许可类型检索令牌,请向 oauth/token
端点发出请求
1use Illuminate\Support\Facades\Http; 2 3$response = Http::asForm()->post('http://passport-app.test/oauth/token', [ 4 'grant_type' => 'client_credentials', 5 'client_id' => 'client-id', 6 'client_secret' => 'client-secret', 7 'scope' => 'your-scope', 8]); 9 10return $response->json()['access_token'];
个人访问令牌
有时,您的用户可能希望向自己颁发访问令牌,而无需完成典型的授权码重定向流程。允许用户通过您的应用程序 UI 向自己颁发令牌对于允许用户试用您的 API 或作为在一般情况下颁发访问令牌的更简单方法可能很有用。
如果您的应用程序主要使用 Passport 来颁发个人访问令牌,请考虑使用 Laravel Sanctum,Laravel 的轻量级第一方库,用于颁发 API 访问令牌。
创建个人访问客户端
在您的应用程序可以颁发个人访问令牌之前,您需要创建一个个人访问客户端。您可以通过执行带有 --personal
选项的 passport:client
Artisan 命令来执行此操作。如果您已运行 passport:install
命令,则无需运行此命令
1php artisan passport:client --personal
创建个人访问客户端后,将客户端的 ID 和纯文本密钥值放在应用程序的 .env
文件中
1PASSPORT_PERSONAL_ACCESS_CLIENT_ID="client-id-value"2PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="unhashed-client-secret-value"
管理个人访问令牌
创建个人访问客户端后,您可以使用 App\Models\User
模型实例上的 createToken
方法为给定用户颁发令牌。createToken
方法接受令牌名称作为其第一个参数,并接受一个可选的作用域数组作为其第二个参数
1use App\Models\User;2 3$user = User::find(1);4 5// Creating a token without scopes...6$token = $user->createToken('Token Name')->accessToken;7 8// Creating a token with scopes...9$token = $user->createToken('My Token', ['place-orders'])->accessToken;
JSON API
Passport 还包括一个 JSON API,用于管理个人访问令牌。您可以将其与您自己的前端配对,以便为用户提供一个仪表板来管理个人访问令牌。下面,我们将回顾所有用于管理个人访问令牌的 API 端点。为了方便起见,我们将使用 Axios 来演示向端点发出 HTTP 请求。
JSON API 由 web
和 auth
中间件保护;因此,它只能从您自己的应用程序中调用。它无法从外部来源调用。
GET /oauth/scopes
此路由返回为您的应用程序定义的所有作用域。您可以使用此路由列出用户可以分配给个人访问令牌的作用域
1axios.get('/oauth/scopes')2 .then(response => {3 console.log(response.data);4 });
GET /oauth/personal-access-tokens
此路由返回经过身份验证的用户创建的所有个人访问令牌。这主要用于列出用户的所有令牌,以便他们可以编辑或撤销它们
1axios.get('/oauth/personal-access-tokens')2 .then(response => {3 console.log(response.data);4 });
POST /oauth/personal-access-tokens
此路由创建新的个人访问令牌。它需要两个数据:令牌的 name
和应分配给令牌的 scopes
1const data = { 2 name: 'Token Name', 3 scopes: [] 4}; 5 6axios.post('/oauth/personal-access-tokens', data) 7 .then(response => { 8 console.log(response.data.accessToken); 9 })10 .catch (response => {11 // List errors on response...12 });
DELETE /oauth/personal-access-tokens/{token-id}
此路由可用于撤销个人访问令牌
1axios.delete('/oauth/personal-access-tokens/' + tokenId);
保护路由
通过中间件
Passport 包含一个身份验证守卫,它将在传入请求中验证访问令牌。将 api
守卫配置为使用 passport
驱动程序后,您只需在任何需要有效访问令牌的路由上指定 auth:api
中间件
1Route::get('/user', function () {2 // ...3})->middleware('auth:api');
如果您正在使用客户端凭据许可,则应使用client
中间件来保护您的路由,而不是 auth:api
中间件。
多个身份验证守卫
如果您的应用程序验证不同类型的用户(他们可能使用完全不同的 Eloquent 模型),您可能需要在应用程序中为每种用户提供器类型定义守卫配置。这允许您保护旨在用于特定用户提供器的请求。例如,给定 config/auth.php
配置文件中的以下守卫配置
1'api' => [2 'driver' => 'passport',3 'provider' => 'users',4],5 6'api-customers' => [7 'driver' => 'passport',8 'provider' => 'customers',9],
以下路由将利用 api-customers
守卫(它使用 customers
用户提供器)来验证传入请求
1Route::get('/customer', function () {2 // ...3})->middleware('auth:api-customers');
有关将多个用户提供器与 Passport 结合使用的更多信息,请查阅密码许可文档。
传递访问令牌
当调用受 Passport 保护的路由时,您的应用程序的 API 消费者应将他们的访问令牌指定为请求的 Authorization
标头中的 Bearer
令牌。例如,当使用 Guzzle HTTP 库时
1use Illuminate\Support\Facades\Http;2 3$response = Http::withHeaders([4 'Accept' => 'application/json',5 'Authorization' => 'Bearer '.$accessToken,6])->get('https://passport-app.test/api/user');7 8return $response->json();
令牌作用域
作用域允许您的 API 客户端在请求授权访问帐户时请求一组特定的权限。例如,如果您正在构建电子商务应用程序,则并非所有 API 消费者都需要下订单的功能。相反,您可以允许消费者仅请求授权访问订单发货状态。换句话说,作用域允许您的应用程序用户限制第三方应用程序可以代表他们执行的操作。
定义作用域
您可以使用应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中的 Passport::tokensCan
方法定义 API 的作用域。tokensCan
方法接受作用域名称和作用域描述的数组。作用域描述可以是您希望的任何内容,并将显示在授权批准屏幕上
1/** 2 * Bootstrap any application services. 3 */ 4public function boot(): void 5{ 6 Passport::tokensCan([ 7 'place-orders' => 'Place orders', 8 'check-status' => 'Check order status', 9 ]);10}
默认作用域
如果客户端未请求任何特定作用域,您可以配置 Passport 服务器以使用 setDefaultScope
方法将默认作用域附加到令牌。通常,您应该从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用此方法
1use Laravel\Passport\Passport; 2 3Passport::tokensCan([ 4 'place-orders' => 'Place orders', 5 'check-status' => 'Check order status', 6]); 7 8Passport::setDefaultScope([ 9 'check-status',10 'place-orders',11]);
Passport 的默认作用域不适用于用户生成的个人访问令牌。
将作用域分配给令牌
请求授权码时
当使用授权码许可请求访问令牌时,消费者应将其所需的 scope 指定为 scope
查询字符串参数。scope
参数应是以空格分隔的 scope 列表
1Route::get('/redirect', function () { 2 $query = http_build_query([ 3 'client_id' => 'client-id', 4 'redirect_uri' => 'http://example.com/callback', 5 'response_type' => 'code', 6 'scope' => 'place-orders check-status', 7 ]); 8 9 return redirect('http://passport-app.test/oauth/authorize?'.$query);10});
颁发个人访问令牌时
如果您正在使用 App\Models\User
模型的 createToken
方法颁发个人访问令牌,您可以将所需 scope 的数组作为该方法的第二个参数传递
1$token = $user->createToken('My Token', ['place-orders'])->accessToken;
检查作用域
Passport 包含两个中间件,可用于验证传入的请求是否已使用已被授予给定 scope 的令牌进行身份验证。要开始使用,请在应用程序的 bootstrap/app.php
文件中定义以下中间件别名
1use Laravel\Passport\Http\Middleware\CheckForAnyScope;2use Laravel\Passport\Http\Middleware\CheckScopes;3 4->withMiddleware(function (Middleware $middleware) {5 $middleware->alias([6 'scopes' => CheckScopes::class,7 'scope' => CheckForAnyScope::class,8 ]);9})
检查所有 Scope
可以将 scopes
中间件分配给路由,以验证传入请求的访问令牌是否具有所有列出的 scope
1Route::get('/orders', function () {2 // Access token has both "check-status" and "place-orders" scopes...3})->middleware(['auth:api', 'scopes:check-status,place-orders']);
检查任何 Scope
可以将 scope
中间件分配给路由,以验证传入请求的访问令牌是否具有至少一个列出的 scope
1Route::get('/orders', function () {2 // Access token has either "check-status" or "place-orders" scope...3})->middleware(['auth:api', 'scope:check-status,place-orders']);
检查令牌实例上的 Scope
一旦访问令牌身份验证的请求进入您的应用程序,您仍然可以使用经过身份验证的 App\Models\User
实例上的 tokenCan
方法来检查令牌是否具有给定的 scope
1use Illuminate\Http\Request;2 3Route::get('/orders', function (Request $request) {4 if ($request->user()->tokenCan('place-orders')) {5 // ...6 }7});
其他 Scope 方法
scopeIds
方法将返回所有已定义的 ID / 名称的数组
1use Laravel\Passport\Passport;2 3Passport::scopeIds();
scopes
方法将返回所有已定义的 scope 的数组,作为 Laravel\Passport\Scope
的实例
1Passport::scopes();
scopesFor
方法将返回与给定 ID / 名称匹配的 Laravel\Passport\Scope
实例的数组
1Passport::scopesFor(['place-orders', 'check-status']);
您可以使用 hasScope
方法确定是否已定义给定的 scope
1Passport::hasScope('place-orders');
使用 JavaScript 消费您的 API
在构建 API 时,能够从您的 JavaScript 应用程序中使用您自己的 API 非常有用。这种 API 开发方法允许您自己的应用程序使用与您与世界共享的 API 相同的 API。您的 Web 应用程序、移动应用程序、第三方应用程序以及您可能在各种包管理器上发布的任何 SDK 都可以使用相同的 API。
通常,如果您想从您的 JavaScript 应用程序中使用您的 API,您需要手动将访问令牌发送到应用程序,并将其与对您的应用程序的每个请求一起传递。但是,Passport 包含一个可以为您处理此问题的中间件。您只需将 CreateFreshApiToken
中间件附加到应用程序的 bootstrap/app.php
文件中的 web
中间件组即可
1use Laravel\Passport\Http\Middleware\CreateFreshApiToken;2 3->withMiddleware(function (Middleware $middleware) {4 $middleware->web(append: [5 CreateFreshApiToken::class,6 ]);7})
您应确保 CreateFreshApiToken
中间件是中间件堆栈中列出的最后一个中间件。
此中间件会将 laravel_token
cookie 附加到您的传出响应。此 cookie 包含一个加密的 JWT,Passport 将使用它来验证来自您的 JavaScript 应用程序的 API 请求。JWT 的生命周期等于您的 session.lifetime
配置值。现在,由于浏览器会自动将 cookie 与所有后续请求一起发送,因此您可以向您的应用程序的 API 发出请求,而无需显式传递访问令牌
1axios.get('/api/user')2 .then(response => {3 console.log(response.data);4 });
自定义 Cookie 名称
如果需要,您可以使用 Passport::cookie
方法自定义 laravel_token
cookie 的名称。通常,应从应用程序的 App\Providers\AppServiceProvider
类的 boot
方法中调用此方法
1/**2 * Bootstrap any application services.3 */4public function boot(): void5{6 Passport::cookie('custom_name');7}
CSRF 保护
当使用这种身份验证方法时,您需要确保在您的请求中包含有效的 CSRF 令牌标头。默认的 Laravel JavaScript scaffolding 包含一个 Axios 实例,它将自动使用加密的 XSRF-TOKEN
cookie 值在同源请求上发送 X-XSRF-TOKEN
标头。
如果您选择发送 X-CSRF-TOKEN
标头而不是 X-XSRF-TOKEN
,您将需要使用 csrf_token()
提供的未加密令牌。
事件
Passport 在颁发访问令牌和刷新令牌时会引发事件。您可以监听这些事件,以修剪或撤销数据库中的其他访问令牌
事件名称 |
---|
Laravel\Passport\Events\AccessTokenCreated |
Laravel\Passport\Events\RefreshTokenCreated |
测试
Passport 的 actingAs
方法可用于指定当前经过身份验证的用户及其 scope。传递给 actingAs
方法的第一个参数是用户实例,第二个参数是应授予用户令牌的 scope 数组
1use App\Models\User; 2use Laravel\Passport\Passport; 3 4test('servers can be created', function () { 5 Passport::actingAs( 6 User::factory()->create(), 7 ['create-servers'] 8 ); 9 10 $response = $this->post('/api/create-server');11 12 $response->assertStatus(201);13});
1use App\Models\User; 2use Laravel\Passport\Passport; 3 4public function test_servers_can_be_created(): void 5{ 6 Passport::actingAs( 7 User::factory()->create(), 8 ['create-servers'] 9 );10 11 $response = $this->post('/api/create-server');12 13 $response->assertStatus(201);14}
Passport 的 actingAsClient
方法可用于指定当前经过身份验证的客户端及其 scope。传递给 actingAsClient
方法的第一个参数是客户端实例,第二个参数是应授予客户端令牌的 scope 数组
1use Laravel\Passport\Client; 2use Laravel\Passport\Passport; 3 4test('orders can be retrieved', function () { 5 Passport::actingAsClient( 6 Client::factory()->create(), 7 ['check-status'] 8 ); 9 10 $response = $this->get('/api/orders');11 12 $response->assertStatus(200);13});
1use Laravel\Passport\Client; 2use Laravel\Passport\Passport; 3 4public function test_orders_can_be_retrieved(): void 5{ 6 Passport::actingAsClient( 7 Client::factory()->create(), 8 ['check-status'] 9 );10 11 $response = $this->get('/api/orders');12 13 $response->assertStatus(200);14}