aeris / zf-auth
为 Zend Framework 2 提供认证/授权组件
Requires
- aeris/fn: ^1.2.0
- aeris/spy: ^1.1
- aeris/zend-rest-module: ~1.1.3
- aeris/zf-di-config: ^1.3.1
- bshaffer/oauth2-server-php: ~1.3
- doctrine/doctrine-orm-module: 0.8.*
- doctrine/orm: ^2.5
- symfony/security-core: ^2.7
- zendframework/zend-authentication: >2.3
- zendframework/zend-http: ~2.3
- zfcampus/zf-oauth2: ~1.3.1
Requires (Dev)
- mockery/mockery: ^0.9.4
- nelmio/alice: >=2.0
This package is not auto-updated.
Last update: 2024-09-18 08:19:47 UTC
README
为 Zend Framework 2 提供认证/授权组件。
安装
使用 composer 安装
composer require aeris/zf-auth
将模块添加到您的 application.config.php
return [ 'modules' => [ 'Aeris\ZfAuth', // REQUIRED Dependencies 'Aeris\ZfDiConfig', // for fancy service manager config // OPTIONAL Dependencies 'Zf\OAuth2', // if using OAuth IdentityProviders 'Zf\ContentNegotiation', // if using OAuth IdentityProviders 'DoctrineModule', // if using DoctrineOrmIdentityRepository 'DoctrineORMmodule', // if using DoctrineOrmIdentityRepository 'ZfcRbac', // if using Route Guards ] ]; // Note that unless you're customizing Zf\OAuth2 services, // you probably will need all of the "optional" modules.
配置参考
return [ // See https://github.com/zfcampus/zf-oauth2/blob/master/config/oauth2.local.php.dist 'zf-oauth2' => [...], // See https://github.com/doctrine/DoctrineORMModule/blob/master/config/module.config.php 'doctrine' => [...], // Aeris\ZfAuth configuration 'zf_auth' => [ 'authentication' => [ // If you're using a Doctrine Entity as a user identity, // supply the entity class here (required for DoctrineOrmIdentityRepository). 'user_entity_class' => 'Path\To\Entity\User' ] ] ]
OAuth2 数据库设置
如果您使用的是 Zf\OAuth2
模块,您需要为 OAuth 存储创建数据库表。有关 MySQL OAuth 数据库架构的示例,请参阅 /tests/data/zf-oauth-test.sql
。
Aeris\ZfAuth
包含一组 Doctrine 实体,它们映射到 OAuth 数据库表,位于 Aeris\ZfAuth\Entity
命名空间下。
您可以在 /tests/config/autoload/
中查看 Zf\OAuth2
和 DoctrineOrmModule
的示例配置文件。
认证
ZfAuth 尝试使用一系列 IdentityProviders
对请求进行认证。默认情况下,用户可以按以下方式认证:
- 实现
IdentityInterface
的用户,如配置在zf_auth.authentication.user_entity_class
中(带有access_token
的请求) \Aeris\ZfAuth\Identity\OAuthClientIdentity
(仅带有 client_id/client_secret 的请求)\Aeris\ZfAuth\Identity\AnonymousIdentity
(没有认证键的请求)
处理无效凭据
如果请求包含认证凭据,但身份提供者无法提供身份(例如,请求包含无效/过期的 access_token
),则将触发一个 MvcEvent::EVENT_DISPATCH_ERROR
事件,该事件包含一个 \Aeris\ZfAuth\Exception\AuthenticationException
。
这可以通过您希望使用的任何视图机制进行处理。如果您使用 Aeris\ZendRestModule
,您将在 errors
配置中处理 AuthenticationExceptions
。
return [ 'zend_rest' => [ 'errors' => [ // ... [ 'error' => '\Aeris\ZfAuth\Exception\AuthenticationException', 'http_code' => 401, 'application_code' => 'authentication_error', 'details' => 'The request failed to be authenticated. Check your access keys, and try again.' ] ] ] ]
身份提供者
ZfAuth 通过暴露 IdentityInterface
对象的 Identity Providers 对请求进行认证。可以将身份提供者包装为 ZF2 服务,并将其注入到控制器、授权服务等中。
默认 ZfAuth 身份提供者使用 Zf\OAuth
模块通过访问令牌认证用户,并返回在 zf_auth.authentication.user_entity_class
配置中定义的类型。
默认身份提供者是 ChainedIdentityProvider
,这意味着它将尝试从一系列身份提供者中返回一个身份,返回第一个提供者。调用 getIdentity()
的过程如下:
- 查找与请求的
access_token
相关联的用户 - 如果没有找到用户,查找与请求的
client_id
/client_secret
相关联的\Aeris\ZfAuth\Identity\OAuthClientIdentity
- 如果没有找到用户,返回一个
\Aeris\ZfAuth\Identity\AnonymousIdentity
实例
使用示例
$identityProvider = $serviceLocator->get('Aeris\ZfAuth\IdentityProvider'); $user = $identityProvider->getIdentity(); // See "Authorization" docs for a more advanced approach to authorization. if (in_array('admin', $user->getRoles()) { $this->doLotsOfCoolThings(); } else { throw new UnauthorizedUserException(); }
自定义身份提供者
假设我们有一个具有超级特殊用户,他们有一个超级特殊的静态密码,允许他们做超级特殊的事情。以下是认证该用户的方法:
use Aeris\ZfAuth\IdentityProvider\IdentityProviderInterface; use Zend\Http\Request; use Zend\ServiceManager\ServiceLocatorAwareInterface; class SuperSpecialIdentityProvider implements IdentityProviderInterface, ServiceLocatorAwareInterface { use \Zend\ServiceManager\ServiceLocatorAwareTrait; public function canAuthenticate() { /** @var Request $request */ $request = $this->serviceLocator->get('Application') ->getMvcEvent() ->getRequest(); return $request->getQuery('super_secret_password') !== null; } /** @return \Aeris\ZfAuth\Identity\IdentityInterface */ public function getIdentity() { /** @var Request $request */ $request = $this->serviceLocator->get('Application') ->getMvcEvent() ->getRequest(); $password = $request->getQuery('super_secret_password'); $isSuperSecretUser = $password === '42'; // Return null if we cannot authenticate the user if ($isSuperSecretUser) { return null; } // Return our super-secret user return $this->serviceLocator ->get('entity_manager') ->getRepo('MyApp\Entity\User') ->findOneByUsername('superSecretUser'); } }
现在让我们将其连接起来。
// module.config.php return [ 'service_manager' => [ // Aeris\ZfDiConfig ftw 'di' => [ // Override default identity provider 'Aeris\ZfAuth\IdentityProvider' => [ // Wrap in ChainedIdentityProvider, so we still // have access to other authenticators 'class' => 'Aeris\ZfAuth\IdentityProvider\ChainedIdentityProvider', 'setters' => [ 'providers' => [ // Add our provider to the top of the list '$factory:\MyApp\IdentityProviders\SuperSpecialIdentityProvider' // Include default set of providers '@Aeris\ZfAuth\IdentityProvider\OAuthUserIdentityProvider', '@Aeris\ZfAuth\IdentityProvider\OAuthClientIdentityProvider', '@Aeris\ZfAuth\IdentityProvider\AnonymousIdentityProvider' ] ] ] ] ] ];
授权
ZfAuth 提供两种方法来限制授权身份对资源的访问:
- 路由守卫
- 投票者
路由守卫允许您在请求到达控制器之前,使用一组简单的规则来限制对资源的访问。投票者允许您使用高级逻辑来限制对 特定资源 的访问。
路由守卫
在将路由匹配到控制器之后,但在控制器操作执行之前,ZfAuth 将检查您的路由守卫规则,以查看当前身份是否通过每个规则。
配置
路由守卫通过 zf_auth.guards
模块选项进行配置。每个键是守卫服务的名称,值是应用于守卫的规则数组。
return [ 'zf_auth' => [ 'guards' => [ 'Aeris\ZfAuth\Guard\ControllerGuard' => [ [ 'controller' => 'Aeris\ZfAuthTest\Controller\IndexController', 'actions' => ['*'], 'roles' => ['*'] ], [ 'controller' => 'Aeris\ZfAuthTest\Controller\AdminController', 'actions' => ['get', 'getList', 'update', 'foo' ], 'roles' => ['admin'] ], ], ] ] ]
以下配置示例允许任何用户访问 IndexController
的任何操作,但只允许具有 admin
角色的用户访问 AdminController
上的 get
、getList
、update
和 fooAction
方法。
请注意,未配置的任何控制器/操作将默认受到限制。
控制器守卫
Aeris\ZfAuth\Guard\ControllerGuard
根据请求用户的角色限制对控制器操作的访问。
选项有
'controller'
此规则适用的控制器(ControllerManager
服务名称)'actions'
此规则适用的操作。使用'*'
将此规则应用于控制器的所有操作。注意,要使用REST
操作,您必须使用来自Aeris\ZendRestModule
的Aeris\ZendRestModule\Mvc\Router\Http\RestSegment
路由类型'roles'
允许访问此控制器操作的权限角色。使用'*'
允许任何角色。
自定义守卫
您可以创建一个自定义守卫,该守卫实现了 GuardInterface
namespace Aeris\ZfAuth\Guard; use Zend\Mvc\Router\RouteMatch; interface GuardInterface { public function __construct(array $rules = []); public function setRules(array $rules); /** @return boolean */ public function isGranted(RouteMatch $event); }
isGranted
方法应该在当前身份允许访问资源时返回 true。
为了演示,让我们创建一个基于用户名限制用户的守卫。我们的最终配置将如下所示
[ 'zf_auth' => [ 'guards' => [ 'MyApp\Guard\UsernameGuard' => [ // Rules to pass to our guard [ 'controller' => 'MyApp\Controller\AdminController', 'usernames' => ['alice', 'bob'] ], [ 'controller' => 'MyApp\Controller\IndexController', 'usernames' => ['*'] ], ] ] ] ]
我们的 UsernameGuard
类将检查当前控制器和用户身份与配置中提供的规则
class UsernameGuard implements GuardInterface { /** @var array */ protected $rules; /** @var IdentityProviderInterface */ protected $identityProvider; public function __construct(array $rules = []) { $this->setRules($rules); } public function setRules(array $rules) { $this->rules = $rules; } /** @return boolean */ public function isGranted(RouteMatch $routeMatch) { $controller = $routeMatch->getParam('controller'); // Find usernames allowed for this controller $allowedUsernames = array_reduce($this->rules, function($allowed, $rule) use ($controller) { $isMatch = $rule['controller'] === $controller; return array_merge($allowed, $isMatch ? $rule['usernames'] : []); }, []); $username = $this->identityProvider->getIdentity()->getUsername(); return in_array('*', $allowedUsernames) || in_array($username, $allowedUsernames); } public function setIdentityProvider(IdentityProviderInterface $identityProvider) { $this->identityProvider = $identityProvider; } }
最后一步是将您的守卫注册到 ZfAuth 守卫管理器
[ 'guard_manager' => [ // Using Aeris\ZfDiConfig, because I'm fancy // but you can use service factories if you want to be lame 'di' => [ 'MyApp\Guard\UsernameGuard' => [ 'class' => '\MyApp\Guard\UsernameGuard', 'setters' => [ 'identityProvider' => '@Aeris\ZfAuth\IdentityProvider' ] ] ] ] ]
投票者
投票者允许您限制对特定资源的访问。
使用投票者
使用投票者的主要方式是通过 AuthService
。以下是一个在控制器中使用 AuthService
的示例
use Aeris\ZfAuth\Service\AuthServiceAwareInterface; use Zend\Mvc\Controller\AbstractRestfulController; class AnimalRestController extends AbstractRestfulController implements AuthServiceAwareInterface { use \Aeris\ZfAuth\Service\AuthServiceAwareTrait; public function create($data) { $animal = new Animal($data); // Check if the current identity is allowed to create this animal if (!$this->authService->isGranted('create', $animal)) { throw new AuthorizationException('Tsk tsk tsk, you cannot create an animal, you!'); } $this->persist($animal); return $animal; } }
请注意,此控制器实现了 Aeris\ZfAuth\Service\AuthServiceAwareInterface
-- 这将导致 ZF2 ControllerManager
自动将 AuthService\Aeris\ZfAuth\Service\AuthService
服务注入到控制器中。
您还可以从应用程序服务定位器中获取 AuthService:$serviceLocator->get('AuthService\Aeris\ZfAuth\Service\AuthService')
投票者如何工作
投票者是一个实现了 \Symfony\Component\Security\Core\Authorization\Voter\VoterInterface
的类。Voter::vote()
方法返回以下之一
VoterInterface::ACCESS_GRANTED
VoterInterface::ACCESS_DENIED
VoterInterface::ACCESS_ABSTAIN
当您调用 AuthService::isGranted($action, $resource)
时,认证服务将运行每个已注册的投票者,并收集投票。如果有任何投票者返回 ACCESS_DENIED
,则 isGranted()
将返回 false。
实现自定义投票者
让我们以上面的 AnimalRestController::create()
示例为基础。假设老板给了我们两条必须执行的规则
- 只有登录的 OAuth 用户可以创建动物
- 如果您想创建一只猴子,您必须首先 成为 一只猴子。
对于这两条规则,我们将创建两个不同的投票者
class OnlyUsersCanCreateAnimalsVoter implements VoterInterface { public function vote(TokenInterface $token, $resource, array $actions) { // First, we need to decide whether we care about this resource/action $doWeCare = $this->supportsClass(get_class($resource)) && Aeris\Fn\any($actions, [$this, 'supportsAttribute']); if (!$doWeCare) { // Returning ACCESS_ABSTAIN tells our AuthService to ignore // the results of this voter return self::ACCESS_ABSTAIN; } // We can get the current Identity from the $token argument $currentIdentity = $token->getUser(); $isLoggedInUser = !($currentIdentity instanceof \Aeris\ZfAuth\Identity\AnonymousIdentity); // Do not allow anonymous requests to create animals return $isLoggedInUser ? self::ACCESS_GRANTED : self::ACCESS_DENIED; } public function supportsAttribute($action) { // This voter only cares about `create` actions (aka "attributes") return $action === 'create'; } public function supportsClass($class) { // This voter only cares about `Animal` objects return $class === 'MyApp\Model\Animal' || is_a($class, 'MyApp\Model\Animal'); } } class OnlyMonkeysCanCreateMonkeysVoter implements VoterInterface { public function vote(TokenInterface $token, $resource, array $actions) { // Again, we need to decide whether we care about this resource/action $doWeCare = $this->supportsClass(get_class($resource)) && Aeris\Fn\any($actions, [$this, 'supportsAttribute']) && // And in this case, we only care about animals which are also monkeys $resource->getType() === 'monkey'; if (!$doWeCare) { // Returning ACCESS_ABSTAIN tells our AuthService to ignore // the results of this voter return self::ACCESS_ABSTAIN; } // The $token is simply a Symfony interface which wraps a ZfAuth IdentityInterface object $currentIdentity = $token->getUser(); $isCurrentIdentityAMonkey = $currentIdentity instanceof Animal && $currentIdentity->getType() === 'monkey'; return $isCurrentIdentityAMonkey ? self::ACCESS_GRANTED : self::ACCESS_DENIED; } public function supportsAttribute($action) { // This voter only cares about `create` attribues (aka "actions") return $action === 'create'; } public function supportsClass($class) { // This voter only cares about `Animal` objects return $class === 'MyApp\Model\Animal' || is_a($class, 'MyApp\Model\Animal'); } }
最后,我们需要使用 zf_auth.voter_manager
配置注册这些投票者
[ 'voter_manager' => [ 'invokables' => [ 'OnlyUsersCanCreateAnimalsVoter' => '\MyApp\Voter\OnlyUsersCanCreateAnimalsVoter', 'OnlyMonkeysCanCreateMonkeysVoter' => '\MyApp\Voter\OnlyMonkeysCanCreateMonkeysVoter' ] ] ];
投票者配置参考
[ 'zf_auth' => [ // Register voters here 'voter_manager' => [ // Accepts same config as `service_manager` 'di' => [ // Also accepts Aeris\ZfDiConfig ] ], 'voter_options' => [ // `strategy` can be one of: // - 'affirmative': grant access as soon as any voter returns ACCESS_GRANTED // - 'consensus': grant access if there are more voters granting access than there are denying // - 'unanimous' (default): only grant access if none of the voters has denied access 'strategy' => 'unanimous', 'allow_if_all_abstain' => true, ] ] ]