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 定义了四个角色
- Client:客户端,第三方应用程序。
- Resource Owner:资源所有者,授权 Client 访问其帐户的用户。
- Authorization server:授权服务器,服务商专用于处理用户授权认证的服务器。
- Resource server:资源服务器,服务商用于存放用户受保护资源的服务器,它可以与授权服务器是同一台服务器,也可以是不同的服务器。
oauth2-server
- Access token:用于访问受保护资源的令牌。
- Authorization code:发放给应用程序的中间令牌,客户端应用使用此令牌交换 access token。
- Scope:授予应用程序的权限范围。
- JWT:Json 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(授权服务器启动类)接受两种加密密钥,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);
}
});
此时展示给用户的是这样的页面:
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
:权限范围,可选项,以空格分隔 -
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);
}
});
此时展示给用户的是这样的页面:
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 实现
初始化授权码类型
$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)