Laravel Passport——OAuth2 API 认证系统源码解析(下)

隐式授权

隐式授权类似于授权码授权,但是它只令牌将返回给客户端而不交换授权码。这种授权最常用于无法安全存储客户端凭据的 JavaScript 或移动应用程序。通过调用 AuthServiceProvider 中的 enableImplicitGrant 方法来启用这种授权:

  1. public function boot()
  2. {
  3. $this->registerPolicies();
  4. Passport::routes();
  5. Passport::enableImplicitGrant();
  6. }

调用上面方法开启授权后,开发者可以使用他们的客户端 ID 从应用程序请求访问令牌。接入的应用程序应该向你的应用程序的 /oauth/authorize 路由发出重定向请求,如下所示:

  1. Route::get('/redirect', function () {
  2. $query = http_build_query([
  3. 'client_id' => 'client-id',
  4. 'redirect_uri' => 'http://example.com/callback',
  5. 'response_type' => 'token',
  6. 'scope' => '',
  7. ]);
  8. return redirect('http://your-app.com/oauth/authorize?'.$query);
  9. });

首先仍然是验证授权请求的合法性,其流程与授权码模式基本一致:

  1. public function validateAuthorizationRequest(ServerRequestInterface $request)
  2. {
  3. $clientId = $this->getQueryStringParameter(
  4. 'client_id',
  5. $request,
  6. $this->getServerParameter('PHP_AUTH_USER', $request)
  7. );
  8. if (is_null($clientId)) {
  9. throw OAuthServerException::invalidRequest('client_id');
  10. }
  11. $client = $this->clientRepository->getClientEntity(
  12. $clientId,
  13. $this->getIdentifier(),
  14. null,
  15. false
  16. );
  17. if ($client instanceof ClientEntityInterface === false) {
  18. $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
  19. throw OAuthServerException::invalidClient();
  20. }
  21. $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);
  22. $scopes = $this->validateScopes(
  23. $this->getQueryStringParameter('scope', $request, $this->defaultScope),
  24. is_array($client->getRedirectUri())
  25. ? $client->getRedirectUri()[0]
  26. : $client->getRedirectUri()
  27. );
  28. // Finalize the requested scopes
  29. $finalizedScopes = $this->scopeRepository->finalizeScopes(
  30. $scopes,
  31. $this->getIdentifier(),
  32. $client
  33. );
  34. $stateParameter = $this->getQueryStringParameter('state', $request);
  35. $authorizationRequest = new AuthorizationRequest();
  36. $authorizationRequest->setGrantTypeId($this->getIdentifier());
  37. $authorizationRequest->setClient($client);
  38. $authorizationRequest->setRedirectUri($redirectUri);
  39. $authorizationRequest->setState($stateParameter);
  40. $authorizationRequest->setScopes($finalizedScopes);
  41. return $authorizationRequest;
  42. }

