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

栏目: IT技术 · 发布时间: 6年前

内容简介:Laravel Passport——OAuth2 API 认证系统源码解析(下)

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis

隐式授权

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

public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::enableImplicitGrant();
}

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

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

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

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

public function validateAuthorizationRequest(ServerRequestInterface $request)
{
    $clientId = $this->getQueryStringParameter(
        'client_id',
        $request,
        $this->getServerParameter('PHP_AUTH_USER', $request)
    );
    if (is_null($clientId)) {
        throw OAuthServerException::invalidRequest('client_id');
    }

    $client = $this->clientRepository->getClientEntity(
        $clientId,
        $this->getIdentifier(),
        null,
        false
    );

    if ($client instanceof ClientEntityInterface === false) {
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
        throw OAuthServerException::invalidClient();
    }

    $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);

    $scopes = $this->validateScopes(
        $this->getQueryStringParameter('scope', $request, $this->defaultScope),
        is_array($client->getRedirectUri())
            ? $client->getRedirectUri()[0]
            : $client->getRedirectUri()
    );

    // Finalize the requested scopes
    $finalizedScopes = $this->scopeRepository->finalizeScopes(
        $scopes,
        $this->getIdentifier(),
        $client
    );

    $stateParameter = $this->getQueryStringParameter('state', $request);

    $authorizationRequest = new AuthorizationRequest();
    $authorizationRequest->setGrantTypeId($this->getIdentifier());
    $authorizationRequest->setClient($client);
    $authorizationRequest->setRedirectUri($redirectUri);
    $authorizationRequest->setState($stateParameter);
    $authorizationRequest->setScopes($finalizedScopes);

    return $authorizationRequest;
}

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

public function __construct(\DateInterval $accessTokenTTL, $queryDelimiter = '#')
{
    $this->accessTokenTTL = $accessTokenTTL;
    $this->queryDelimiter = $queryDelimiter;
}

public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
    if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
        throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
    }

    $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
        ? is_array($authorizationRequest->getClient()->getRedirectUri())
            ? $authorizationRequest->getClient()->getRedirectUri()[0]
            : $authorizationRequest->getClient()->getRedirectUri()
        : $authorizationRequest->getRedirectUri();

    // The user approved the client, redirect them back with an access token
    if ($authorizationRequest->isAuthorizationApproved() === true) {
        $accessToken = $this->issueAccessToken(
            $this->accessTokenTTL,
            $authorizationRequest->getClient(),
            $authorizationRequest->getUser()->getIdentifier(),
            $authorizationRequest->getScopes()
        );

        $response = new RedirectResponse();
        $response->setRedirectUri(
            $this->makeRedirectUri(
                $finalRedirectUri,
                [
                    'access_token' => (string) $accessToken->convertToJWT($this->privateKey),
                    'token_type'   => 'Bearer',
                    'expires_in'   => $accessToken->getExpiryDateTime()->getTimestamp() - (new \DateTime())->getTimestamp(),
                    'state'        => $authorizationRequest->getState(),
                ],
                $this->queryDelimiter
            )
        );

        return $response;
    }

    // The user denied the client, redirect them back with an error
    throw OAuthServerException::accessDenied(
        'The user denied the request',
        $this->makeRedirectUri(
            $finalRedirectUri,
            [
                'state' => $authorizationRequest->getState(),
            ]
        )
    );
}

这个用于构建 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

public function convertToJWT(CryptKey $privateKey)
{
    return (new Builder())
        ->setAudience($this->getClient()->getIdentifier())
        ->setId($this->getIdentifier(), true)
        ->setIssuedAt(time())
        ->setNotBefore(time())
        ->setExpiration($this->getExpiryDateTime()->getTimestamp())
        ->setSubject($this->getUserIdentifier())
        ->set('scopes', $this->getScopes())
        ->sign(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()))
        ->getToken();
}

public function __construct(
    Encoder $encoder = null,
    ClaimFactory $claimFactory = null
) {
    $this->encoder = $encoder ?: new Encoder();
    $this->claimFactory = $claimFactory ?: new ClaimFactory();
    $this->headers = ['typ'=> 'JWT', 'alg' => 'none'];
    $this->claims = [];
}

public function setAudience($audience, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('aud', (string) $audience, $replicateAsHeader);
}

public function setId($id, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('jti', (string) $id, $replicateAsHeader);
}

public function setIssuedAt($issuedAt, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('iat', (int) $issuedAt, $replicateAsHeader);
}

public function setNotBefore($notBefore, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('nbf', (int) $notBefore, $replicateAsHeader);
}

public function setExpiration($expiration, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('exp', (int) $expiration, $replicateAsHeader);
}

public function setSubject($subject, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('sub', (string) $subject, $replicateAsHeader);
}

