作者:baiyi
链接:https://www.jianshu.com/p/83b0f6d82d6c
來源:简书
OAuth2.0 是关于授权的开放网络标准,它允许用户已第三方应用获取该用户在某一网站的私密资源,而无需提供用户名与密码,目前已在全世界得到广泛应用。
league/oauth2-server 是一个轻量级并且功能强大的符合 OAuth2.0 协议的 PHP 库,使用它可以构建出标准的 OAuth2.0 授权服务器。
本文通过对 PHP 库:league/oauth2-server 进行实践的同时,理解 OAuth2.0 的工作流程与设计思路。
术语
了解 OAuth2.0 与 oauth2-server 的专用术语,对于理解后面内容很有帮助。
OAuth2.0 定义了四个角色
- Client:客户端,第三方应用程序。
- Resource Owner:资源所有者,授权 Client 访问其帐户的用户。
- Authorization server:授权服务器,服务商专用于处理用户授权认证的服务器。
- Resource server:资源服务器,服务商用于存放用户受保护资源的服务器,它可以与授权服务器是同一台服务器,也可以是不同的服务器。
oauth2-server
- Access token:用于访问受保护资源的令牌。
- Authorization code:发放给应用程序的中间令牌,客户端应用使用此令牌交换 access token。
- Scope:授予应用程序的权限范围。
- JWT:Json Web Token 是一种用于安全传输的数据传输格式。
运行流程
安装
推荐使用 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(授权服务器启动类)接受两种加密密钥,string
或 defuse/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 定义了四种授权模式,以应对不同情况时的授权。
- 授权码模式
- 隐式授权模式
- 密码模式
- 客户端模式
客户端类型
- 保密的:
- 客户端可以安全的存储自己与用户的凭据(例如:有所属的服务器端)
- 公开的:
- 客户端无法安全的存储自己与用户的凭据(例如:运行在浏览器的单页应用)
选用哪种授权模式?
如果客户端是保密的,应使用授权码模式。
如果客户端是公开的,应使用隐式授权模式。
如果用户对于此客户端高度信任(例如:第一方应用程序或操作系统程序),应使用密码模式。
如果客户端是以自己的名义,不与用户产生关系,应使用客户端模式。
预先注册
客户端需要预先在授权服务器进行注册,用以获取 client_id
与 client_secret
,也可以在注册是预先设定好 redirect_uri
,以便于之后可以使用默认的 redirect_uri
。
授权码模式
授权码模式是 OAuth2.0 种功能最完整,流程最严密的一种模式,如果你使用过 Google 或 QQ 登录过第三方应用程序,应该会对这个流程的第一部分很熟悉。
流程
第一部分(用户可见)
用户访问客户端,客户端将用户导向授权服务器时,将以下参数通过 GET query
传入:
-
response_type
:授权类型,必选项,值固定为:code
-
client_id
:客户端ID,必选项 -
redirect_uri
:重定向URI,可选项,不填写时默认预先注册的重定向URI -
scope
:权限范围,可选项,以空格分隔 -
state
:CSRF令牌,可选项,但强烈建议使用,应将该值存储与用户会话中,以便在返回时验证
用户选择是否给予客户端授权
假设用户给予授权,授权服务器将用户导向客户端事先指定的 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 的新版本并没有指定某种数据库,但它提供了 Interfaces 与 Traits 帮助我们实现,这让我们可以方便的使用任何形式的数据存储方式,这种方便的代价就是需要我们自行创建 Repositories 与 Entities。
初始化 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小时 );
使用
注意:这里的示例演示的是 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); } });
此时展示给用户的是这样的页面:
用户提交登录后,设置好用户实体(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
:权限范围,可选项,以空格分隔 -
state
:CSRF令牌,可选项,但强烈建议使用,应将该值存储与用户会话中,以便在返回时验证
用户选择是否给予客户端授权
假设用户给予授权,授权服务器将用户导向客户端事先指定的 redirect_uri
,并将以下参数通过 GET query
传入:
-
token_type
:令牌类型,值固定为:Bearer
-
expires_in
:访问令牌的存活时间 -
access_token
:访问令牌 -
state
:请求中发送的state
,原样返回。客户端将此值与用户会话中的值进行对比,以确保授权码响应的是此应用程序而非其他应用程序
整个流程与授权码模式的第一部分类似,只是授权服务器直接响应了访问令牌,跳过了授权码的步骤。它适用于没有服务器,完全运行在前端的应用程序。
此模式下没有刷新令牌(refresh token)的返回。
使用 oauth2-server 实现
初始化授权码类型
// 将隐式授权类型添加进 server $server->enableGrantType( new ImplicitGrant(new \DateInterval('PT1H')), new \DateInterval('PT1H') // 设置访问令牌过期时间1小时 );
使用
注意:这里的示例演示的是 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); } });
此时展示给用户的是这样的页面:
用户提交登录后,设置好用户实体(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 实现
初始化授权码类型
$grant = new \League\OAuth2\Server\Grant\PasswordGrant( $userRepository, $refreshTokenRepository ); $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 设置刷新令牌过期时间1个月 // 将密码授权类型添加进 server $server->enableGrantType( $grant, new \DateInterval('PT1H') // 设置访问令牌过期时间1小时 );
使用
注意:这里的示例演示的是 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->enableGrantType( new \League\OAuth2\Server\Grant\ClientCredentialsGrant(), new \DateInterval('PT1H') // 设置访问令牌过期时间1小时 );
使用
注意:这里的示例演示的是 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 实现
初始化授权码类型
$grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant($refreshTokenRepository); $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // 新的刷新令牌过期时间1个月 // 将刷新访问令牌添加进 server $server->enableGrantType( $grant, new \DateInterval('PT1H') // 新的访问令牌过期时间1小时 );
使用
$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 的实现需要我们手动创建 Repositories 与 Entities,下面展示一个项目目录示例:
- 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)