接着,当用户同意授权之后,就要直接返回 access_tokenLeague OAuth2 直接将令牌放入 JWT 中发送回第三方客户端,值得注意的是依据 OAuth2 标准,参数都是以 location hash 的形式返回的,间隔符是 #,而不是 ?:

  1. public function __construct(\DateInterval $accessTokenTTL, $queryDelimiter = '#')
  2. {
  3. $this->accessTokenTTL = $accessTokenTTL;
  4. $this->queryDelimiter = $queryDelimiter;
  5. }
  6. public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
  7. {
  8. if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
  9. throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
  10. }
  11. $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
  12. ? is_array($authorizationRequest->getClient()->getRedirectUri())
  13. ? $authorizationRequest->getClient()->getRedirectUri()[0]
  14. : $authorizationRequest->getClient()->getRedirectUri()
  15. : $authorizationRequest->getRedirectUri();
  16. // The user approved the client, redirect them back with an access token
  17. if ($authorizationRequest->isAuthorizationApproved() === true) {
  18. $accessToken = $this->issueAccessToken(
  19. $this->accessTokenTTL,
  20. $authorizationRequest->getClient(),
  21. $authorizationRequest->getUser()->getIdentifier(),
  22. $authorizationRequest->getScopes()
  23. );
  24. $response = new RedirectResponse();
  25. $response->setRedirectUri(
  26. $this->makeRedirectUri(
  27. $finalRedirectUri,
  28. [
  29. 'access_token' => (string) $accessToken->convertToJWT($this->privateKey),
  30. 'token_type' => 'Bearer',
  31. 'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - (new \DateTime())->getTimestamp(),
  32. 'state' => $authorizationRequest->getState(),
  33. ],
  34. $this->queryDelimiter
  35. )
  36. );
  37. return $response;
  38. }
  39. // The user denied the client, redirect them back with an error
  40. throw OAuthServerException::accessDenied(
  41. 'The user denied the request',
  42. $this->makeRedirectUri(
  43. $finalRedirectUri,
  44. [
  45. 'state' => $authorizationRequest->getState(),
  46. ]
  47. )
  48. );
  49. }

这个用于构建 jwt 的私钥就是 oauth-private.key,我们知道,jwt 一般有三个部分组成:headerclaimsign, 用于 oauth2jwtclaim 主要构成有:

  • aud 客户端 id
  • jti access_token 随机码
  • iat 生成时间
  • nbf 拒绝接受 jwt 时间
  • exp access_token 失效时间
  • sub 用户 id

具体可以参考 : JSON Web Token (JWT) draft-ietf-oauth-json-web-token-32

  1. public function convertToJWT(CryptKey $privateKey)
  2. {
  3. return (new Builder())
  4. ->setAudience($this->getClient()->getIdentifier())
  5. ->setId($this->getIdentifier(), true)
  6. ->setIssuedAt(time())
  7. ->setNotBefore(time())
  8. ->setExpiration($this->getExpiryDateTime()->getTimestamp())
  9. ->setSubject($this->getUserIdentifier())
  10. ->set('scopes', $this->getScopes())
  11. ->sign(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()))
  12. ->getToken();
  13. }
  14. public function __construct(
  15. Encoder $encoder = null,
  16. ClaimFactory $claimFactory = null
  17. ) {
  18. $this->encoder = $encoder ?: new Encoder();
  19. $this->claimFactory = $claimFactory ?: new ClaimFactory();
  20. $this->headers = ['typ'=> 'JWT', 'alg' => 'none'];
  21. $this->claims = [];
  22. }
  23. public function setAudience($audience, $replicateAsHeader = false)
  24. {
  25. return $this->setRegisteredClaim('aud', (string) $audience, $replicateAsHeader);
  26. }
  27. public function setId($id, $replicateAsHeader = false)
  28. {
  29. return $this->setRegisteredClaim('jti', (string) $id, $replicateAsHeader);
  30. }
  31. public function setIssuedAt($issuedAt, $replicateAsHeader = false)
  32. {
  33. return $this->setRegisteredClaim('iat', (int) $issuedAt, $replicateAsHeader);
  34. }
  35. public function setNotBefore($notBefore, $replicateAsHeader = false)
  36. {
  37. return $this->setRegisteredClaim('nbf', (int) $notBefore, $replicateAsHeader);
  38. }
  39. public function setExpiration($expiration, $replicateAsHeader = false)
  40. {
  41. return $this->setRegisteredClaim('exp', (int) $expiration, $replicateAsHeader);
  42. }
  43. public function setSubject($subject, $replicateAsHeader = false)
  44. {
  45. return $this->setRegisteredClaim('sub', (string) $subject, $replicateAsHeader);
  46. }
  47. public function sign(Signer $signer, $key)
  48. {
  49. $signer->modifyHeader($this->headers);
  50. $this->signature = $signer->sign(
  51. $this->getToken()->getPayload(),
  52. $key
  53. );
  54. return $this;
  55. }
  56. public function getToken()
  57. {
  58. $payload = [
  59. $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->headers)),
  60. $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->claims))
  61. ];
  62. if ($this->signature !== null) {
  63. $payload[] = $this->encoder->base64UrlEncode($this->signature);
  64. }
  65. return new Token($this->headers, $this->claims, $this->signature, $payload);
  66. }

