aeris/zf-auth

为 Zend Framework 2 提供认证/授权组件

v1.0.0 2015-08-20 17:06 UTC

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\OAuth2DoctrineOrmModule 的示例配置文件。

认证

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 提供两种方法来限制授权身份对资源的访问:

  1. 路由守卫
  2. 投票者

路由守卫允许您在请求到达控制器之前,使用一组简单的规则来限制对资源的访问。投票者允许您使用高级逻辑来限制对 特定资源 的访问。

路由守卫

在将路由匹配到控制器之后,但在控制器操作执行之前,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 上的 getgetListupdatefooAction 方法。

请注意,未配置的任何控制器/操作将默认受到限制。

控制器守卫

Aeris\ZfAuth\Guard\ControllerGuard 根据请求用户的角色限制对控制器操作的访问。

选项有

  • 'controller' 此规则适用的控制器(ControllerManager 服务名称)
  • 'actions' 此规则适用的操作。使用 '*' 将此规则应用于控制器的所有操作。注意,要使用 REST 操作,您必须使用来自 Aeris\ZendRestModuleAeris\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() 示例为基础。假设老板给了我们两条必须执行的规则

  1. 只有登录的 OAuth 用户可以创建动物
  2. 如果您想创建一只猴子,您必须首先 成为 一只猴子。

对于这两条规则,我们将创建两个不同的投票者

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,
		]
	]
]