public function sign(Signer $signer, $key)
{
    $signer->modifyHeader($this->headers);

    $this->signature = $signer->sign(
        $this->getToken()->getPayload(),
        $key
    );

    return $this;
}

public function getToken()
{
    $payload = [
        $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->headers)),
        $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->claims))
    ];

    if ($this->signature !== null) {
        $payload[] = $this->encoder->base64UrlEncode($this->signature);
    }

    return new Token($this->headers, $this->claims, $this->signature, $payload);
}

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

 

客户端模式

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

protected $routeMiddleware = [
    'client' => CheckClientCredentials::class,
];

Route::get('/user', function(Request $request) {
    ...
})->middleware('client');

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

$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',
    ],
]);

echo json_decode((string) $response->getBody(), true);

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

public function respondToAccessTokenRequest(
    ServerRequestInterface $request,
    ResponseTypeInterface $responseType,
    \DateInterval $accessTokenTTL
) {
    // Validate request
    $client = $this->validateClient($request);
    $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));

    // Finalize the requested scopes
    $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client);

    // Issue and persist access token
    $accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes);

    // Inject access token into response type
    $responseType->setAccessToken($accessToken);

    return $responseType;
}

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

 

密码模式

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

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

$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);

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

public function respondToAccessTokenRequest(
    ServerRequestInterface $request,
    ResponseTypeInterface $responseType,
    \DateInterval $accessTokenTTL
) {
    // Validate request
    $client = $this->validateClient($request);
    $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
    $user = $this->validateUser($request, $client);

    // Finalize the requested scopes
    $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());

    // Issue and persist new tokens
    $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
    $refreshToken = $this->issueRefreshToken($accessToken);

    // Inject tokens into response
    $responseType->setAccessToken($accessToken);
    $responseType->setRefreshToken($refreshToken);

    return $responseType;
}

protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
{
    $username = $this->getRequestParameter('username', $request);
    if (is_null($username)) {
        throw OAuthServerException::invalidRequest('username');
    }

    $password = $this->getRequestParameter('password', $request);
    if (is_null($password)) {
        throw OAuthServerException::invalidRequest('password');
    }

    $user = $this->userRepository->getUserEntityByUserCredentials(
        $username,
        $password,
        $this->getIdentifier(),
        $client
    );
    if ($user instanceof UserEntityInterface === false) {
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));

        throw OAuthServerException::invalidCredentials();
    }

    return $user;
}

 

路由保护

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

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

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

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

 

auth:api 中间件

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


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

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


class Authenticate
{
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($guards);

        return $next($request);
    }

    protected function authenticate(array $guards)
    {
        if (empty($guards)) {
            return $this->auth->authenticate();
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        throw new AuthenticationException('Unauthenticated.', $guards);
    }
}

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


class AuthManager implements FactoryContract
{
    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
    }

    protected function resolve($name)
    {
        $config = $this->getConfig($name);

        if (is_null($config)) {
            throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
        }

        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($name, $config);
        }

        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($name, $config);
        }

        throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined.");
    }

}

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


'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

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

protected function registerGuard()
{
    Auth::extend('passport', function ($app, $name, array $config) {
        return tap($this->makeGuard($config), function ($guard) {
            $this->app->refresh('request', $guard, 'setRequest');
        });
    });
}

protected function makeGuard(array $config)
{
    return new RequestGuard(function ($request) use ($config) {
        return (new TokenGuard(
            $this->app->make(ResourceServer::class),
            Auth::createUserProvider($config['provider']),
            $this->app->make(TokenRepository::class),
            $this->app->make(ClientRepository::class),
            $this->app->make('encrypter')
        ))->user($request);
    }, $this->app['request']);
}

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

class TokenGuard
{
    public function __construct(ResourceServer $server,
                                UserProvider $provider,
                                TokenRepository $tokens,
                                ClientRepository $clients,
                                Encrypter $encrypter)
    {
        $this->server = $server;
        $this->tokens = $tokens;
        $this->clients = $clients;
        $this->provider = $provider;
        $this->encrypter = $encrypter;
    }

    public function user(Request $request)
    {
        if ($request->bearerToken()) {
            return $this->authenticateViaBearerToken($request);
        } elseif ($request->cookie(Passport::cookie())) {
            return $this->authenticateViaCookie($request);
        }
    }
}

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

我们首先看 BearerToken:

public function bearerToken()
{
    $header = $this->header('Authorization', '');

    if (Str::startsWith($header, 'Bearer ')) {
        return Str::substr($header, 7);
    }
}