根据 JWT 的生成方法,签名部分 signatureheaderclaim 进行 base64 编码后再加密的结果。

客户端模式

客户端凭据授权适用于机器到机器的认证。例如,你可以在通过 API 执行维护任务中使用此授权。要使用这种授权,你首先需要在 app/Http/Kernel.php 的 routeMiddleware 变量中添加新的中间件:

  1. protected $routeMiddleware = [
  2. 'client' => CheckClientCredentials::class,
  3. ];
  4. Route::get('/user', function(Request $request) {
  5. ...
  6. })->middleware('client');

接下来通过向 oauth/token 接口发出请求来获取令牌:

  1. $response = $guzzle->post('http://your-app.com/oauth/token', [
  2. 'form_params' => [
  3. 'grant_type' => 'client_credentials',
  4. 'client_id' => 'client-id',
  5. 'client_secret' => 'client-secret',
  6. 'scope' => 'your-scope',
  7. ],
  8. ]);
  9. echo json_decode((string) $response->getBody(), true);

客户端模式类似于授权码模式的后一部分,利用客户端 id 与客户端密码来获取 access_token

  1. public function respondToAccessTokenRequest(
  2. ServerRequestInterface $request,
  3. ResponseTypeInterface $responseType,
  4. \DateInterval $accessTokenTTL
  5. ) {
  6. // Validate request
  7. $client = $this->validateClient($request);
  8. $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
  9. // Finalize the requested scopes
  10. $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client);
  11. // Issue and persist access token
  12. $accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes);
  13. // Inject access token into response type
  14. $responseType->setAccessToken($accessToken);
  15. return $responseType;
  16. }

类似于授权码模式,access_token 的发放也是通过 Bearer Token 中存放 JWT。

密码模式

OAuth2 密码授权机制可以让你自己的客户端(如移动应用程序)邮箱地址或者用户名和密码获取访问令牌。如此一来你就可以安全地向自己的客户端发出访问令牌,而不需要遍历整个 OAuth2 授权代码重定向流程。

创建密码授权的客户端后,就可以通过向用户的电子邮件地址和密码向 /oauth/token 路由发出 POST 请求来获取访问令牌。而该路由已经由 Passport::routes 方法注册,因此不需要手动定义它。如果请求成功,会在服务端返回的 JSON 响应中收到一个 access_token 和 refresh_token:

  1. $response = $http->post('http://your-app.com/oauth/token', [
  2. 'form_params' => [
  3. 'grant_type' => 'password',
  4. 'client_id' => 'client-id',
  5. 'client_secret' => 'client-secret',
  6. 'username' => 'taylor@laravel.com',
  7. 'password' => 'my-password',
  8. 'scope' => '',
  9. ],
  10. ]);
  11. return json_decode((string) $response->getBody(), true);

