简介 在 Laravel 中,实现基于传统表单的登陆和授权已经非常简单,但是如何满足 API 场景下的授权需求呢?在 API 场景里通常通过令牌来实现用户授权,而非维护请求之间的 Session 状态。在 Laravel 项目中使用 Passport 可以轻而易举地实现 API 授权认证,Passport 可以在几分钟之内为你的应用程序提供完整的 OAuth2 服务端实现。Passport 是基于由 Andy Millington 和 Simon Hamp 维护的 League OAuth2 server 建立的。 注意:本文档假定你已熟悉 OAuth2。如果你并不了解 OAuth2,阅读之前请先熟悉下 OAuth2 的 常用术语 和特性。 升级 Passport 当您想升级 Passport 到最新版本,仔细阅读 升级指南 将十分重要。 安装 在开始之前,请通过 Composer 包管理器安装 Passport: ``` composer require laravel/passport ``` Passport 服务提供器使用框架注册自己的数据库迁移目录,因此在注册提供器后,就应该运行 Passport 的迁移命令来自动创建存储客户端和令牌的数据表: ``` php artisan migrate ``` 接下来,运行 passport:install 命令来创建生成安全访问令牌时所需的加密密钥,同时,这条命令也会创建用于生成访问令牌的「个人访问」客户端和「密码授权」客户端: ``` php artisan passport:install ``` 技巧:如果您想使用「uuid」作为 Passport「客户端」模型的主键,而不是自动递增的整数,请使用 选项 uuids 来安装 Passport。 上面命令执行后,请将 Laravel\Passport\HasApiTokens Trait 添加到 App\Models\User 模型中,这个 Trait 会给你的模型提供一些辅助函数,用于检查已认证用户的令牌和使用范围: ``` <?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Passport\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; } ``` 接下来,在 AuthServiceProvider 的 boot 方法中调用 Passport::routes 函数。这个函数会注册发出访问令牌并撤销访问令牌、客户端和个人访问令牌所必需的路由: ``` <?php namespace App\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; use Laravel\Passport\Passport; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. * * @var array */ protected $policies = [ 'App\Models\Model' => 'App\Policies\ModelPolicy', ]; /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); if (! $this->app->routesAreCached()) { Passport::routes(); } } } ``` 最后,将配置文件 config/auth.php 中授权看守器 guards 的 api 的 driver 选项改为 passport。此调整会让你的应用程序在在验证传入的 API 的请求时使用 Passport 的 TokenGuard 来处理: ``` 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], ], ``` UUID 的客户端 现在,您可以在执行 passport:install 时使用 --uuids 选项,这个选项将引导 Passport 使用 UUID 作为 Passport Client 的主键,而不是使用自动递增的整数作为主键。执行 php artisan passport:install --uuids 后,您将得到关于禁用 Passport 默认迁移的相关指令说明: ``` php artisan passport:install --uuids ``` 前端快速上手 注意:为了使用 Passport 的 Vue 组件,你必须使用 Vue JavaScript 框架。这些组件也使用了 Bootstrap CSS 框架。然而,如果你不打算使用这些工具,这些组件对于你自己的前端组件编写也十分有价值。 提供了一系列 JSON API ,你可以用它们来允许你的用户创建客户端和个人访问令牌。然而,编写与这些 API 交互的前端代码可能是很占用时间的。因此,Passport 也包括了预编译的 Vue 组件,你可以直接使用或将其作为你自己的前端参考。 要使用 Passport 的 Vue 组件,使用 vendor:publish Artisan 命令: ``` php artisan vendor:publish --tag=passport-components ``` 被发布的组件将会被放到 resources/js/components 目录下。当组件被发布后,你应该在你的 resources/js/app.js 文件中注册它们: ``` Vue.component( 'passport-clients', require('./components/passport/Clients.vue').default ); Vue.component( 'passport-authorized-clients', require('./components/passport/AuthorizedClients.vue').default ); Vue.component( 'passport-personal-access-tokens', require('./components/passport/PersonalAccessTokens.vue').default ); ``` 注意:在 Laravel v5.7.19 之前,在注册组件时添加 .default 会导致控制台错误。有关此更改的解释,请参阅 Laravel Mix v4.0.0 release notes 发布说明。 注册完组件之后,需要运行 npm run dev 重新编译代码。编译好之后就可以在项目中使用组件建立客户端和个人访问令牌: ``` <passport-clients></passport-clients> <passport-authorized-clients></passport-authorized-clients> <passport-personal-access-tokens></passport-personal-access-tokens> ``` 部署 Passport 当要首次部署 Passport 到生产环境中,你需要运行 passport:keys 命令。 这个命令能生成访问令牌所需的密钥。生成的密钥一般情况下不应放在版本控制中: ``` php artisan passport:keys ``` 可以使用 Passport::loadKeysFrom 方法来自定义 Passport 密钥的加载路径: ``` /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); Passport::routes(); Passport::loadKeysFrom(__DIR__.'/../secrets/oauth'); } ``` 从环境中加载密钥 您可以使用 vendor:publish 命令发布配置文件: ``` php artisan vendor:publish --tag=passport-config ``` 发布配置文件后,可以通过将应用程序的加密密钥定义为环境变量来加载它们: ``` 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 默认的迁移,你需要在 AppServiceProvider 中的 register 方法使用 Passport::ignoreMigrations。使用 php artisan vendor:publish --tag=passport-migrations 会在 /database/migrations 下生成默认的迁移文件,可以再自行进行修改。 配置 客户端密钥哈希加密( Secret Hashing) 如果你希望客户端的密钥哈希加密的形式保存到数据库,你可以在 AppServiceProvider 的 boot 方法中添加 Passport::hashClientSecrets: ``` Passport::hashClientSecrets(); ``` 一旦被启用,当创建客户端时所有客户端密钥将会同时显示。如果丢失,则不可能恢复的,因为纯文本(plain-text)客户端密钥从不存储在数据库中。 令牌的有效期 Passport 发放的令牌的有效期默认为一年。如果你希望令牌有效期更长或更短,你可以用 tokensExpireIn, refreshTokensExpireIn, 或 personalAccessTokensExpireIn 的方法。 这些方法都在 AuthServiceProvider 的 boot 方法调用: ``` /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); Passport::routes(); Passport::tokensExpireIn(now()->addDays(15)); Passport::refreshTokensExpireIn(now()->addDays(30)); Passport::personalAccessTokensExpireIn(now()->addMonths(6)); } ``` 注意:在 Passprot 数据库表中,expires_at 字段是只读(read-only)的,用来展示有效期。Passport 发放令牌时,Passport 有效时间信息存储在签名和加密的令牌中。如果你希望使令牌无效,你可以撤销它。 覆盖默认模型 可以自由扩展 Passport 使用的模型: ``` use Laravel\Passport\Client as PassportClient; class Client extends PassportClient { // ... } 然后,你可以通过 Passport 类自定义模型覆盖默认模型: 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); } ``` 发放访问令牌 将 OAuth2 与授权代码一起使用是大多数开发者熟悉 OAuth2 的方式。当使用授权代码时,客户端应用程序将用户重定向到你的服务器,在那里他们将批准或拒绝向客户端发出访问令牌的请求。 管理客户端 首先,构建需要与应用程序 API 交互的应用程序,开发人员将需要通过创建一个「客户端」来注册自己的应用程序。一般来说,这包括在用户批准其授权请求后,提供其应用程序的名称和应用程序可以重定向到的 URL。 passport:client 命令 创建客户端最简单的方式是使用 passport:client Artisan 命令,你可以使用此命令创建自己的客户端,用于测试 OAuth2 的功能。在你执行 client 命令时,Passport 会提示你输入有关客户端的信息,最终会给你提供客户端的 ID 和密钥: ``` php artisan passport:client Redirect URLs ``` 如果你想为你的客户端允许多个重定向 URL,可以在 passport:client 命令提示输入 URL 时,使用逗号分隔来指定一个列表: http://example.com/callback,http://examplefoo.com/callback 注意:任何包含逗号的 URL 都必须进行编码。 JSON API 考虑到你的用户无法使用 client 命令,Passport 为此提供了可用于创建「客户端」的 JSON API 。这样你就省去了编写控制器来创建、更新和删除客户端的麻烦。 然而,你仍旧需要基于 Passport 的 JSON API 开发一套前端面板,以便你的用户提供管理客户端。下面我们会列出所有用于管理客户端的 API,为了方便起见,我们使用 Axios 来演示对端口发出 HTTP 请求。 这个 JSON API 由 web 和 auth 两个中间件保护,所以只能从应用程序中调用,不能从外部调用。 技巧:如果你不想自己实现整个客户端管理的前端界面,可以使用 前端快速上手 在几分钟内组建一套功能齐全的前端界面。 ``` GET /oauth/clients ``` 此路由会返回认证用户的所有客户端。主要用途是列出所有用户的客户端,以便他们可以编辑或删除它们: ``` axios.get('/oauth/clients') .then(response => { console.log(response.data); }); POST /oauth/clients ``` 此路由用于创建新客户端。它需要两个参数:客户端的名称 name 和授权后回调的 URL redirect。在批准或拒绝授权请求后,用户会被重定向到 redirect 参数提供的 URL 。 当客户端创建后,会返回客户端的 ID 和密钥。客户端可以使用这两个值从你的授权服务请求访问令牌。客户端创建的路由会返回新的客户端实例: ``` const data = { name: 'Client Name', redirect: 'http://example.com/callback' }; axios.post('/oauth/clients', data) .then(response => { console.log(response.data); }) .catch (response => { // 列出响应的错误... }); PUT /oauth/clients/{client-id} ``` 此路由用于更新客户端。 它需要需两个参数: 客户端名称 name 和 重定向 URL redirect 。 在请求被授权或被拒绝授权后,用户会重定向到 redirect 链接 。这个路由将会返回更新后的客户端实例: ``` 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/{client-id} ``` 此路由用于删除客户端(根据 client-id ): ``` axios.delete('/oauth/clients/' + clientId) .then(response => { // }); ``` 请求授权 授权时重定向 当客户端被创建后,开发者们会用它们的客户端 ID (client_id) 和密钥(secret) 请求授权码,并从应用程序访问令牌。 首先,接入应用的用户向你应用程序的 /oauth/authorize 路由发出重定向请求,示例如下: ``` 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); }); ``` 技巧:注意,路由 /oauth/authorize 已经在 Passport::routes 方法中定义。你不需要手动定义此路由。 批准请求 当接收到授权请求时,Passport 会自动向用户显示一个模版页面,允许用户批准或拒绝授权请求。如果用户批准请求,他们会被重定向回接入的应用程序指定的 redirect_uri。redirect_uri 必须和客户端创建时指定的 redirect 链接完全一致。 如果你想要自定义授权通过界面, 可以使用 Artisan 命令 vendor:publish 发布 Passport 的视图模板。发布的视图位于 resources/views/vendor/passport: ``` php artisan vendor:publish --tag=passport-views ``` 有时候你可能希望跳过授权提示页面, 例如授权第一方客户端时。你可以通过在 客户端模型 上定义 skipsAuthorization 方法来实现此目的。 如果 skipsAuthorization 返回 true,客户端将被批准,用户将被立即重定向回 redirect_uri : ``` <?php namespace App\Models\Passport; use Laravel\Passport\Client as BaseClient; class Client extends BaseClient { /** * 确定客户端是否应跳过授权提示 * * @return bool */ public function skipsAuthorization() { return $this->firstParty(); } } ``` 将授权码转化为访问令牌 如果用户通过了授权请求,会被重定向回客户端应用。客户端应用首先会验证之前传递给授权服务方的 state 参数。 如果该参数与之前传递的参数值匹配,则客户端会发起一个 POST 请求到服务端来请求一个访问令牌。该请求应包含用户通过授权请求时指定的授权码。在下面的例子中, 我们会使用 Guzzle HTTP 库来生成 POST 请求: ``` 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); }); /oauth/token 路由返回的 JSON 响应中应包含 access_token、 refresh_token 和 expires_in 属性。expires_in 属性包含访问令牌的过期时间(单位:秒)。 ``` 技巧:和 /oauth/authorize 路由一样,/oauth/token 路由在 Passport::routes 方法中定义了,你没必要去手动定义它。默认情况下,该路由使用 ThrottleRequests 中间件设置对访问频率进行限制。 JSON API Passport 还内置了 JSON API 来管理授权访问令牌,你可以将其与自己的前端代码整合为用户提供访问令牌管理后台。为了方便起见, 我们使用 Axios 来演示对 API 发起 HTTP 请求。这个 JSON API 应用了 web 和 auth 中间件; 因此,只能从自己的应用中访问。 ``` GET /oauth/tokens ``` 这个路由会返回认证用户创建的所有授权访问令牌,这在列举该用户的所有令牌以便撤销时很有用: ``` axios.get('/oauth/tokens') .then(response => { console.log(response.data); }); DELETE /oauth/tokens/{token-id} ``` 这个路由可用于撤销授权访问令牌及其相关的刷新令牌: ``` axios.delete('/oauth/tokens/' + tokenId); ``` 刷新令牌 如果你的应用程序发放了短期的访问令牌,那么用户需要通过访问令牌颁发时提供的刷新令牌来刷新访问令牌。在下面的例子中,我们使用 Guzzle HTTP 库来刷新令牌: ``` $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); /oauth/token 路由会返回一个 JSON 响应,其中包含 access_token 、 refresh_token 和 expires_in 属性。 expires_in 属性包含访问令牌过期时间(单位:秒)。 ``` 撤回令牌 如果你想撤回令牌,那么你可以在 TokenRepository 的 revokeAccessToken 方法撤销它(让 token 失效)。也可以在 RefreshTokenRepository 的 revokeRefreshTokensByAccessTokenId 中撤销某个 token 的刷新令牌: ``` $tokenRepository = app('Laravel\Passport\TokenRepository'); $refreshTokenRepository = app('Laravel\Passport\RefreshTokenRepository'); // Revoke an access token... $tokenRepository->revokeAccessToken($tokenId); // Revoke all of the token's refresh tokens... $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId); ``` 清除令牌 如果你想清除被撤销或过期的令牌,Passport 会提供以下的命令来清除它: ``` # Purge revoked and expired tokens and auth codes... php artisan passport:purge # Only purge revoked tokens and auth codes... php artisan passport:purge --revoked # Only purge expired tokens and auth codes... php artisan passport:purge --expired ``` 你可以在控制台的 Kernel 类中配置任务调度(scheduled_job)(/docs/laravel/8.x/scheduling) 来定时自动清理令牌: ``` /** * Define the application's command schedule. * * @param \Illuminate\Console\Scheduling\Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { $schedule->command('passport:purge')->hourly(); } ``` 通过 PKCE 发放授权码 通过 PKCE(Proof Key for Code Exchange, 中文译为” 代码交换的证明密钥”) 发放授权码是对单页面应用或原生应用进行认证以便访问 API 接口的安全方式。这种发放授权码是用于不能保证客户端密码被安全储存,或为降低攻击者拦截授权码的威胁。在这种模式下,当授权码获取令牌时,用” 验证码”(code verifier) 和” 质疑码”(code challenge, “challenge”,名词可译为’挑战;异议;质疑’等)的组合来交换客户端密钥。 创建客户端 在使用 PKCE 的授权码令牌之前,您需要创建一个启用了 PKCE 的客户机。您可以使用 passport:client 命令和 --public 选项来完成此操作: ``` php artisan passport:client --public ``` 请求令牌 ``` Code Verifier & Code Challenge ``` 由于这个授权准许不提供客户端密钥,开发者需要生成 Code Verifier 和 Code Challenge 的组合以请求令牌。 Code Verifier 应该是一个随机字符串,长度在 43 到 128 个字符之间,包含字母、数字和 -、. 、_ 、~,如 [RFC 7636 规范](tools.ietf.org/html/rfc7636) 中定义的那样。 Code Challenge 应该是一个 Base64 编码包含 URL 和文件名安全字符的字符串。应删除结尾的 '=' 字符,并且不应出现换行符、空白或其他附加字符。 ``` $encoded = base64_encode(hash('sha256', $code_verifier, true)); $codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_'); ``` 授权时的重定向 客户端创建之后,可以使用客户端 ID 和生成的 code verifier 和 code challenge 来从你的应用程序请求授权码和访问令牌。首先,接入的应用程序需要向你应用程序的 /oauth/authorize 路由发出重定向请求: ``` Route::get('/redirect', function (Request $request) { $request->session()->put('state', $state = Str::random(40)); $request->session()->put('code_verifier', $code_verifier = Str::random(128)); $codeChallenge = strtr(rtrim( base64_encode(hash('sha256', $code_verifier, true)) , '='), '+/', '-_'); $query = http_build_query([ 'client_id' => 'client-id', 'redirect_uri' => 'http://example.com/callback', 'response_type' => 'code', 'scope' => '', 'state' => $state, 'code_challenge' => $codeChallenge, 'code_challenge_method' => 'S256', ]); return redirect('http://your-app.com/oauth/authorize?'.$query); }); ``` 将授权码转换为访问令牌 如果用户批准授权请求,他们将被重定向回接入的应用程序。就像授权码许可规定的那样,接入方应首先根据重定向之前存储的值验证 state 参数。 如果 state 参数匹配,接入方应该向你的应用程序发出一个 POST 请求来获得访问令牌。该请求应该包含用户批准授权时从你的应用程序颁发的授权码和之前生成的 code verifier: ``` Route::get('/callback', function (Request $request) { $state = $request->session()->pull('state'); $codeVerifier = $request->session()->pull('code_verifier'); throw_unless( strlen($state) > 0 && $state === $request->state, InvalidArgumentException::class ); $response = (new GuzzleHttp\Client)->post('http://your-app.com/oauth/token', [ 'form_params' => [ 'grant_type' => 'authorization_code', 'client_id' => 'client-id', 'redirect_uri' => 'http://example.com/callback', 'code_verifier' => $codeVerifier, 'code' => $request->code, ], ]); return json_decode((string) $response->getBody(), true); }); ``` 密码授权令牌 OAuth2 密码授权机制可以让你自己的客户端(如移动应用程序)使用邮箱地址或者用户名和密码获取访问令牌。如此一来你就可以安全地向自己的客户端发出访问令牌,而不需要遍历整个 OAuth2 授权代码重定向流程。 创建密码授权客户端 在应用程序通过密码授权机制来发布令牌之前,在 passport:client 命令后加上 --password 参数来创建密码授权的客户端。如果你已经运行了 passport:install 命令,则不需要再运行此命令: ``` php artisan passport:client --password ``` 请求令牌 创建密码授权的客户端后,就可以使用用户的电子邮件地址和密码向 /oauth/token 路由发出 POST 请求来获取访问令牌。而该路由已经由 Passport::routes 方法注册,因此不需要手动定义它。如果请求成功,会在服务端返回的 JSON 响应中收到一个 access_token 和 refresh_token: ``` $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 参数 * 来授权应用程序支持的所有范围的令牌。如果你的请求中包含 scope 为 * 的参数,令牌实例上的 can 方法会始终返回 true。这种作用域的授权只能分配给使用 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' => '*', ], ]); ``` 自定义用户提供者 如果您的应用使用多个身份验证用户提供者,则可以在通过 artisan passport:client --password 命令创建客户端授权密码时提供 --provider 选项来指定使用的用户提供者。 指定的服务提供者名称应与 config/auth.php 配置文件中定义的有效提供者匹配。 然后,您可以使用中间件保护您的路由 以确保仅授权来自 guard 指定提供者的用户。 自定义用户名字段 当使用密码验证时,Passport 会在模型中使用 email 属性作为「username」。不过,你仍然可以通过在模型中定义 findForPassport 方法来自定义验证行为: ``` <?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Passport\HasApiTokens; class User extends Authenticatable { use HasApiTokens, Notifiable; /** * 通过给定的username获取用户实例 * * @param string $username * @return \App\Models\User */ public function findForPassport($username) { return $this->where('username', $username)->first(); } } ``` 自定义密码验证 当使用密码进行验证时,Passport 将使用模型中 password 属性值验证给定的密码。如果你的模型没有 password 属性或者你希望自定义密码验证的逻辑,你可以在模型中定义一个 validateForPassportPasswordGrant 方法来实现: ``` <?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Hash; use Laravel\Passport\HasApiTokens; class User extends Authenticatable { use HasApiTokens, Notifiable; /** * 通过Passport的密码授权验证用户使用的密码 * * @param string $password * @return bool */ public function validateForPassportPasswordGrant($password) { return Hash::check($password, $this->password); } } ``` 隐式授权令牌 隐式授权类似于授权码授权;但是,令牌将在不交换授权码的情况下返回给客户端。这种授权最常用于无法安全存储客户端凭据的 javascript 或移动应用程序。通过调用 AuthServiceProvider 中的 enableImplicitGrant 方法来启用这种授权: ``` /** * 注册任何身份验证/授权服务。 * * @return void */ public function boot() { $this->registerPolicies(); Passport::routes(); Passport::enableImplicitGrant(); } ``` 启用上述授权后,开发者们可以使用他们的客户端 ID 从你的应用程序请求访问令牌。接入的应用程序应该向你的应用程序的 /oauth/authorize 路由发出重定向请求,如下所示: ``` 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); }); ``` 技巧: 请记住,/oauth/authorize 路由已经在 Passport::routes 方法中定义好了,所以无需再次手动定义此路由。 客户端凭证授予令牌 客户端凭证授予令牌适用于计算机到计算机的身份验证。例如,你可以在通过 API 执行维护任务的计划作业中使用此授权。 在应用程序可以通过客户端凭证授予令牌之前,需要先创建一个客户端凭证授权的客户端。你可以通过 passport:client 命令行的 --client 选项来完成此操作: ``` php artisan passport:client --client ``` 接下来,要使用这种授权,你首先需要在 app/Http/Kernel.php 的 $routeMiddleware 属性中添加 CheckClientCredentials 中间件: ``` use Laravel\Passport\Http\Middleware\CheckClientCredentials; protected $routeMiddleware = [ 'client' => CheckClientCredentials::class, ]; ``` 之后,在路由上添加中间件: ``` Route::get('/orders', function (Request $request) { ... })->middleware('client'); ``` 要将对路由的访问限制在特定作用域内,可以在将 client 中间件附加到路由并提供以逗号分隔的所需作用域列表: ``` Route::get('/orders', function (Request $request) { ... })->middleware('client:check-status,your-scope'); ``` 获取令牌 要获取此授权类型的令牌,请向 oauth/token 发出请求: ``` $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']; ``` 个人访问令牌 有时候,你的用户要在不经过传统的授权码重定向流程的情况下向自己颁发访问令牌。允许用户通过应用程序用户界面对自己发出令牌,有助于用户体验你的 API,或者也可以将其作为一种更简单的发布访问令牌的方式。 创建个人访问客户端 在你的应用程序发布个人访问令牌之前,你需要在 passport:client 命令后带上 --personal 参数来创建对应的客户端。如果你已经运行了 passport:install 命令,则无需再运行此命令: ``` php artisan passport:client --personal ``` 创建个人访问客户端后,将客户端的 ID 和纯文本密钥放在应用程序的 .env 文件中: ``` PASSPORT_PERSONAL_ACCESS_CLIENT_ID=client-id-value PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value ``` 管理个人访问令牌 创建个人访问客户端后,你可以使用 User 模型实例上的 createToken 方法来为给定用户发布令牌。 createToken 方法接受令牌的名称作为其第一个参数和可选的 作用域 数组作为其第二个参数: ``` $user = App\Models\User::find(1); // 创建没有作用域的访问令牌... $token = $user->createToken('Token Name')->accessToken; // 创建有作用域的访问令牌... $token = $user->createToken('My Token', ['place-orders'])->accessToken; ``` JSON API Passport 中也有用来管理个人访问令牌的 JSON API,你可以将其与自己的前端配对,为用户提供管理个人访问令牌的仪表板。下面我们会介绍用于管理个人访问令牌的所有 API 接口。方便起见,我们使用 Axios 来演示对 API 的接口发出 HTTP 请求。 JSON API 由 web 和 auth 中间件保护;因此,只能从您自己的应用程序中调用它。无法从外部源调用它。 技巧:如果你不想实现自己的个人访问令牌管理的前端界面,可以根据 前端快速上手 在几分钟内组建功能齐全的前端界面。 ``` GET /oauth/scopes ``` 此路由会返回应用程序中定义的所有 作用域。你可以使用此路由列出用户可能分配给个人访问令牌的范围: ``` axios.get('/oauth/scopes') .then(response => { console.log(response.data); }); GET /oauth/personal-access-tokens ``` 此路由返回认证用户创建的所有个人访问令牌。这主要用于列出所有用户的令牌,以便他们可以编辑或删除它们: ``` axios.get('/oauth/personal-access-tokens') .then(response => { console.log(response.data); }); POST /oauth/personal-access-tokens ``` 此路由用于创建新的个人访问令牌。它需要两个数据:令牌的 name 和 scopes 。 ``` const data = { name: 'Token Name', scopes: [] }; axios.post('/oauth/personal-access-tokens', data) .then(response => { console.log(response.data.accessToken); }) .catch (response => { // List errors on response... }); DELETE /oauth/personal-access-tokens/{token-id} ``` 此路由可用于删除个人访问令牌: ``` axios.delete('/oauth/personal-access-tokens/' + tokenId); ``` 路由保护 通过中间件 Passport 包含一个 验证保护机制 可以验证请求中传入的访问令牌。配置 api 的看守器使用 passport 驱动程序后,只需要在需要有效访问令牌的任何路由上指定 auth:api 中间件: ``` Route::get('/user', function () { // })->middleware('auth:api'); ``` 多个身份验证 guard 如果您的应用程序可能使用完全不同的 Eloquent 模型、不同类型的用户进行身份验证,则可能需要为应用程序中的每种用户设置 guard。 这使您可以保护特定 guard 的请求。 例如,设置以下 guard config/auth.php 配置文件: ``` 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], 'api-customers' => [ 'driver' => 'passport', 'provider' => 'customers', ], ``` 以下路由将使用 customers 用户提供者的 api-customers guard 来验证传入的请求: ``` Route::get('/customer', function () { // })->middleware('auth:api-customers'); ``` 技巧:有关在 Passport 上使用多个用户提供者的更多信息,请查阅密码授予文档. 传递访问令牌 当调用 Passport 保护下的路由时,接入的 API 应用需要将访问令牌作为 Bearer 令牌放在请求头 Authorization 中。例如,使用 Guzzle HTTP 库时: ``` $response = $client->request('GET', '/api/user', [ 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer '.$accessToken, ], ]); ``` 令牌作用域 作用域可以让 API 客户端在请求账户授权时请求特定的权限。例如,如果你正在构建电子商务应用程序,并不是所有接入的 API 应用都需要下订单的功能。你可以让接入的 API 应用只被允许授权访问订单发货状态。换句话说,作用域允许应用程序的用户限制第三方应用程序执行的操作。 定义作用域 你可以在 AuthServiceProvider 的 boot 方法中使用 Passport::tokensCan 方法来定义 API 的作用域。tokensCan 方法接受一个包含作用域名称和描述的数组作为参数。作用域描述将会在授权确认页中直接展示给用户,你可以将其定义为任何你需要的内容: ``` use Laravel\Passport\Passport; Passport::tokensCan([ 'place-orders' => 'Place orders', 'check-status' => 'Check order status', ]); ``` 默认作用域 如果客户端没有请求任何特定的范围,你可以在 AuthServiceProvider 的 boot 方法中使用 Passport::setDefaultScope 方法来定义默认的作用域。 ``` use Laravel\Passport\Passport; Passport::setDefaultScope([ 'check-status', 'place-orders', ]); ``` 给令牌分配作用域 请求授权码 使用授权码请求访问令牌时,接入的应用需为 scope 参数指定所需作用域。 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); }); ``` 分发个人访问令牌 使用 User 模型的 createToken 方法发放个人访问令牌时,可以将所需作用域的数组作为第二个参数传给此方法: ``` $token = $user->createToken('My Token', ['place-orders'])->accessToken; ``` 检查作用域 Passport 包含两个中间件,可用于验证传入的请求是否包含访问指定作用域的令牌。 使用之前,需要将下面的中间件添加到 app/Http/Kernel.php 文件的 $routeMiddleware 属性中: ``` 'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class, 'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class, ``` 检查所有作用域 路由可以使用 scopes 中间件来检查当前请求是否拥有指定的 所有 作用域: ``` Route::get('/orders', function () { // 访问令牌具有 "check-status" 和 "place-orders" 作用域... })->middleware(['auth:api', 'scopes:check-status,place-orders']); ``` 检查任意作用域 路由可以使用 scope 中间件来检查当前请求是否拥有指定的 任意 作用域: ``` Route::get('/orders', function () { // Access token has either "check-status" or "place-orders" scope... })->middleware(['auth:api', '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'); ``` 使用 JavaScript 接入 API 在构建 API 时, 如果能通过 JavaScript 应用接入自己的 API 将会给开发过程带来极大的便利。这种 API 开发方法允许你使用自己的应用程序的 API 和别人共享的 API 。你的 Web 应用程序、移动应用程序、第三方应用程序以及可能在各种软件包管理器上发布的任何 SDK 都可能会使用相同的 API 。 通常,如果要在 JavaScript 应用程序中使用 API ,需要手动向应用程序发送访问令牌,并将其传递给应用程序。但是, Passport 有一个可以处理这个问题的中间件。将 CreateFreshApiToken 中间件添加到 app/Http/Kernel.php 文件中的 web 中间件组就可以了: ``` 'web' => [ // Other middleware... \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, ], ``` 注意:你需要确保 CreateFreshApiToken 是你的中间件堆栈中的最后一个中间件。 这个 Passport 中间件将在你所有的对外请求中添加一个 laravel_token cookie 。该 cookie 将包含一个加密后的 JWT , Passport 将用来验证来自 JavaScript 应用程序的 API 请求。至此,您可以在不明确传递访问令牌的情况下向应用程序的 API 发出请求: ``` axios.get('/api/user') .then(response => { console.log(response.data); }); ``` 自定义 Cookie 名称 如果需要,你可以在 AuthServiceProvider 的 boot 方法中使用 Passport::cookie 方法来自定义 laravel_token cookie 的名称。 ``` /** * 注册认证 / 授权服务 * * @return void */ public function boot() { $this->registerPolicies(); Passport::routes(); Passport::cookie('custom_name'); } ``` CSRF 保护 当使用这种授权方法时,您需要确认请求中包含有效的 CSRF 令牌。默认的 Laravel JavaScript 脚手架会包含一个 Axios 实例,该实例是自动使用加密的 XSRF-TOKEN cookie 值在同源请求上发送 X-XSRF-TOKEN 请求头。 技巧:如果您选择发送 X-CSRF-TOKEN 请求头而不是 X-XSRF-TOKEN ,则需要使用 csrf_token() 提供的未加密令牌。 事件 Passport 在发出访问令牌和刷新令牌时引发事件。 您可以使用这些事件来修改或撤消数据库中的其他访问令牌。 您可以在应用程序的 EventServiceProvider 中将***附加到这些事件: ``` /** * 应用程序事件监听映射 * * @var array */ protected $listen = [ 'Laravel\Passport\Events\AccessTokenCreated' => [ 'App\Listeners\RevokeOldTokens', ], 'Laravel\Passport\Events\RefreshTokenCreated' => [ 'App\Listeners\PruneOldTokens', ], ]; ``` 测试 Passport 的 actingAs 方法可以指定当前已认证用户及其作用域。 actingAs 方法的第一个参数是用户实例,第二个参数是用户令牌作用域数组: ``` use App\Models\User; use Laravel\Passport\Passport; public function testServerCreation() { Passport::actingAs( User::factory()->create(), ['create-servers'] ); $response = $this->post('/api/create-server'); $response->assertStatus(201); } Passport 的 actingAsClient 方法可以指定当前已认证用户及其作用域。 actingAsClient 方法的第一个参数是用户实例,第二个参数是用户令牌作用域数组: use Laravel\Passport\Client; use Laravel\Passport\Passport; public function testGetOrders() { Passport::actingAsClient( Client::factory()->create(), ['check-status'] ); $response = $this->get('/api/orders'); $response->assertStatus(200); } ```