protected function authenticateViaBearerToken($request)
{
    $psr = (new DiactorosFactory)->createRequest($request);

    try {
        $psr = $this->server->validateAuthenticatedRequest($psr);

        $user = $this->provider->retrieveById(
            $psr->getAttribute('oauth_user_id')
        );

        if (! $user) {
            return;
        }

        $token = $this->tokens->find(
            $psr->getAttribute('oauth_access_token_id')
        );

        $clientId = $psr->getAttribute('oauth_client_id');

        if ($this->clients->revoked($clientId)) {
            return;
        }

        return $token ? $user->withAccessToken($token) : null;
    } catch (OAuthServerException $e) {
        return Container::getInstance()->make(
            ExceptionHandler::class
        )->report($e);
    }
}

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

class ResourceServer
{
    public function validateAuthenticatedRequest(ServerRequestInterface $request)
    {
        return $this->getAuthorizationValidator()->validateAuthorization($request);
    }

    protected function getAuthorizationValidator()
    {
        if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) {
            $this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository);
        }

        $this->authorizationValidator->setPublicKey($this->publicKey);

        return $this->authorizationValidator;
    }

}

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

class BearerTokenValidator implements AuthorizationValidatorInterface
{
    public function validateAuthorization(ServerRequestInterface $request)
    {
        if ($request->hasHeader('authorization') === false) {
            throw OAuthServerException::accessDenied('Missing "Authorization" header');
        }

        $header = $request->getHeader('authorization');
        $jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));

        try {
            // Attempt to parse and validate the JWT
            $token = (new Parser())->parse($jwt);
            if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
                throw OAuthServerException::accessDenied('Access token could not be verified');
            }

            // Ensure access token hasn't expired
            $data = new ValidationData();
            $data->setCurrentTime(time());

            if ($token->validate($data) === false) {
                throw OAuthServerException::accessDenied('Access token is invalid');
            }

            // Check if token has been revoked
            if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
                throw OAuthServerException::accessDenied('Access token has been revoked');
            }

            // Return the request with additional attributes
            return $request
                ->withAttribute('oauth_access_token_id', $token->getClaim('jti'))
                ->withAttribute('oauth_client_id', $token->getClaim('aud'))
                ->withAttribute('oauth_user_id', $token->getClaim('sub'))
                ->withAttribute('oauth_scopes', $token->getClaim('scopes'));
        } catch (\InvalidArgumentException $exception) {
            // JWT couldn't be parsed so return the request as is
            throw OAuthServerException::accessDenied($exception->getMessage());
        } catch (\RuntimeException $exception) {
            //JWR couldn't be parsed so return the request as is
            throw OAuthServerException::accessDenied('Error while decoding to JSON');
        }
    }
}

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

class Parser
{
    public function parse($jwt)
    {
        $data = $this->splitJwt($jwt);
        $header = $this->parseHeader($data[0]);
        $claims = $this->parseClaims($data[1]);
        $signature = $this->parseSignature($header, $data[2]);

        foreach ($claims as $name => $value) {
            if (isset($header[$name])) {
                $header[$name] = $value;
            }
        }

        if ($signature === null) {
            unset($data[2]);
        }

        return new Token($header, $claims, $signature, $data);
    }

    protected function splitJwt($jwt)
    {
        if (!is_string($jwt)) {
            throw new InvalidArgumentException('The JWT string must have two dots');
        }

        $data = explode('.', $jwt);

        if (count($data) != 3) {
            throw new InvalidArgumentException('The JWT string must have two dots');
        }

        return $data;
    }

    protected function parseHeader($data)
    {
        $header = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));

        if (isset($header['enc'])) {
            throw new InvalidArgumentException('Encryption is not supported yet');
        }

        return $header;
    }

    protected function parseClaims($data)
    {
        $claims = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));

        foreach ($claims as $name => &$value) {
            $value = $this->claimFactory->create($name, $value);
        }

        return $claims;
    }

    protected function parseSignature(array $header, $data)
    {
        if ($data == '' || !isset($header['alg']) || $header['alg'] == 'none') {
            return null;
        }

        $hash = $this->decoder->base64UrlDecode($data);

        return new Signature($hash);
    }

}

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

class Token
{
    public function verify(Signer $signer, $key)
    {
        if ($this->signature === null) {
            throw new BadMethodCallException('This token is not signed');
        }

        if ($this->headers['alg'] !== $signer->getAlgorithmId()) {
            return false;
        }

        return $this->signature->verify($signer, $this->getPayload(), $key);
    }
}

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

$data = new ValidationData();
$data->setCurrentTime(time());

public function __construct($currentTime = null)
{
    $currentTime = $currentTime ?: time();

    $this->items = [
        'jti' => null,
        'iss' => null,
        'aud' => null,
        'sub' => null,
        'iat' => $currentTime,
        'nbf' => $currentTime,
        'exp' => $currentTime
    ];
}

public function validate(ValidationData $data)
{
    foreach ($this->getValidatableClaims() as $claim) {
        if (!$claim->validate($data)) {
            return false;
        }
    }

    return true;
}    