只要用用户名与密码来验证合法性就可以发放 access_tokenrefresh_token

  1. public function respondToAccessTokenRequest(
  2. ServerRequestInterface $request,
  3. ResponseTypeInterface $responseType,
  4. \DateInterval $accessTokenTTL
  5. ) {
  6. // Validate request
  7. $client = $this->validateClient($request);
  8. $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
  9. $user = $this->validateUser($request, $client);
  10. // Finalize the requested scopes
  11. $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
  12. // Issue and persist new tokens
  13. $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
  14. $refreshToken = $this->issueRefreshToken($accessToken);
  15. // Inject tokens into response
  16. $responseType->setAccessToken($accessToken);
  17. $responseType->setRefreshToken($refreshToken);
  18. return $responseType;
  19. }
  20. protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
  21. {
  22. $username = $this->getRequestParameter('username', $request);
  23. if (is_null($username)) {
  24. throw OAuthServerException::invalidRequest('username');
  25. }
  26. $password = $this->getRequestParameter('password', $request);
  27. if (is_null($password)) {
  28. throw OAuthServerException::invalidRequest('password');
  29. }
  30. $user = $this->userRepository->getUserEntityByUserCredentials(
  31. $username,
  32. $password,
  33. $this->getIdentifier(),
  34. $client
  35. );
  36. if ($user instanceof UserEntityInterface === false) {
  37. $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
  38. throw OAuthServerException::invalidCredentials();
  39. }
  40. return $user;
  41. }

路由保护

Passport 包含一个 验证保护机制 可以验证请求中传入的访问令牌。配置 api 的看守器使用 passport 驱动程序后,只需要在需要有效访问令牌的任何路由上指定 auth:api 中间件:

  1. Route::get('/user', function () {
  2. //
  3. })->middleware('auth:api');

当调用 Passport 保护下的路由时,接入的 API 应用需要将访问令牌作为 Bearer 令牌放在请求头 Authorization 中。例如,使用 Guzzle HTTP 库时:

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

auth:api 中间件

当我们已经配置完成 Passport 的四种模式并拿到 access_token 之后,我们就可以利用令牌去资源服务器获取数据了。资源服务器最常用的校验令牌的中间件就是 auth:api,中间件是 authapi 是中间件的参数:

  1. 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,

这个中间件是验证登录状态的常用中间件:

  1. class Authenticate
  2. {
  3. public function __construct(Auth $auth)
  4. {
  5. $this->auth = $auth;
  6. }
  7. public function handle($request, Closure $next, ...$guards)
  8. {
  9. $this->authenticate($guards);
  10. return $next($request);
  11. }
  12. protected function authenticate(array $guards)
  13. {
  14. if (empty($guards)) {
  15. return $this->auth->authenticate();
  16. }
  17. foreach ($guards as $guard) {
  18. if ($this->auth->guard($guard)->check()) {
  19. return $this->auth->shouldUse($guard);
  20. }
  21. }
  22. throw new AuthenticationException('Unauthenticated.', $guards);
  23. }
  24. }

我们的参数 api 就是上面的 guardsAuthlaravel 自带的登录校验服务:

  1. class AuthManager implements FactoryContract
  2. {
  3. public function guard($name = null)
  4. {
  5. $name = $name ?: $this->getDefaultDriver();
  6. return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
  7. }
  8. protected function resolve($name)
  9. {
  10. $config = $this->getConfig($name);
  11. if (is_null($config)) {
  12. throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
  13. }
  14. if (isset($this->customCreators[$config['driver']])) {
  15. return $this->callCustomCreator($name, $config);
  16. }
  17. $driverMethod = 'create'.ucfirst($config['driver']).'Driver';
  18. if (method_exists($this, $driverMethod)) {
  19. return $this->{$driverMethod}($name, $config);
  20. }
  21. throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined.");
  22. }
  23. }

文档告诉我们,若想要使用 passport 服务,我们的 config/auth 文件需要如此配置:

  1. 'guards' => [
  2. 'web' => [
  3. 'driver' => 'session',
  4. 'provider' => 'users',
  5. ],
  6. 'api' => [
  7. 'driver' => 'passport',
  8. 'provider' => 'users',
  9. ],
  10. ],

