OAuth2.0 与 oauth2-server 库的使用

OAuth2.0 是关于授权的开放网络标准,它允许用户已第三方应用获取该用户在某一网站的私密资源,而无需提供用户名与密码,目前已在全世界得到广泛应用。

league/oauth2-server 是一个轻量级并且功能强大的符合 OAuth2.0 协议的 PHP 库,使用它可以构建出标准的 OAuth2.0 授权服务器。

本文通过对 PHP 库:league/oauth2-server 进行实践的同时,理解 OAuth2.0 的工作流程与设计思路。

术语

了解 OAuth2.0 与 oauth2-server 的专用术语,对于理解后面内容很有帮助。

OAuth2.0 定义了四个角色

  1. Client:客户端,第三方应用程序。
  2. Resource Owner:资源所有者,授权 Client 访问其帐户的用户。
  3. Authorization server:授权服务器,服务商专用于处理用户授权认证的服务器。
  4. Resource server:资源服务器,服务商用于存放用户受保护资源的服务器,它可以与授权服务器是同一台服务器,也可以是不同的服务器。

oauth2-server

  1. Access token:用于访问受保护资源的令牌。
  2. Authorization code:发放给应用程序的中间令牌,客户端应用使用此令牌交换 access token。
  3. Scope:授予应用程序的权限范围。
  4. JWTJson Web Token 是一种用于安全传输的数据传输格式。

运行流程

flowchart.png

安装

推荐使用 Composer 进行安装:

composer require league/oauth2-server

根据授权模式的不同,oauth2-server 提供了不同的 Interface 与 Triat 帮助实现。

本文发布时,版本号为7.3.1。

生成公钥与私钥

公钥与私钥用于签名和验证传输的 JWT,授权服务器使用私钥签名 JWT,资源服务器拥有公钥验证 JWT。

oauth2-server 使用 JWT 传输访问令牌(access token),方便资源服务器获取其中内容,所以需要使用非对称加密。

生成私钥,在终端中运行:

openssl genrsa -out private.key 2048

使用私钥提取私钥:

openssl rsa -in private.key -pubout -out public.key

私钥必须保密于授权服务器中,并将公钥分发给资源服务器。

生成加密密钥

加密密钥用于加密授权码(auth code)与刷新令牌(refesh token),AuthorizationServer(授权服务器启动类)接受两种加密密钥,stringdefuse/php-encryption 库的对象。

加密授权码(auth code)与刷新令牌(refesh token)只有授权权服务器使用,所以使用对称加密。

生成字符串密钥,在终端中输入:

php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'

生成对象,在项目根目录的终端中输入:

vendor/bin/generate-defuse-key

将获得的内容,传入 AuthorizationServer:

use \Defuse\Crypto\Key;
$server = new AuthorizationServer(
    $clientRepository,
    $accessTokenRepository,
    $scopeRepository,
    $privateKeyPath,
    Key::loadFromAsciiSafeString($encryptionKey) //传入加密密钥
);

PHP版本支持

  • PHP 7.0
  • PHP 7.1
  • PHP 7.2

授权模式

OAuth2.0 定义了四种授权模式,以应对不同情况时的授权。

  1. 授权码模式
  2. 隐式授权模式
  3. 密码模式
  4. 客户端模式

客户端类型

  • 保密的:
    • 客户端可以安全的存储自己与用户的凭据(例如:有所属的服务器端)
  • 公开的:
    • 客户端无法安全的存储自己与用户的凭据(例如:运行在浏览器的单页应用)

选用哪种授权模式?

如果客户端是保密的,应使用授权码模式

如果客户端是公开的,应使用隐式授权模式

如果用户对于此客户端高度信任(例如:第一方应用程序或操作系统程序),应使用密码模式

如果客户端是以自己的名义,不与用户产生关系,应使用客户端模式

预先注册

客户端需要预先在授权服务器进行注册,用以获取 client_idclient_secret,也可以在注册是预先设定好 redirect_uri,以便于之后可以使用默认的 redirect_uri

授权码模式

授权码模式是 OAuth2.0 种功能最完整,流程最严密的一种模式,如果你使用过 Google 或 QQ 登录过第三方应用程序,应该会对这个流程的第一部分很熟悉。

流程

第一部分(用户可见)