public function __construct(array $callbacks = [])
{
    $this->callbacks = array_merge(
        [
            'iat' => [$this, 'createLesserOrEqualsTo'],
            'nbf' => [$this, 'createLesserOrEqualsTo'],
            'exp' => [$this, 'createGreaterOrEqualsTo'],
            'iss' => [$this, 'createEqualsTo'],
            'aud' => [$this, 'createEqualsTo'],
            'sub' => [$this, 'createEqualsTo'],
            'jti' => [$this, 'createEqualsTo']
        ],
        $callbacks
    );
}

我们前面说过,

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

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

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

if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
    throw OAuthServerException::accessDenied('Access token has been revoked');
}

public function isAccessTokenRevoked($tokenId)
{
    return $this->tokenRepository->isAccessTokenRevoked($tokenId);
}

public function isAccessTokenRevoked($id)
{
    if ($token = $this->find($id)) {
        return $token->revoked;
    }

    return true;
}

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

$user = $this->provider->retrieveById(
    $psr->getAttribute('oauth_user_id')
);

if (! $user) {
    return;
}

$token = $this->tokens->find(
    $psr->getAttribute('oauth_access_token_id')
);

$clientId = $psr->getAttribute('oauth_client_id');

if ($this->clients->revoked($clientId)) {
    return;
}

return $token ? $user->withAccessToken($token) : null;

中间件验证完成。

 

客户端模式中间件 CheckClientCredentials

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

class CheckClientCredentials
{
    public function handle($request, Closure $next, ...$scopes)
    {
        $psr = (new DiactorosFactory)->createRequest($request);

        try {
            $psr = $this->server->validateAuthenticatedRequest($psr);
        } catch (OAuthServerException $e) {
            throw new AuthenticationException;
        }

        $this->validateScopes($psr, $scopes);

        return $next($request);
    }
}

 

使用 JavaScript 接入 API

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

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

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

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

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

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

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

 

CreateFreshApiToken 中间件

class CreateFreshApiToken
{
    public function handle($request, Closure $next, $guard = null)
    {
        $this->guard = $guard;

        $response = $next($request);

        if ($this->shouldReceiveFreshToken($request, $response)) {
            $response->withCookie($this->cookieFactory->make(
                $request->user($this->guard)->getKey(), $request->session()->token()
            ));
        }

        return $response;
    }

    public function make($userId, $csrfToken)
    {
        $config = $this->config->get('session');

        $expiration = Carbon::now()->addMinutes($config['lifetime']);

        return new Cookie(
            Passport::cookie(),
            $this->createToken($userId, $csrfToken, $expiration),
            $expiration,
            $config['path'],
            $config['domain'],
            $config['secure'],
            true
        );
    }

    protected function createToken($userId, $csrfToken, Carbon $expiration)
    {
        return JWT::encode([
            'sub' => $userId,
            'csrf' => $csrfToken,
            'expiry' => $expiration->getTimestamp(),
        ], $this->encrypter->getKey());
    }

    protected function shouldReceiveFreshToken($request, $response)
    {
        return $this->requestShouldReceiveFreshToken($request) &&
               $this->responseShouldReceiveFreshToken($response);
    }

    protected function requestShouldReceiveFreshToken($request)
    {
        return $request->isMethod('GET') && $request->user($this->guard);
    }

    protected function responseShouldReceiveFreshToken($response)
    {
        return $response instanceof Response && ! $this->alreadyContainsToken($response);
    }
}

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

public function user(Request $request)
{
    if ($request->bearerToken()) {
        return $this->authenticateViaBearerToken($request);
    } elseif ($request->cookie(Passport::cookie())) {
        return $this->authenticateViaCookie($request);
    }
}

protected function authenticateViaCookie($request)
{
    try {
        $token = $this->decodeJwtTokenCookie($request);
    } catch (Exception $e) {
        return;
    }

    if (! $this->validCsrf($token, $request) ||
        time() >= $token['expiry']) {
        return;
    }

    if ($user = $this->provider->retrieveById($token['sub'])) {
        return $user->withAccessToken(new TransientToken);
    }
}

protected function decodeJwtTokenCookie($request)
{
    return (array) JWT::decode(
        $this->encrypter->decrypt($request->cookie(Passport::cookie())),
        $this->encrypter->getKey(), ['HS256']
    );
}

protected function validCsrf($token, $request)
{
    return isset($token['csrf']) && hash_equals(
        $token['csrf'], (string) $request->header('X-CSRF-TOKEN')
    );
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Don't Make Me Think

Don't Make Me Think

Steve Krug / New Riders Press / 18 August, 2005 / $35.00

Five years and more than 100,000 copies after it was first published, it's hard to imagine anyone working in Web design who hasn't read Steve Krug's "instant classic" on Web usability, but people are ......一起来看看 《Don't Make Me Think》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试