可以看出,driver 就是 passport,我们在启动 passport 服务的时候曾经注册过一个 Guard

  1. protected function registerGuard()
  2. {
  3. Auth::extend('passport', function ($app, $name, array $config) {
  4. return tap($this->makeGuard($config), function ($guard) {
  5. $this->app->refresh('request', $guard, 'setRequest');
  6. });
  7. });
  8. }
  9. protected function makeGuard(array $config)
  10. {
  11. return new RequestGuard(function ($request) use ($config) {
  12. return (new TokenGuard(
  13. $this->app->make(ResourceServer::class),
  14. Auth::createUserProvider($config['provider']),
  15. $this->app->make(TokenRepository::class),
  16. $this->app->make(ClientRepository::class),
  17. $this->app->make('encrypter')
  18. ))->user($request);
  19. }, $this->app['request']);
  20. }

因此,passport 使用的就是这个 TokenGuard

  1. class TokenGuard
  2. {
  3. public function __construct(ResourceServer $server,
  4. UserProvider $provider,
  5. TokenRepository $tokens,
  6. ClientRepository $clients,
  7. Encrypter $encrypter)
  8. {
  9. $this->server = $server;
  10. $this->tokens = $tokens;
  11. $this->clients = $clients;
  12. $this->provider = $provider;
  13. $this->encrypter = $encrypter;
  14. }
  15. public function user(Request $request)
  16. {
  17. if ($request->bearerToken()) {
  18. return $this->authenticateViaBearerToken($request);
  19. } elseif ($request->cookie(Passport::cookie())) {
  20. return $this->authenticateViaCookie($request);
  21. }
  22. }
  23. }

可以看到,TokenGuard 支持两种 Token 的验证:BearerTokencookie

我们首先看 BearerToken:

  1. public function bearerToken()
  2. {
  3. $header = $this->header('Authorization', '');
  4. if (Str::startsWith($header, 'Bearer ')) {
  5. return Str::substr($header, 7);
  6. }
  7. }
  8. protected function authenticateViaBearerToken($request)
  9. {
  10. $psr = (new DiactorosFactory)->createRequest($request);
  11. try {
  12. $psr = $this->server->validateAuthenticatedRequest($psr);
  13. $user = $this->provider->retrieveById(
  14. $psr->getAttribute('oauth_user_id')
  15. );
  16. if (! $user) {
  17. return;
  18. }
  19. $token = $this->tokens->find(
  20. $psr->getAttribute('oauth_access_token_id')
  21. );
  22. $clientId = $psr->getAttribute('oauth_client_id');
  23. if ($this->clients->revoked($clientId)) {
  24. return;
  25. }
  26. return $token ? $user->withAccessToken($token) : null;
  27. } catch (OAuthServerException $e) {
  28. return Container::getInstance()->make(
  29. ExceptionHandler::class
  30. )->report($e);
  31. }
  32. }

首先,需要验证请求的合法性:

  1. class ResourceServer
  2. {
  3. public function validateAuthenticatedRequest(ServerRequestInterface $request)
  4. {
  5. return $this->getAuthorizationValidator()->validateAuthorization($request);
  6. }
  7. protected function getAuthorizationValidator()
  8. {
  9. if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) {
  10. $this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository);
  11. }
  12. $this->authorizationValidator->setPublicKey($this->publicKey);
  13. return $this->authorizationValidator;
  14. }
  15. }