用户访问客户端,客户端将用户导向授权服务器时,将以下参数通过 GET query 传入:

  • response_type:授权类型,必选项,值固定为:code
  • client_id:客户端ID,必选项
  • redirect_uri:重定向URI,可选项,不填写时默认预先注册的重定向URI
  • scope:权限范围,可选项,以空格分隔
  • stateCSRF令牌,可选项,但强烈建议使用,应将该值存储与用户会话中,以便在返回时验证

用户选择是否给予客户端授权

假设用户给予授权,授权服务器将用户导向客户端事先指定的 redirect_uri,并将以下参数通过 GET query 传入:

  • code:授权码(Authorization code)
  • state:请求中发送的 state,原样返回。客户端将此值与用户会话中的值进行对比,以确保授权码响应的是此客户端而非其他客户端程序

第二部分(用户不可见)

客户端已得到授权,通过 POST 请求向授权服务器获取访问令牌(access token):

  • grant_type:授权模式,值固定为:authorization_code
  • client_id:客户端ID
  • client_secret:客户端 secret
  • redirect_uri:使用与第一部分请求相同的 URI
  • code:第一部分所获的的授权码,要注意URL解码

授权服务器核对授权码与重定向 URI,确认无误后,向客户端响应下列内容:

  • token_type:令牌类型,值固定为:Bearer

  • expires_in:访问令牌的存活时间

  • access_token:访问令牌

  • refresh_token:刷新令牌,访问令牌过期后,使用刷新令牌重新获取

使用 oauth2-server 实现

初始化

OAuth2.0 只是协议,在实现上需要联系到用户与数据库存储,oauth2-server 的新版本并没有指定某种数据库,但它提供了 InterfacesTraits 帮助我们实现,这让我们可以方便的使用任何形式的数据存储方式,这种方便的代价就是需要我们自行创建 RepositoriesEntities

初始化 server
// 初始化存储库
$clientRepository = new ClientRepository(); // Interface: ClientRepositoryInterface
$scopeRepository = new ScopeRepository(); // Interface: ScopeRepositoryInterface
$accessTokenRepository = new AccessTokenRepository(); // Interface: AccessTokenRepositoryInterface
$authCodeRepository = new AuthCodeRepository(); // Interface: AuthCodeRepositoryInterface
$refreshTokenRepository = new RefreshTokenRepository(); // Interface: RefreshTokenRepositoryInterface
$userRepository = new UserRepository(); //Interface: UserRepositoryInterface

// 私钥与加密密钥
$privateKey = 'file://path/to/private.key';
//$privateKey = new CryptKey('file://path/to/private.key', 'passphrase'); // 如果私钥文件有密码
$encryptionKey = 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen'; // 加密密钥字符串
// $encryptionKey = Key::loadFromAsciiSafeString($encryptionKey); //如果通过 generate-defuse-key 脚本生成的字符串,可使用此方法传入

// 初始化 server
$server = new \League\OAuth2\Server\AuthorizationServer(
    $clientRepository,
    $accessTokenRepository,
    $scopeRepository,
    $privateKey,
    $encryptionKey
);
初始化授权码类型
// 授权码授权类型初始化
$grant = new \League\OAuth2\Server\Grant\AuthCodeGrant(
    $authCodeRepository,
    $refreshTokenRepository,
    new \DateInterval('PT10M') // 设置授权码过期时间为10分钟
);
$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 设置刷新令牌过期时间1个月

// 将授权码授权类型添加进 server
$server->enableGrantType(
    $grant,
    new \DateInterval('PT1H') // 设置访问令牌过期时间1小时
);

DateInterval

使用

注意:这里的示例演示的是 Slim Framework 的用法,Slim 不是这个库的必要条件,只需要请求与响应符合PSR-7规范即可。

用户向客户端提出 OAuth 登录请求,客户端将用户重定向授权服务器的地址(例如:https://example.com/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&scope{scope}&state={state}):

$app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
   
    try {
        // 验证 HTTP 请求,并返回 authRequest 对象
        $authRequest = $server->validateAuthorizationRequest($request);
        // 此时应将 authRequest 对象序列化后存在当前会话(session)中
        $_SESSION['authRequest'] = serialize($authRequest);
        // 然后将用户重定向至登录入口或在当前地址直接响应登录页面
        return $response->getBody()->write(file_get_contents("login.html"));
        
    } catch (OAuthServerException $exception) {
        // 可以捕获 OAuthServerException,将其转为 HTTP 响应
        return $exception->generateHttpResponse($response);
        
    } catch (\Exception $exception) {
        // 其他异常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
        
    }
});

