markdown Zend Framework 3:授权和RBAC
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了markdown Zend Framework 3:授权和RBAC相关的知识,希望对你有一定的参考价值。
# RBAC and Permission
## Dynamic Assertions
Check permission against the real user to determine if the resource really belongs to that user.
## RbacAssertionManager
```php
namespace User\Service;
use Doctrine\ORM\EntityManager;
use User\Entity\User;
use Zend\Authentication\AuthenticationService;
use Zend\Permissions\Rbac\Rbac;
/**
* This service is used for invoking user-defined BRAC dynamic assertions.
* Class RbacAssertionManager
* @package User\Service
*/
class RbacAssertionManager {
/**
* Entity Manager.
* @var EntityManager
*/
private $entityManager;
/**
* Authentication Service.
* @var AuthenticationService
*/
private $authService;
public function __construct(EntityManager $entityManager, AuthenticationService $authService)
{
$this->entityManager = $entityManager;
$this->authService = $authService;
}
/**
* This method is used for dynamic assertions.
* @param Rbac $rbac
* @param $permission
* @param $params
* @return bool
*/
public function assert(Rbac $rbac, $permission, $params) {
$currentUser = $this->entityManager->getRepository(User::class)
->findOneByEmail($this->authService->getIdentity());
if ($permission == 'profile.own.view' && $params['user']->getId() == $currentUser->getId())
return true;
return false;
}
}
```
## RbacAssertionManagerFactory
```php
namespace User\Service\Factory;
use Interop\Container\ContainerInterface;
use User\Service\RbacAssertionManager;
use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Factory\FactoryInterface;
/**
* This is the factory class for RbacAssertionManager service. The purpose of the factory
* is to instantiate the service and pass it dependencies (inject dependencies).
* Class RbacAssertionManagerFactory
* @package User\Service\Factory
*/
class RbacAssertionManagerFactory implements FactoryInterface {
/**
* This method creates the RbacManager service and returns its instance.
* @param ContainerInterface $container
* @param string $requestedName
* @param array|null $options
* @return object|RbacAssertionManager
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$entityManager = $container->get('doctrine.entitymanager.orm_default');
$authService = $container->get(AuthenticationService::class);
return new RbacAssertionManager($entityManager, $authService);
}
}
```
## Role Entity
```php
namespace User\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* This class represents a role.
* @ORM\Entity()
* @ORM\Table(name="role")
*/
class Role
{
/**
* @ORM\Id
* @ORM\Column(name="id")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(name="name")
*/
protected $name;
/**
* @ORM\Column(name="description")
*/
protected $description;
/**
* @ORM\Column(name="date_created")
*/
protected $dateCreated;
/**
* @ORM\ManyToMany(targetEntity="User\Entity\Role")
* @ORM\JoinTable(name="role_hierarchy",
* joinColumns={@ORM\JoinColumn(name="child_role_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="parent_role_id", referencedColumnName="id")}
* )
*/
private $parentRoles;
/**
* @ORM\ManyToMany(targetEntity="User\Entity\Role")
* @ORM\JoinTable(name="role_hierarchy",
* joinColumns={@ORM\JoinColumn(name="parent_role_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="child_role_id", referencedColumnName="id")}
* )
*/
protected $childRoles;
/**
* @ORM\ManyToMany(targetEntity="User\Entity\Permission")
* @ORM\JoinTable(name="role_permission",
* joinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="permission_id", referencedColumnName="id")}
* )
*/
private $permissions;
/**
* Constructor.
*/
public function __construct()
{
$this->parentRoles = new ArrayCollection();
$this->childRoles = new ArrayCollection();
$this->permissions = new ArrayCollection();
}
/**
* Returns role ID.
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Sets role ID.
* @param int $id
*/
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getDateCreated()
{
return $this->dateCreated;
}
public function setDateCreated($dateCreated)
{
$this->dateCreated = $dateCreated;
}
public function getParentRoles()
{
return $this->parentRoles;
}
public function getChildRoles()
{
return $this->childRoles;
}
public function getPermissions()
{
return $this->permissions;
}
public function addParent(Role $role)
{
if ($this->getId() == $role->getId()) {
return false;
}
if (!$this->hasParent($role)) {
$this->parentRoles[] = $role;
return true;
}
return false;
}
public function clearParentRoles()
{
$this->parentRoles = new ArrayCollection();
}
public function hasParent(Role $role)
{
if ($this->getParentRoles()->contains($role)) {
return true;
}
return false;
}
}
```
## Permission Entity
```php
namespace User\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* This class represents a permission.
* @ORM\Entity()
* @ORM\Table(name="permission")
*/
class Permission
{
/**
* @ORM\Id
* @ORM\Column(name="id")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(name="name")
*/
protected $name;
/**
* @ORM\Column(name="description")
*/
protected $description;
/**
* @ORM\Column(name="date_created")
*/
protected $dateCreated;
/**
* @ORM\ManyToMany(targetEntity="User\Entity\Role")
* @ORM\JoinTable(name="role_permission",
* joinColumns={@ORM\JoinColumn(name="permission_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
private $roles;
/**
* Constructor.
*/
public function __construct()
{
$this->roles = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getDateCreated()
{
return $this->dateCreated;
}
public function setDateCreated($dateCreated)
{
$this->dateCreated = $dateCreated;
}
public function getRoles()
{
return $this->roles;
}
}
```
## User Entity
```php
namespace User\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* This class represents a registered user.
* @ORM\Entity()
* @ORM\Table(name="user")
*/
class User
{
// User status constants.
const STATUS_ACTIVE = 1; // Active user.
const STATUS_RETIRED = 2; // Retired user.
/**
* @ORM\Id
* @ORM\Column(name="id")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(name="email")
*/
protected $email;
/**
* @ORM\Column(name="full_name")
*/
protected $fullName;
/**
* @ORM\Column(name="password")
*/
protected $password;
/**
* @ORM\Column(name="status")
*/
protected $status;
/**
* @ORM\Column(name="date_created")
*/
protected $dateCreated;
/**
* @ORM\Column(name="pwd_reset_token")
*/
protected $passwordResetToken;
/**
* @ORM\Column(name="pwd_reset_token_creation_date")
*/
protected $passwordResetTokenCreationDate;
/**
* @ORM\ManyToMany(targetEntity="User\Entity\Role")
* @ORM\JoinTable(name="user_role",
* joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
private $roles;
/**
* Constructor.
*/
public function __construct()
{
$this->roles = new ArrayCollection();
}
/**
* Returns user ID.
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Sets user ID.
* @param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* Returns email.
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* Sets email.
* @param string $email
*/
public function setEmail($email)
{
$this->email = $email;
}
/**
* Returns full name.
* @return string
*/
public function getFullName()
{
return $this->fullName;
}
/**
* Sets full name.
* @param string $fullName
*/
public function setFullName($fullName)
{
$this->fullName = $fullName;
}
/**
* Returns status.
* @return int
*/
public function getStatus()
{
return $this->status;
}
/**
* Returns possible statuses as array.
* @return array
*/
public static function getStatusList()
{
return [
self::STATUS_ACTIVE => 'Active',
self::STATUS_RETIRED => 'Retired'
];
}
/**
* Returns user status as string.
* @return string
*/
public function getStatusAsString()
{
$list = self::getStatusList();
if (isset($list[$this->status]))
return $list[$this->status];
return 'Unknown';
}
/**
* Sets status.
* @param int $status
*/
public function setStatus($status)
{
$this->status = $status;
}
/**
* Returns password.
* @return string
*/
public function getPassword()
{
return $this->password;
}
/**
* Sets password.
* @param string $password
*/
public function setPassword($password)
{
$this->password = $password;
}
/**
* Returns the date of user creation.
* @return string
*/
public function getDateCreated()
{
return $this->dateCreated;
}
/**
* Sets the date when this user was created.
* @param string $dateCreated
*/
public function setDateCreated($dateCreated)
{
$this->dateCreated = $dateCreated;
}
/**
* Returns password reset token.
* @return string
*/
public function getResetPasswordToken()
{
return $this->passwordResetToken;
}
/**
* Sets password reset token.
* @param string $token
*/
public function setPasswordResetToken($token)
{
$this->passwordResetToken = $token;
}
/**
* Returns password reset token's creation date.
* @return string
*/
public function getPasswordResetTokenCreationDate()
{
return $this->passwordResetTokenCreationDate;
}
/**
* Sets password reset token's creation date.
* @param string $date
*/
public function setPasswordResetTokenCreationDate($date)
{
$this->passwordResetTokenCreationDate = $date;
}
/**
* Returns the array of roles assigned to this user.
* @return array
*/
public function getRoles()
{
return $this->roles;
}
/**
* Returns the string of assigned role names.
*/
public function getRolesAsString()
{
$roleList = '';
$count = count($this->roles);
$i = 0;
foreach ($this->roles as $role) {
$roleList .= $role->getName();
if ($i<$count-1)
$roleList .= ', ';
$i++;
}
return $roleList;
}
/**
* Assigns a role to user.
*/
public function addRole($role)
{
$this->roles->add($role);
}
}
```
## Setup Caching for Role and Permission
on `config/autoload/global.php`
```php
use Doctrine\Common\Cache\FilesystemCache;
use Zend\Cache\Storage\Adapter\Filesystem;
return [
// ...
'caches' => [
FilesystemCache::class => [
'adapter' => [
'name' => Filesystem::class,
'options' => [
// store cache data in this directory.
'cache_dir' => './data/cache',
// store cached data for 1 hour.
'ttl' => 60*60*1
]
],
'plugins' => [
[
'name' => 'serializer',
'options' => []
]
]
]
],
// ...
]
```
## RbacManager
```php
namespace User\Service;
use Doctrine\ORM\EntityManager;
use User\Entity\Role;
use Zend\Authentication\AuthenticationService;
use Zend\Cache\Storage\StorageInterface;
use Zend\Permissions\Rbac\Rbac;
use User\Entity\User;
/**
* This service is used for invoking user-defined RBAC dynamic assertion.
* @package User\Service
*/
class RbacManager
{
/**
* Entity Manager.
* @var EntityManager
*/
private $entityManager;
/**
* RBAC Service.
* @var Rbac
*/
private $rbac;
/**
* Auth Service.
* @var AuthenticationService
*/
private $authService;
/**
* Filesystem cache.
* @var StorageInterface
*/
private $cache;
/**
* Assertions manager.
* @var array
*/
private $assertionManagers = [];
/**
* RbacAssertionManager constructor.
* @param $entityManager
* @param $authService
* @param $cache
* @param $assertionManagers
*/
public function __construct($entityManager, $authService, $cache, $assertionManagers)
{
$this->entityManager = $entityManager;
$this->authService = $authService;
$this->cache = $cache;
$this->assertionManagers = $assertionManagers;
}
/**
* Initializes the RBAC container.
* @param bool $forceCreate
*/
public function init($forceCreate = false) {
if ($this->rbac != null && !$forceCreate) {
// already initialized, do nothing.
return ;
}
// if user wants us to re-init RBAC container, clear cache now.
if ($forceCreate)
$this->cache->removeItem('rbac_container');
// try to load Rbac container from cache.
$this->rbac = $this->cache->getItem('rbac_container', $result);
if (!$result) {
// Create Rbac container.
$rbac = new Rbac();
$this->rbac = $rbac;
// Construct role hierarchy by loading roles and permissions from database.
$rbac->setCreateMissingRoles(true);
$roles = $this->entityManager->getRepository(Role::class)
->findBy([], ['id' => 'ASC']);
foreach ($roles as $role) {
$roleName = $role->getName();
$parentRoleNames = [];
foreach ($role->getParentRoles() as $parentRole) {
$parentRoleNames[] = $parentRole->getName();
}
$rbac->addRole($roleName, $parentRoleNames);
foreach ($role->getPermissions() as $permission) {
$rbac->getRole($roleName)->addPermission($permission->getName);
}
}
// Save Rbac container to cache.
$this->cache->setItem('rbac_container', $rbac);
}
}
/**
* Checks whether the given user has permission
* @param $user
* @param $permission
* @param null $params
* @return bool
* @throws \Exception
*/
public function isGranted($user, $permission, $params = null) {
if ($this->rbac == null)
$this->init();
if ($user == null) {
$identity = $this->authService->getIdentity();
if ($identity == null)
return false;
$user = $this->entityManager->getRepository(User::class)
->findOneByEmail($identity);
if ($user == null)
// Oops..the identity presents in session, but there is no such user in database.
// We throw an exception, because this is a possible security problem.
throw new \Exception('There is no user with such identity');
}
$roles = $user->getRoles();
// loop through all roles of current user to check permission
foreach ($roles as $role) {
if ($this->rbac->isGranted($role->getName(), $permission)) {
if ($params == null)
return true;
// check assertion
foreach ($this->assertionManagers as $assertionManager) {
if ($assertionManager->assert($this->rbac, $permission, $params))
return true;
}
}
$parentRoles = $role->getParentRoles();
foreach ($parentRoles as $parentRole) {
if ($this->rbac->isGranted($parentRole->getName(), $permission))
return true;
}
}
return false;
}
}
```
## RbacManagerFactory
```php
namespace User\Service\Factory;
use Doctrine\Common\Cache\FilesystemCache;
use Interop\Container\ContainerInterface;
use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Factory\FactoryInterface;
use User\Service\RbacManager;
/**
* This is the factory class for RbacManager service. The purpose of the factory
* is to instantiate the service and pass id dependencies (inject dependencies)
* @package User\Service\Factory
*/
class RbacManagerFactory implements FactoryInterface {
/**
* This method creates the RbacManager service and returns its instance.
* @param ContainerInterface $container
* @param string $requestedName
* @param array|null $options
* @return object|RbacManager
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$entityManager = $container->get('doctrine.entitymanager.orm_default');
$authService = $container->get(AuthenticationService::class);
$cache = $container->get(FilesystemCache::class);
$assertionManagers = [];
$config = $container->get('config');
if (isset($config['rbac_manager']['assertions'])) {
foreach ($config['rbac_manager']['assertions'] as $serviceName) {
$assertionManagers[$serviceName] = $container->get($serviceName);
}
}
return new RbacManager($entityManager, $authService, $cache, $assertionManagers);
}
}
```
## Rbac Manager config
```php
use User\Service\RbacAssertionManager;
return [
// ...
'rbac_manager' => [
'assertions' => [
RbacAssertionManager::class
]
]
// ...
]
```
## Add Not Authorized page
```php
namespace User\Controller;
/**
* This controller is responsible for letting the user to log in and log out.
*
* Class AuthController
* @package User\Controller
*/
class AuthController extends AbstractActionController {
/**
* Displays the "Not Authorized" page.
* @return ViewModel
*/
public function notAuthorizedAction() {
$this->getResponse()->setStatusCode(403);
return new ViewModel();
}
// ...
```
## Modify access_filter
```php
use User\Controller\UserController;
use User\Controller\RoleController;
use User\Controller\PermissionController;
return [
// The 'access_filter' key is used by module User to restrict or permit
// access to certain controller actions for unauthenticated visitors.
'access_filter' => [
'options' => [
// The access filter can work in 'restrictive' (recommended) or 'permissive'
// mode. In restrictive mode all controller actions must be explicitly listed
// under the 'access_filter' config key, and access is denied to any not listed
// action for users not logged in. In permissive mode, if an action is not listed
// under the 'access_filter' key, access to it is permitted to anyone (even for
// users not logged in. Restrictive mode is more secure and recommended.
'mode' => 'permissive'
],
'controllers' => [
UserController::class => [
// Allow anyone to visit "resetPassword", "message" and "setPassword" actions
['actions' => ['resetPassword', 'message', 'setPassword'], 'allow' => '*'],
// Allow authenticated users to visit "index", "add", "edit", "view", "changePassword" action
// Give access to "index", "add", "edit", "view, "changePassword" actions
// to users having the "user.manager" permission.
['actions' => ['index', 'add', 'edit', 'view', 'changePassword'], 'allow' => '+user.manage']
],
RoleController::class => [
// Allow access to authenticated users having the "role.manage" permission.
['actions' => '*', 'allow' => '+role.manage']
],
PermissionController::class => [
// Allow access to authenticated users having "permission.manage" permission.
['action' => '*', 'allow' => '+permission.manage']
]
]
],
//...
]
```
## Modify `filterAccess` in `AuthManager`
```php
namespace User\Service;
class AuthManager {
// Constants returned by the access filter.
const ACCESS_GRANTED = 1; // Access to the page is granted.
const AUTH_REQUIRED = 2; // Authentication is required to see the page.
const ACCESS_DENIED = 3; // Access to the page is denied.
/**
* @var SessionManager
*/
protected $sessionManager;
/**
* @var AuthenticationService
*/
protected $authService;
/**
* Contents of the 'access_filter' config key.
* @var array
*/
protected $config;
/**
* RBAC Manager
* @var RbacManager
*/
protected $rbacManager;
/**
* AuthManager constructor.
* @param SessionManager $sessionManager
* @param AuthenticationService $authService
* @param array $config
*/
public function __construct(SessionManager $sessionManager, AuthenticationService $authService, $config, RbacManager $rbacManager)
{
$this->sessionManager = $sessionManager;
$this->authService = $authService;
$this->config = $config;
$this->rbacManager = $rbacManager;
}
// ...
/**
* This is a simple access control filter. It is able to restrict unauthorized
* users to visit certain pages.
*
* This method uses the 'access_filter' key in the config file and determines
* whether the current visitor is allowed to access the given controller action
* or not. It returns true if allowed; otherwise false.
* @param $controllerName
* @param $actionName
* @return bool
* @throws \Exception
*/
public function filterAccess($controllerName, $actionName) {
// Determine mode - 'restrictive' (default) or 'permissive'. In restrictive
// mode all controller actions must be explicitly listed under the 'access_filter'
// config key, and access is denied to any not listed action for unauthorized users.
// In permissive mode, if an action is not listed under the 'access_filter' key,
// access to it is permitted to anyone (even for not logged in users.
// Restrictive mode is more secure and recommended to use.
$mode = isset($this->config['options']['mode']) ? $this->config['options']['mode'] : 'restrictive';
if ($mode != 'restrictive' && $mode != 'permissive')
throw new \Exception('Invalid filter access mode (expected either restrictive or permissive mode)');
if (isset($this->config['controllers'][$controllerName])) {
$items = $this->config['controllers'][$controllerName];
foreach ($items as $item) {
$actionList = $item['actions'];
$allow = $item['allow'];
if (is_array($actionList) && in_array($actionName, $actionList) ||
$actionList == '*') {
if ($allow == '*')
// Anyone is allowed to see the page.
return self::ACCESS_GRANTED;
elseif (!$this->authService->hasIdentity())
// Only authenticated user is allowed to see the page.
return self::AUTH_REQUIRED;
if ($allow == '@')
// Any authenticated user is allowed to see the page.
return self::ACCESS_GRANTED;
elseif (substr($allow, 0, 1) == '@') {
// Only the user with specific identity is allowed to see the page.
$identity = substr($allow, 1);
if ($this->authService->getIdentity() == $identity)
return self::ACCESS_GRANTED;
else
return self::ACCESS_DENIED;
} elseif (substr($allow, 0, 1) == '+') {
// Only the user with this permission is allowed to see the page.
$permission = substr($allow, 1);
if ($this->rbacManager->isGranted(null, $permission))
return self::ACCESS_GRANTED;
else
return self::ACCESS_DENIED;
} else
throw new \Exception('Unexpected value for "allow" - expected either "?", "@", "@identity" or "+permission".');
}
}
}
// In restrictive mode, we forbid access for authenticated users to any
// action not listed under 'access_filter' key (for security reasons).
if ($mode == 'restrictive' && !$this->authService->hasIdentity())
if (!$this->authService->hasIdentity())
return self::AUTH_REQUIRED;
else
return self::ACCESS_DENIED;
// Permit access to this page.
return self::ACCESS_GRANTED;
}
// ...
}
```
## Modify `onDispatch` on `Module.php`
```php
namespace User;
use User\Service\AuthManager;
use User\Controller\AuthController;
class Module {
// ...
/**
* Event listener method for the 'Dispatch' event. We listen to the Dispatch
* event to call the access filter. The access filter allows to determine if
* the current visitor is allowed to see the page or not. If he/she
* is not authenticated and is not allowed to see the page, we redirect the user
* to the login page.
*
* @param MvcEvent $event
* @return mixed
*/
public function onDispatch(MvcEvent $event) {
// Get controller and action to which the HTTP request was dispatched.
$controller = $event->getTarget();
$controllerName = $event->getRouteMatch()->getParam('controller', null);
$actionName = $event->getRouteMatch()->getParam('action', null);
// Convert dash-style action name to camel-case.
$actionName = str_replace('-', '', lcfirst(ucwords($actionName, '-')));
// Get the instance of AuthManager service.
$authManager = $event->getApplication()->getServiceManager()->get(AuthManager::class);
// Execute the access filter on every controller except AuthController
// (to avoid infinite redirect).
if ($controllerName != AuthController::class) {
$result = $authManager->filterAccess($controllerName, $actionName);
if ($result == AuthManager::AUTH_REQUIRED) {
// Remember the URL of the page the user tried to access. We will
// redirect the user to that URL after successful login.
$uri = $event->getApplication()->getRequest()->getUri();
// Make the URL relative (remove scheme, user info, host name and port)
// to avoid redirecting to other domain by a malicious user.
$uri->setScheme(null)
->setHost(null)
->setPort(null)
->setUserInfo(null);
$redirectUrl = $uri->toString();
// Redirect the user to the "Login" page.
return $controller->redirect()->toRoute('login', [],
['query' => ['redirectUrl' => $redirectUrl]]);
}
elseif ($result == AuthManager::ACCESS_DENIED) {
// Redirect the user to the "Not Authorized" page.
return $controller->redirect()->toRoute('not-authorized');
}
}
}
}
```
以上是关于markdown Zend Framework 3:授权和RBAC的主要内容,如果未能解决你的问题,请参考以下文章
markdown Zend Framework 3:授权和RBAC
markdown Zend Framework 3:Controller插件管理器
markdown Zend Framework 3:RBAC的访问过滤器和访问视图助手
markdown Zend Framework 3:用于身份验证服务的CurrentUser Filter和CurrentUser View Helper