BearerTokenValidator 专门用于验证 BearerToken 的合法性:

  1. class BearerTokenValidator implements AuthorizationValidatorInterface
  2. {
  3. public function validateAuthorization(ServerRequestInterface $request)
  4. {
  5. if ($request->hasHeader('authorization') === false) {
  6. throw OAuthServerException::accessDenied('Missing "Authorization" header');
  7. }
  8. $header = $request->getHeader('authorization');
  9. $jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
  10. try {
  11. // Attempt to parse and validate the JWT
  12. $token = (new Parser())->parse($jwt);
  13. if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
  14. throw OAuthServerException::accessDenied('Access token could not be verified');
  15. }
  16. // Ensure access token hasn't expired
  17. $data = new ValidationData();
  18. $data->setCurrentTime(time());
  19. if ($token->validate($data) === false) {
  20. throw OAuthServerException::accessDenied('Access token is invalid');
  21. }
  22. // Check if token has been revoked
  23. if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
  24. throw OAuthServerException::accessDenied('Access token has been revoked');
  25. }
  26. // Return the request with additional attributes
  27. return $request
  28. ->withAttribute('oauth_access_token_id', $token->getClaim('jti'))
  29. ->withAttribute('oauth_client_id', $token->getClaim('aud'))
  30. ->withAttribute('oauth_user_id', $token->getClaim('sub'))
  31. ->withAttribute('oauth_scopes', $token->getClaim('scopes'));
  32. } catch (\InvalidArgumentException $exception) {
  33. // JWT couldn't be parsed so return the request as is
  34. throw OAuthServerException::accessDenied($exception->getMessage());
  35. } catch (\RuntimeException $exception) {
  36. //JWR couldn't be parsed so return the request as is
  37. throw OAuthServerException::accessDenied('Error while decoding to JSON');
  38. }
  39. }
  40. }

通过 passport 拿到的 access_token 都是 JWT 格式的,因此首先第一步需要将 JWT 解析:

  1. class Parser
  2. {
  3. public function parse($jwt)
  4. {
  5. $data = $this->splitJwt($jwt);
  6. $header = $this->parseHeader($data[0]);
  7. $claims = $this->parseClaims($data[1]);
  8. $signature = $this->parseSignature($header, $data[2]);
  9. foreach ($claims as $name => $value) {
  10. if (isset($header[$name])) {
  11. $header[$name] = $value;
  12. }
  13. }
  14. if ($signature === null) {
  15. unset($data[2]);
  16. }
  17. return new Token($header, $claims, $signature, $data);
  18. }
  19. protected function splitJwt($jwt)
  20. {
  21. if (!is_string($jwt)) {
  22. throw new InvalidArgumentException('The JWT string must have two dots');
  23. }
  24. $data = explode('.', $jwt);
  25. if (count($data) != 3) {
  26. throw new InvalidArgumentException('The JWT string must have two dots');
  27. }
  28. return $data;
  29. }
  30. protected function parseHeader($data)
  31. {
  32. $header = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));
  33. if (isset($header['enc'])) {
  34. throw new InvalidArgumentException('Encryption is not supported yet');
  35. }
  36. return $header;
  37. }
  38. protected function parseClaims($data)
  39. {
  40. $claims = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));
  41. foreach ($claims as $name => &$value) {
  42. $value = $this->claimFactory->create($name, $value);
  43. }
  44. return $claims;
  45. }
  46. protected function parseSignature(array $header, $data)
  47. {
  48. if ($data == '' || !isset($header['alg']) || $header['alg'] == 'none') {
  49. return null;
  50. }
  51. $hash = $this->decoder->base64UrlDecode($data);
  52. return new Signature($hash);
  53. }
  54. }

获得 JWT 的三个部分之后,就要验证签名部分是否合法:

  1. class Token
  2. {
  3. public function verify(Signer $signer, $key)
  4. {
  5. if ($this->signature === null) {
  6. throw new BadMethodCallException('This token is not signed');
  7. }
  8. if ($this->headers['alg'] !== $signer->getAlgorithmId()) {
  9. return false;
  10. }
  11. return $this->signature->verify($signer, $this->getPayload(), $key);
  12. }
  13. }