此时展示给用户的是这样的页面:


qq-oauth.png

用户提交登录后,设置好用户实体(userEntity):

$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
    try {
        // 在会话(session)中取出 authRequest 对象
        $authRequest = unserialize($_SESSION['authRequest']);
        // 设置用户实体(userEntity)
        $authRequest->setUser(new UserEntity(1));
        // 设置权限范围
        $authRequest->setScopes(['basic'])
        // true = 批准,false = 拒绝
        $authRequest->setAuthorizationApproved(true);
        // 完成后重定向至客户端请求重定向地址
        return $server->completeAuthorizationRequest($authRequest, $response);
    } catch (OAuthServerException $exception) {
        // 可以捕获 OAuthServerException,将其转为 HTTP 响应
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        // 其他异常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

客户端通过授权码请求访问令牌:

$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {

    try {
        // 这里只需要这一行就可以,具体的判断在 Repositories 中
        return $server->respondToAccessTokenRequest($request, $response);
    } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

隐式授权模式

隐式授权相当于是授权码模式的简化版本:

流程(用户可见)

用户访问客户端,客户端将用户导向授权服务器时,将以下参数通过 GET query 传入:

  • response_type:授权类型,必选项,值固定为:token
  • client_id:客户端ID,必选项
  • redirect_uri:重定向URI,可选项,不填写时默认预先注册的重定向URI
  • scope:权限范围,可选项,以空格分隔
  • stateCSRF令牌,可选项,但强烈建议使用,应将该值存储与用户会话中,以便在返回时验证

用户选择是否给予客户端授权

假设用户给予授权,授权服务器将用户导向客户端事先指定的 redirect_uri,并将以下参数通过 GET query 传入:

  • token_type:令牌类型,值固定为:Bearer
  • expires_in:访问令牌的存活时间
  • access_token:访问令牌
  • state:请求中发送的 state,原样返回。客户端将此值与用户会话中的值进行对比,以确保授权码响应的是此应用程序而非其他应用程序

整个流程与授权码模式的第一部分类似,只是授权服务器直接响应了访问令牌,跳过了授权码的步骤。它适用于没有服务器,完全运行在前端的应用程序。

此模式下没有刷新令牌(refresh token)的返回。

使用 oauth2-server 实现

初始化 server

初始化授权码类型
// 将隐式授权类型添加进 server
$server->enableGrantType(
    new ImplicitGrant(new \DateInterval('PT1H')),
    new \DateInterval('PT1H') // 设置访问令牌过期时间1小时
);

DateInterval

使用

注意:这里的示例演示的是 Slim Framework 的用法,Slim 不是这个库的必要条件,只需要请求与响应符合PSR-7规范即可。

$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
    try {
        // 在会话(session)中取出 authRequest 对象
        $authRequest = unserialize($_SESSION['authRequest']);
        // 设置用户实体(userEntity)
        $authRequest->setUser(new UserEntity(1));
        // 设置权限范围
        $authRequest->setScopes(['basic'])
        // true = 批准,false = 拒绝
        $authRequest->setAuthorizationApproved(true);
        // 完成后重定向至客户端请求重定向地址
        return $server->completeAuthorizationRequest($authRequest, $response);
    } catch (OAuthServerException $exception) {
        // 可以捕获 OAuthServerException,将其转为 HTTP 响应
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        // 其他异常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

此时展示给用户的是这样的页面:


qq-oauth.png

用户提交登录后,设置好用户实体(userEntity):

$app->post('/login', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {
   
    try {
          // 在会话(session)中取出 authRequest 对象
        $authRequest = unserialize($_SESSION['authRequest']);
                // 设置用户实体(userEntity)
            $authRequest->setUser(new UserEntity(1));
                // 设置权限范围
                $authRequest->setScopes(['basic'])
                // true = 批准,false = 拒绝
            $authRequest->setAuthorizationApproved(true);
                // 完成后重定向至客户端请求重定向地址
            return $server->completeAuthorizationRequest($authRequest, $response);
    } catch (OAuthServerException $exception) {
        // 可以捕获 OAuthServerException,将其转为 HTTP 响应
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        // 其他异常
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

密码模式

密码模式是由用户提供给客户端账号密码来获取访问令牌,这属于危险行为,所以此模式只适用于高度信任的客户端(例如第一方应用程序)。客户端不应存储用户的账号密码。

OAuth2 协议规定此模式不需要传 client_id & client_secret,但 oauth-server 库需要

流程

客户端要求用户提供授权凭据,通常是账号密码

然后,客户端发送 POST 请求至授权服务器,携带以下参数:

  • grant_type:授权类型,必选项,值固定为:password
  • client_id:客户端ID,必选项
  • client_secret:客户端 secret
  • scope:权限范围,可选项,以空格分隔
  • username:用户账号
  • password:用户密码

授权服务器响应以下内容:

  • token_type:令牌类型,值固定为:Bearer
  • expires_in:访问令牌的存活时间
  • access_token:访问令牌
  • refresh_token:刷新令牌,访问令牌过期后,使用刷新令牌重新获取

使用 oauth2-server 实现

初始化 server

初始化授权码类型
$grant = new \League\OAuth2\Server\Grant\PasswordGrant(
     $userRepository,
     $refreshTokenRepository
);

$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 设置刷新令牌过期时间1个月

// 将密码授权类型添加进 server
$server->enableGrantType(
    $grant,
    new \DateInterval('PT1H') // 设置访问令牌过期时间1小时
);

DateInterval

使用

注意:这里的示例演示的是 Slim Framework 的用法,Slim 不是这个库的必要条件,只需要请求与响应符合PSR-7规范即可。

$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {

    try {
        // 这里只需要这一行就可以,具体的判断在 Repositories 中
        return $server->respondToAccessTokenRequest($request, $response);
    } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

客户端模式

客户端模式是指以客户端的名义,而不是用户的名义,向授权服务器获取认证。在这个模式下,用户与授权服务器不产生关系,用户只能感知到的客户端,所产生的资源也都由客户端处理。

流程

客户端发送 POST 请求至授权服务器,携带以下参数:

  • grant_type:授权类型,必选项,值固定为:client_credentials
  • client_id:客户端ID,必选项
  • client_secret:客户端 secret
  • scope:权限范围,可选项,以空格分隔

授权服务器响应以下内容:

  • token_type:令牌类型,值固定为:Bearer
  • expires_in:访问令牌的存活时间
  • access_token:访问令牌

此模式下无需刷新令牌(refresh token)的返回。

使用 oauth2-server 实现

初始化 server

初始化授权码类型
// 将客户端授权类型添加进 server
$server->enableGrantType(
    new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
    new \DateInterval('PT1H') // 设置访问令牌过期时间1小时
);

DateInterval

使用

注意:这里的示例演示的是 Slim Framework 的用法,Slim 不是这个库的必要条件,只需要请求与响应符合PSR-7规范即可。

$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {

    try {
        // 这里只需要这一行就可以,具体的判断在 Repositories 中
        return $server->respondToAccessTokenRequest($request, $response);
    } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

刷新访问令牌(access token)

访问令牌有一个较短的存活时间,在过期后,客户端通过刷新令牌来获得新的访问令牌与刷新令牌。当用户长时间不活跃,刷新令牌也过期后,就需要重新获取授权。

流程

客户端发送 POST 请求至授权服务器,携带以下参数:

  • grant_type:授权类型,必选项,值固定为:refresh_token
  • client_id:客户端ID,必选项
  • client_secret:客户端 secret
  • scope:权限范围,可选项,以空格分隔
  • refresh_token:刷新令牌

授权服务器响应以下内容:

  • token_type:令牌类型,值固定为:Bearer
  • expires_in:访问令牌的存活时间
  • access_token:访问令牌
  • refresh_token:刷新令牌,访问令牌过期后,使用刷新令牌重新获取

使用 oauth2-server 实现

初始化 server

初始化授权码类型
$grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant($refreshTokenRepository);
$grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 新的刷新令牌过期时间1个月

// 将刷新访问令牌添加进 server
$server->enableGrantType(
    $grant,
    new \DateInterval('PT1H') // 新的访问令牌过期时间1小时
);

DateInterval

使用
$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($server) {

    try {
        // 这里只需要这一行就可以,具体的判断在 Repositories 中
        return $server->respondToAccessTokenRequest($request, $response);
    } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) {
        return $exception->generateHttpResponse($response);
    } catch (\Exception $exception) {
        $body = new Stream(fopen('php://temp', 'r+'));
        $body->write($exception->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

资源服务器验证访问令牌

oauth2-server 为资源服务器提供了一个中间件用于验证访问令牌。

客户端需要在 HTTP Header 中使用 Authorization 传入访问令牌,如果通过,中间件将会在 request 中加入对应数据:

  • oauth_access_token_id:访问令牌 id
  • oauth_client_id: 客户端id
  • oauth_user_id:用户id
  • oauth_scopes:权限范围

授权不通过,则抛出 OAuthServerException::accessDenied 异常。

// 初始化
$accessTokenRepository = new AccessTokenRepository(); // Interface: AccessTokenRepositoryInterface

// 授权服务器分发的公钥
$publicKeyPath = 'file://path/to/public.key';
        
// 创建 ResourceServer
$server = new \League\OAuth2\Server\ResourceServer(
    $accessTokenRepository,
    $publicKeyPath
);

// 中间件
new \League\OAuth2\Server\Middleware\ResourceServerMiddleware($server);

如果所用路由不支持中间件,可自行实现,符合PSR-7规范即可 :

try {
    $request = $server->validateAuthenticatedRequest($request);
} catch (OAuthServerException $exception) {
    return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
    return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))->generateHttpResponse($response);
}

oauth2-server 实现

oauth2-server 的实现需要我们手动创建 RepositoriesEntities,下面展示一个项目目录示例:

- Entities
    - AccessTokenEntity.php
    - AuthCodeEntity.php
    - ClientEntity.php
    - RefreshTokenEntity.php
    - ScopeEntity.php
    - UserEntity.php
- Repositories
    - AccessTokenRepository.php
    - AuthCodeRepository.php
    - ClientRepository.php
    - RefreshTokenRepository.php
    - ScopeRepository.php
    - UserRepository.php

Repositories

Repositories 里主要是处理关于授权码、访问令牌等数据的存储逻辑,oauth2-server 提供了 Interfaces 来定义所需要实现的方法。

class AccessTokenRepository implements AccessTokenRepositoryInterface
{
    /**
     * @return AccessTokenEntityInterface
     */
    public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
    {
        // 创建新访问令牌时调用方法
        // 需要返回 AccessTokenEntityInterface 对象
        // 需要在返回前,向 AccessTokenEntity 传入参数中对应属性
        // 示例代码:
        $accessToken = new AccessTokenEntity();
        $accessToken->setClient($clientEntity);
        foreach ($scopes as $scope) {
            $accessToken->addScope($scope);
        }
        $accessToken->setUserIdentifier($userIdentifier);

        return $accessToken;
    }

    public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity)
    {
        // 创建新访问令牌时调用此方法
        // 可以用于持久化存储访问令牌,持久化数据库自行选择
        // 可以使用参数中的 AccessTokenEntityInterface 对象,获得有价值的信息:
        // $accessTokenEntity->getIdentifier(); // 获得令牌唯一标识符
        // $accessTokenEntity->getExpiryDateTime(); // 获得令牌过期时间
        // $accessTokenEntity->getUserIdentifier(); // 获得用户标识符
        // $accessTokenEntity->getScopes(); // 获得权限范围
        // $accessTokenEntity->getClient()->getIdentifier(); // 获得客户端标识符
    }

    public function revokeAccessToken($tokenId)
    {
        // 使用刷新令牌创建新的访问令牌时调用此方法
        // 参数为原访问令牌的唯一标识符
        // 可将其在持久化存储中过期
    }

    public function isAccessTokenRevoked($tokenId)
    {
        // 资源服务器验证访问令牌时将调用此方法
        // 用于验证访问令牌是否已被删除
        // return true 已删除,false 未删除
        return false;
    }
}
class AuthCodeRepository implements AuthCodeRepositoryInterface
{
    /**
     * @return AuthCodeEntityInterface
     */
    public function getNewAuthCode()
    {
        // 创建新授权码时调用方法
        // 需要返回 AuthCodeEntityInterface 对象
        return new AuthCodeEntity();
    }

    public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity)
    {
        // 创建新授权码时调用此方法
        // 可以用于持久化存储授权码,持久化数据库自行选择
        // 可以使用参数中的 AuthCodeEntityInterface 对象,获得有价值的信息:
        // $authCodeEntity->getIdentifier(); // 获得授权码唯一标识符
        // $authCodeEntity->getExpiryDateTime(); // 获得授权码过期时间
        // $authCodeEntity->getUserIdentifier(); // 获得用户标识符
        // $authCodeEntity->getScopes(); // 获得权限范围
        // $authCodeEntity->getClient()->getIdentifier(); // 获得客户端标识符
    }

    public function revokeAuthCode($codeId)
    {
        // 当使用授权码获取访问令牌时调用此方法
        // 可以在此时将授权码从持久化数据库中删除
        // 参数为授权码唯一标识符
    }

    public function isAuthCodeRevoked($codeId)
    {
        // 当使用授权码获取访问令牌时调用此方法
        // 用于验证授权码是否已被删除
        // return true 已删除,false 未删除
        return false;
    }
}
class ClientRepository implements ClientRepositoryInterface
{
    /**
     * @return ClientEntityInterface
     */
    public function getClientEntity($clientIdentifier, $grantType = null, $clientSecret = null, $mustValidateSecret = true)
    {
        // 获取客户端对象时调用方法,用于验证客户端
        // 需要返回 ClientEntityInterface 对象
        // $clientIdentifier 客户端唯一标识符
        // $grantType 代表授权类型,根据类型不同,验证方式也不同
        // $clientSecret 代表客户端密钥,是客户端事先在授权服务器中注册时得到的
        // $mustValidateSecret 代表是否需要验证客户端密钥
        $client = new ClientEntity();
        $client->setIdentifier($clientIdentifier);

        return $client;
    }
}
class RefreshTokenRepository implements RefreshTokenRepositoryInterface
{
    /**
     * @return RefreshTokenEntityInterface
     */
    public function getNewRefreshToken()
    {
        // 创建新授权码时调用方法
        // 需要返回 RefreshTokenEntityInterface 对象
        return new RefreshTokenEntity();
    }

    public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity)
    {
        // 创建新刷新令牌时调用此方法
        // 用于持久化存储授刷新令牌
        // 可以使用参数中的 RefreshTokenEntityInterface 对象,获得有价值的信息:
        // $refreshTokenEntity->getIdentifier(); // 获得刷新令牌唯一标识符
        // $refreshTokenEntity->getExpiryDateTime(); // 获得刷新令牌过期时间
        // $refreshTokenEntity->getAccessToken()->getIdentifier(); // 获得访问令牌标识符
    }

    public function revokeRefreshToken($tokenId)
    {
        // 当使用刷新令牌获取访问令牌时调用此方法
        // 原刷新令牌将删除,创建新的刷新令牌
        // 参数为原刷新令牌唯一标识
        // 可在此删除原刷新令牌
    }

    public function isRefreshTokenRevoked($tokenId)
    {
        // 当使用刷新令牌获取访问令牌时调用此方法
        // 用于验证刷新令牌是否已被删除
        // return true 已删除,false 未删除
        return false;
    }
}
class ScopeRepository implements ScopeRepositoryInterface
{
    /**
     * @return ScopeEntityInterface
     */
    public function getScopeEntityByIdentifier($identifier)
    {
        // 验证权限是否在权限范围中会调用此方法
        // 参数为单个权限标识符
        // ......
        // 验证成功则返回 ScopeEntityInterface 对象
        $scope = new ScopeEntity();
        $scope->setIdentifier($identifier);

        return $scope;
    }

    public function finalizeScopes(
        array $scopes,
        $grantType,
        ClientEntityInterface $clientEntity,
        $userIdentifier = null
    ) {
        // 在创建授权码与访问令牌前会调用此方法
        // 用于验证权限范围、授权类型、客户端、用户是否匹配
        // 可整合进项目自身的权限控制中
        // 必须返回 ScopeEntityInterface 对象可用的 scope 数组
        // 示例:
        // $scope = new ScopeEntity();
        // $scope->setIdentifier('example');
        // $scopes[] = $scope;

        return $scopes;
    }
}
class UserRepository implements UserRepositoryInterface
{
    /**
     * @return UserEntityInterface
     */
    public function getUserEntityByUserCredentials(
        $username,
        $password,
        $grantType,
        ClientEntityInterface $clientEntity
    ) {
        // 验证用户时调用此方法
        // 用于验证用户信息是否符合
        // 可以验证是否为用户可使用的授权类型($grantType)与客户端($clientEntity)
        // 验证成功返回 UserEntityInterface 对象
        $user = new UserEntity();
        $user->setIdentifier(1);

        return $user;
    }
}

Entities

Entities 里是 oauth2-server 处理授权与认证逻辑的类,它为我们提供了 Interfaces 来定义需要实现的方法,同时提供了 Traits 帮助我们实现,可以选择使用,有需要时也可以重写。

class AccessTokenEntity implements AccessTokenEntityInterface
{
    use AccessTokenTrait, TokenEntityTrait, EntityTrait;
}
class AuthCodeEntity implements AuthCodeEntityInterface
{
    use EntityTrait, TokenEntityTrait, AuthCodeTrait;
}
class ClientEntity implements ClientEntityInterface
{
    use EntityTrait, ClientTrait;
}
class RefreshTokenEntity implements RefreshTokenEntityInterface
{
    use RefreshTokenTrait, EntityTrait;
}
class ScopeEntity implements ScopeEntityInterface
{
    use EntityTrait;
    
    // 没有 Trait 实现这个方法,需要自行实现
    // oauth2-server 项目的测试代码的实现例子
    public function jsonSerialize()
    {
        return $this->getIdentifier();
    }
}
class UserEntity implements UserEntityInterface
{
    use EntityTrait;
}

Interfaces

Repositories

  • League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface.php

  • League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface.php

  • League\OAuth2\Server\Repositories\ClientRepositoryInterface.php

  • League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface.php

  • League\OAuth2\Server\Repositories\ScopeRepositoryInterface.php

  • League\OAuth2\Server\Repositories\UserRepositoryInterface.php

Entities

  • League\OAuth2\Server\Entities\AccessTokenEntityInterface.php
  • League\OAuth2\Server\Entities\AuthCodeEntityInterface.php
  • League\OAuth2\Server\Entities\ClientEntityInterface.php
  • League\OAuth2\Server\Entities\RefreshTokenEntityInterface.php
  • League\OAuth2\Server\Entities\ScopeEntityInterface.php
  • League\OAuth2\Server\Entities\TokenInterface.php
  • League\OAuth2\Server\Entities\UserEntityInterface.php

Traits

  • League\OAuth2\Server\Entities\Traits\AccessTokenTrait.php
  • League\OAuth2\Server\Entities\Traits\AuthCodeTrait.php
  • League\OAuth2\Server\Entities\Traits\ClientTrait.php
  • League\OAuth2\Server\Entities\Traits\EntityTrait.php
  • League\OAuth2\Server\Entities\Traits\RefreshTokenTrait.php
  • League\OAuth2\Server\Entities\Traits\ScopeTrait.php
  • League\OAuth2\Server\Entities\Traits\TokenEntityTrait.php

事件

oauth2-server 预设了一些事件,目前官方文档中只有两个,余下的可以在 RequestEvent.php 文件中查看。

client.authentication.failed
$server->getEmitter()->addListener(
    'client.authentication.failed',
    function (\League\OAuth2\Server\RequestEvent $event) {
        // do something
    }
);

客户端身份验证未通过时触发此事件。你可以在客户端尝试 n 次失败后禁止它一段时间内的再次尝试。

user.authentication.failed
$server->getEmitter()->addListener(
    'user.authentication.failed',
    function (\League\OAuth2\Server\RequestEvent $event) {
        // do something
    }
);

用户身份验证未通过时触发此事件。你可以通过这里提醒用户重置密码,或尝试 n 次后禁止用户再次尝试。

参考文章

《oauth2-server 官方文档》(https://oauth2.thephpleague.com/)

《理解OAuth 2.0》-阮一峰(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

推荐阅读更多精彩内容