验证通过之后,就要验证 JWT 各个部分是否合法:

  1. $data = new ValidationData();
  2. $data->setCurrentTime(time());
  3. public function __construct($currentTime = null)
  4. {
  5. $currentTime = $currentTime ?: time();
  6. $this->items = [
  7. 'jti' => null,
  8. 'iss' => null,
  9. 'aud' => null,
  10. 'sub' => null,
  11. 'iat' => $currentTime,
  12. 'nbf' => $currentTime,
  13. 'exp' => $currentTime
  14. ];
  15. }
  16. public function validate(ValidationData $data)
  17. {
  18. foreach ($this->getValidatableClaims() as $claim) {
  19. if (!$claim->validate($data)) {
  20. return false;
  21. }
  22. }
  23. return true;
  24. }
  25. public function __construct(array $callbacks = [])
  26. {
  27. $this->callbacks = array_merge(
  28. [
  29. 'iat' => [$this, 'createLesserOrEqualsTo'],
  30. 'nbf' => [$this, 'createLesserOrEqualsTo'],
  31. 'exp' => [$this, 'createGreaterOrEqualsTo'],
  32. 'iss' => [$this, 'createEqualsTo'],
  33. 'aud' => [$this, 'createEqualsTo'],
  34. 'sub' => [$this, 'createEqualsTo'],
  35. 'jti' => [$this, 'createEqualsTo']
  36. ],
  37. $callbacks
  38. );
  39. }

我们前面说过,

  • aud 客户端 id
  • jti access_token 随机码
  • iat 生成时间
  • nbf 拒绝接受 jwt 时间
  • exp access_token 失效时间
  • sub 用户 id

因此,JWT 的生成时间、拒绝接受时间、失效时间就会被验证完成。

接下来,还会验证最重要的 access_token

  1. if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
  2. throw OAuthServerException::accessDenied('Access token has been revoked');
  3. }
  4. public function isAccessTokenRevoked($tokenId)
  5. {
  6. return $this->tokenRepository->isAccessTokenRevoked($tokenId);
  7. }
  8. public function isAccessTokenRevoked($id)
  9. {
  10. if ($token = $this->find($id)) {
  11. return $token->revoked;
  12. }
  13. return true;
  14. }

接下来,TokenGuard 就会验证 useridclientidaccess_token 的合法性:

  1. $user = $this->provider->retrieveById(
  2. $psr->getAttribute('oauth_user_id')
  3. );
  4. if (! $user) {
  5. return;
  6. }
  7. $token = $this->tokens->find(
  8. $psr->getAttribute('oauth_access_token_id')
  9. );
  10. $clientId = $psr->getAttribute('oauth_client_id');
  11. if ($this->clients->revoked($clientId)) {
  12. return;
  13. }
  14. return $token ? $user->withAccessToken($token) : null;

中间件验证完成。

客户端模式中间件 CheckClientCredentials

我们在上面可以看到 auth:api 中间件不仅验证 access_token,还会验证 user_id,对于客户端模式来说,由于 JWT 中并没有用户信息,因此 passport 专门存在中间件 CheckClientCredentials 来做非登录状态的校验。

  1. class CheckClientCredentials
  2. {
  3. public function handle($request, Closure $next, ...$scopes)
  4. {
  5. $psr = (new DiactorosFactory)->createRequest($request);
  6. try {
  7. $psr = $this->server->validateAuthenticatedRequest($psr);
  8. } catch (OAuthServerException $e) {
  9. throw new AuthenticationException;
  10. }
  11. $this->validateScopes($psr, $scopes);
  12. return $next($request);
  13. }
  14. }

使用 JavaScript 接入 API

在构建 API 时,如果能通过 JavaScript 应用接入自己的 API 将会给开发过程带来极大的便利。这种 API 开发方法允许你使用自己的应用程序的 API 和别人共享的 API。你的 Web 应用程序、移动应用程序、第三方应用程序以及可能在各种软件包管理器上发布的任何 SDK 都可能会使用相同的API。

通常,如果要从 JavaScript 应用程序中使用 API,则需要手动向应用程序发送访问令牌,并将其传递给应用程序。但是,Passport 有一个可以处理这个问题的中间件。将 CreateFreshApiToken 中间件添加到 web 中间件组就可以了:

  1. 'web' => [
  2. // Other middleware...
  3. \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
  4. ],

Passport 的这个中间件将会在你所有的对外请求中添加一个 laravel_token cookie。该 cookie 将包含一个加密后的 JWT ,Passport 将用来验证来自 JavaScript 应用程序的 API 请求。至此,你可以在不明确传递访问令牌的情况下向应用程序的 API 发出请求

  1. axios.get('/user')
  2. .then(response => {
  3. console.log(response.data);
  4. });

当使用上面的授权方法时,Axios 会自动带上 X-CSRF-TOKEN 请求头传递。另外,默认的 Laravel JavaScript 脚手架会让 Axios 发送 X-Requested-With 请求头:

  1. window.axios.defaults.headers.common = {
  2. 'X-Requested-With': 'XMLHttpRequest',
  3. };

CreateFreshApiToken 中间件

  1. class CreateFreshApiToken
  2. {
  3. public function handle($request, Closure $next, $guard = null)
  4. {
  5. $this->guard = $guard;
  6. $response = $next($request);
  7. if ($this->shouldReceiveFreshToken($request, $response)) {
  8. $response->withCookie($this->cookieFactory->make(
  9. $request->user($this->guard)->getKey(), $request->session()->token()
  10. ));
  11. }
  12. return $response;
  13. }
  14. public function make($userId, $csrfToken)
  15. {
  16. $config = $this->config->get('session');
  17. $expiration = Carbon::now()->addMinutes($config['lifetime']);
  18. return new Cookie(
  19. Passport::cookie(),
  20. $this->createToken($userId, $csrfToken, $expiration),
  21. $expiration,
  22. $config['path'],
  23. $config['domain'],
  24. $config['secure'],
  25. true
  26. );
  27. }
  28. protected function createToken($userId, $csrfToken, Carbon $expiration)
  29. {
  30. return JWT::encode([
  31. 'sub' => $userId,
  32. 'csrf' => $csrfToken,
  33. 'expiry' => $expiration->getTimestamp(),
  34. ], $this->encrypter->getKey());
  35. }
  36. protected function shouldReceiveFreshToken($request, $response)
  37. {
  38. return $this->requestShouldReceiveFreshToken($request) &&
  39. $this->responseShouldReceiveFreshToken($response);
  40. }
  41. protected function requestShouldReceiveFreshToken($request)
  42. {
  43. return $request->isMethod('GET') && $request->user($this->guard);
  44. }
  45. protected function responseShouldReceiveFreshToken($response)
  46. {
  47. return $response instanceof Response && ! $this->alreadyContainsToken($response);
  48. }
  49. }

这个中间件发出的 JWT 令牌仍然由 auth:api 来负责验证,我们前面说过,TokenGuard 负责两种令牌的验证,一种是 BearerToken, 另一种就是这个 Cookie :

  1. public function user(Request $request)
  2. {
  3. if ($request->bearerToken()) {
  4. return $this->authenticateViaBearerToken($request);
  5. } elseif ($request->cookie(Passport::cookie())) {
  6. return $this->authenticateViaCookie($request);
  7. }
  8. }
  9. protected function authenticateViaCookie($request)
  10. {
  11. try {
  12. $token = $this->decodeJwtTokenCookie($request);
  13. } catch (Exception $e) {
  14. return;
  15. }
  16. if (! $this->validCsrf($token, $request) ||
  17. time() >= $token['expiry']) {
  18. return;
  19. }
  20. if ($user = $this->provider->retrieveById($token['sub'])) {
  21. return $user->withAccessToken(new TransientToken);
  22. }
  23. }
  24. protected function decodeJwtTokenCookie($request)
  25. {
  26. return (array) JWT::decode(
  27. $this->encrypter->decrypt($request->cookie(Passport::cookie())),
  28. $this->encrypter->getKey(), ['HS256']
  29. );
  30. }
  31. protected function validCsrf($token, $request)
  32. {
  33. return isset($token['csrf']) && hash_equals(
  34. $token['csrf'], (string) $request->header('X-CSRF-TOKEN')
  35. );
  36. }