Autenticação e Autorização ========================== Autenticação e autorização são necessárias para uma página Web que deve ser limitada a determinados usuários. *Autenticação* serve para verificar se alguém é quem diz ser. Isto geralmente envolve a utilização de um usuário e senha, mas pode incluir outros métodos de validação da identidade, como cartão inteligente (smart card), impressões digitais, etc. *Autorização* serve para descobrir se uma pessoa, uma vez identificada (e autenticada), tem permissão para manipular recursos específicos. Isto geralmente é utilizado para descobrir se esta pessoa está em uma regra específica que possui acesso aos recursos. O Yii tem embutido um framework de autenticação/autorização (auth) que é fácil de usar e pode ser customizado para necessidades específicas. A parte central no framework auth do Yii é a pré-declaração do *componente de aplicação do usuário* que é um objeto implementando a interface [IWebUser]. O componente de usuário representa a persistência da informação de identidade para o usuário corrente. Pode ser acessado de qualquer lugar, utilizando `Yii::app()->user`. Utilizando o componente de usuário, podemos verificar se um usuário está conectado ou não através de [CWebUser::isGuest]; podemos [conectar|CWebUser::login] e [desconectar|CWebUser::logout] um usuário; podemos verificar se um usuário pode executar operações específicas, chamando [CWebUser::checkAccess]; e podemos também obter o [identificador único|CWebUser::name] e outras informações armazenadas sobre a identidade do usuário. Definindo a Classe de Identidade -------------------------------- Como mencionado acima, autenticação é como validar a identidade do usuário. A autenticação em uma aplição Web típica, geralmente é realizada pela combinação de usuário e senha, para verificar a identidade do usuário. Entretanto, pode ser incluído outros métodos e implementações diferentes podem ser necessárias. Para prover os diferentes métodos de autenticação, o framework auth do Yii introduz a classe de identidade. Podemos definir uma classe de identidade que contenha a autenticação lógica atual. A classe de identidade deve implementar a interface [IUserIdentity]. Diferentes classes de identidade podem ser implementadas para diferentes abordagens de autenticação (por exemplo, OpenID, LDAP, Twitter OAuth, Facebook Connect). Um bom começo quando escrevermos nossa própria implementação é extender a classe [CUserIdentity] que é uma classe básica para o modelo de autenticação utilizando usuário e senha. O principal trabalho na definição da classe de identidade é a implementação do método [IUserIdentity::authenticate]. Este método é utilizando para encapsular os principal detalhes da abordagem da autenticação. Uma classe de identidade também pode declarar informações adicionais de identidade que precisam ser armazenadas durante a sessão do usuário. #### Um Exemplo No exemplo a seguir, utilizamos uma classe de identidade para demonstrar a abordagem de autenticação para banco de dados. Isto é muito comum em aplicações Web. O usuário informa um usuário e senha em um formulário de login, e então validamos a credencial utilizando [ActiveRecord](/doc/guide/database.ar), comparando em uma tabela de usuário no banco de dados. Há algumas coisas sendo demonstradas neste simples exemplo: 1. A implementação do método `authenticate()` para utilizar banco de dados para validação das credenciais. 2. Sobreescrita do método `CUserIdentity::getId()` para retornar a propriedade `_id`, porque na implementação padrão retorna o nome de usuário como ID. 3. Utilização do método `setState()` ([CBaseUserIdentity::setState]) para demonstrar o armazenamento de outras informações que podem ser facilmente recuperadas em requisições posteriores. ~~~ [php] class UserIdentity extends CUserIdentity { private $_id; public function authenticate() { $record=User::model()->findByAttributes(array('username'=>$this->username)); if($record===null) $this->errorCode=self::ERROR_USERNAME_INVALID; else if($record->password!==md5($this->password)) $this->errorCode=self::ERROR_PASSWORD_INVALID; else { $this->_id=$record->id; $this->setState('title', $record->title); $this->errorCode=self::ERROR_NONE; } return !$this->errorCode; } public function getId() { return $this->_id; } } ~~~ Quando falarmos sobre login e logout na próxima seção, veremos que passamos a classe identidade como parâmetro ao método login de um usuário. Qualquer informação que precisarmos armazenar no estado (apenas chamando [CBaseUserIdentity::setState])) serão passadas para a classe [CWebUser], que por sua vez irá armazenar de forma persistente, como uma sessão. Esta informação pode ser acessada como uma propriedade da classe [CWebUser]. No nosso exemplo, armazenamos a informação do título do usuário através de `$this->setState('title', $record->title);`. Uma vez finalizado o processo de login, podemos obter a informação `title` do usuário corrente, simplesmente usando `Yii::app()->user->title`. > Info: Por padrão, [CWebUser] utiliza sessão para armazenamento persistente de informação sobre identidade de usuário. Caso o login estiver ativo e baseado na utilização de cookies (a configuração [CWebUser::allowAutoLogin] for verdadeira), a informação de identidade do usuário também será salva como um cookie. Tenha certeza de não declarar informações sensíveis (por exemplo, senha). Login e Logout -------------- Agora que já vimos um exemplo de criação de identidade de usuário, usaremos isto para ajudar a avaliar a implementação das ações de login e logout que necessitamos. O código a seguir demonstra como isto é feito: ~~~ [php] // Login de um usuário com usuário e senha fornecidos. $identity=new UserIdentity($username,$password); if($identity->authenticate()) Yii::app()->user->login($identity); else echo $identity->errorMessage; ...... // Logout do usuário corrente Yii::app()->user->logout(); ~~~ Estamos criando um novo objeto de UserIdentity e passando as credenciais de autenticação (ou seja, os valores de `$username` e `$password` enviados pelo usuário) para o construtor. Então simplesmente chamamos o método `authenticate()`. Se bem sucedido, passamos a informação de identidade no método [CWebUser::login], e armazenamos a informação de identidade em um armazenamento persistente (Por padrão, sessão do PHP) para recuperação em uma requisição subsequente. Caso a autenticação falhe, podemos verificar a propriedade `errorMessage` para obter mais informações sobre a falha. Seja autenticado ou não, um usuário pode ser verificado em qualquer parte do aplicativo usando `Yii::app()->user->isGuest`. Se usarmos armazenamento persistente como sessão (o padrão) e/ou cookie (discutiremos abaixo) para armazenar a informação de identidade, o usuário pode ficar conectado e responder as requisições subsequentes. Neste caso, não precisamos utilizar a classe UserIdentity e todo o processo de login em casa solicitação. Por sua vez a classe CWebUser irá cuidar automaticamente do carregamento das informações de identidade do armazenamento persistente e podemos utilizá-lo para determinar se `Yii::app()->user->isGuest` retorna verdadeiro (true) ou falso (false). Login baseado em Cookie ----------------------- Por padrão, um usuário será desconectado após determinado período de inatividade, que depende da [configuração da sessão](http://www.php.net/manual/en/session.configuration.php). Para alterar este comportamento, podemos setar a propriedade [allowAutoLogin|CWebUser::allowAutoLogin] do componente de usuário para verdadeiro (true) e passar o parâmetro duração para o método [CWebUser::login]. O usuário permanecerá conectado para a duração especificada mesmo que ele feche a janela do navegador. Note que esta funcionalidade requer que o navegador do usuário aceite cookies. ~~~ [php] // Manter o usuário conectado por 7 dias. // Tenha certeza de que allowAutoLogin está setado como verdadeiro (true) no componente de usuário. Yii::app()->user->login($identity,3600*24*7); ~~~ Conforme mencionado acima, quando o login baseado em cookie estiver habilitado, os estados armazenados através de [CBaseUserIdentity::setState] serão gravados no cookie. Na próxima vez que o usuário conectado entrar, estes estados serão lidos do cookie e preparados para serem acessados através de `Yii::app()->user`. Embora o Yii possua medidas para previnir que o estado do cookie sejam alterados no lado do cliente, sugerimos fortemente que informações sensíveis de seguranção não sejam armazenadas como estados. Em vez disso, estas informações devem ser restauradas no lado do servidor através da leitura de algum armazenamento persistente (por exemplo, banco de dados). Além disso, para qualquer aplicação Web séria, recomendamos a utilização das estratégias a seguir para aumentar a segurança de login baseado em cookie. * Quando um usuário efetua login com sucesso através de um formulário de login, geramos e armazenamos uma chave aleatória tando no estado do cookie quanto no armazenamento persistente do lado do servidor (por exemplo, banco de dados). * Mediante a uma requisição subsequente, quando a autenticação do usuário estiver sendo realizada via informação de cookie, podemos comparar as duas cópias da chave aleatória e assegurar uma validação anterior a entrada do usuário. * Se o usuário realizar login através de formulário novamente, a chave precisa ser regerada. Utilizando a estratégia acima, eliminamos a possibilidade que determinado usuário reutilize um estado antigo de um cookie, que possa conter informações de estado desatualizadas. Para implementar a estratégia acima, precisamos sobreescrever os dois métodos a seguir: * [CUserIdentity::authenticate()]: nele é onde a autenticação real é realizada. Se o usuário é autenticado, podemos regerar uma chave aletória e armazená-la na base de dados, bem como no estado da identidade com [CBaseUserIdentity::setState]. * [CWebUser::beforeLogin()]: nele é realizado a chamada quando o usuário será sendo conectado. Devemos verificar se a chave obtida do estado do cookie é a mesma que a do banco de dados. Filtro de Controle de Acesso (Access Control Filter) --------------------- Filtro de controle de acesso é um esquema de autorização preliminar que verifica se o usuário atual pode realizar a ação solicitada. A autorização é baseada no nome do usuário, endereço de IP do cliente e tipos de requisição. É fornecido como um filtro chamado ["accessControl"|CController::filterAccessControl]. > Tip|Dica: Filtros de controle de acesso são suficientes para cenários simples. Para controle de acesso mais complexo você pode usar o controle de acesso baseado em papéis de usuários (role-based access (RBAC)). Para controlar o acesso a ações em um controller nós instalamos o filtro de controle por sobrepor [CController::filters] (veja [Filter](/doc/guide/basics.controller#filter)) para mais detalhes sobre a instalação). ~~~ [php] class PostController extends CController { ...... public function filters() { return array( 'accessControl', ); } } ~~~ Acima especificamos que o [filtro de controle de acesso|CController::filterAccessControl] deve ser aplicado a todas as ações de `PostController`. As regras detalhadas de autorização usadas pelo filtro são especificadas por sobrepor [CController::accessRules] na classe controller. ~~~ [php] class PostController extends CController { ...... public function accessRules() { return array( array('deny', 'actions'=>array('create', 'edit'), 'users'=>array('?'), ), array('allow', 'actions'=>array('delete'), 'roles'=>array('admin'), ), array('deny', 'actions'=>array('delete'), 'users'=>array('*'), ), ); } } ~~~ O código acima especifica três regras, cada uma representada por um array. O primeiro elemento do array é um `'allow'` ou `'deny'` e o segundo é formado por pares do tipo nome-valor que especificam o padrão dos parâmetros da regra. As regras definidas acima são interpretadas como se segue: as ações `create` e `edit` não podem ser executadas por usuários anônimos (não identificados); a ação `delete` pode ser executada por usuários com o papel `admin`; e a ação `delete` não pode ser executada por ninguém. As regras de acesso são avaliadas uma a uma na ordem em que foram especificadas. A primeira regra que bater com o padrão atual (isto é, nome de usuário, papéis, IP do cliente, endereço) determina o resultado da autorização. Se esta regra for um `allow` a ação pode ser executada. Se for um `deny` a ação não pode ser executada; E se nenhuma das regras bater, a ação também poderá ser executada. > Tip|Dica: Para garantir que uma ação não seja executada sob certos contextos, > é benéfico sempre especificar uma regra pega-tudo do tipo `deny` no final > do conjunto de regras, como no exemplo: > ~~~ > [php] > return array( > // ... outras regras... > // a seguinte regra nega a ação 'delete' em todos os contextos > array('deny', > 'actions'=>array('delete'), > ), > ); > ~~~ > Sem esta regra, se nenhuma das regras batesse em algum contexto, a ação `delete` ainda seria executada. Uma regra de acesso pode bater nos seguintes contextos: - [actions|CAccessRule::actions]: especifica com quais ações esta regra bate. Deve ser um array de IDs de ações. A comparação não diferencia maiúsculas/minúsculas (case-insensitive). - [controllers|CAccessRule::controllers]: especifica com quais controllers esta regra bate. Deve ser um array de IDs de ações. A comparação não diferencia maiúsculas/minúsculas (case-insensitive). - [users|CAccessRule::users]: especifica com quais usuários esta regra bate. O [nome de usuário|CWebUser::name] é usado para a comparação. A comparação não diferencia maiúsculas/minúsculas (case-insensitive). Três caracteres especiais podem ser usados aqui: - `*`: qualquer usuário, inclusive anônimos e autenticados. - `?`: usuários anônimos. - `@`: usuários autenticados. - [roles|CAccessRule::roles]: especifica com quais papéis esta regra bate. Isto faz uso do [controle de acesso baseado em papéis](/doc/guide/topics.auth#role-based-access-control), característica que será descrita na próxima sub-seção. Em resumo, a regra é aplicada se [CWebUser::checkAccess] retornar true para um dos papéis. Note que você deve usar principalmente papéis em uma regra `allow` por que, por definição, um papel representa uma permissão de fazer algo. Note também que, embora usemos o termo 'papéis' aqui, seu valor pode realmente ser qualquer item de autenticação, incluindo papéis, tarefas e operações. - [ips|CAccessRule::ips]: especifica com quais endereços de IP de clientes esta regra bate. - [verbs|CAccessRule::verbs]: especifica com quais tipos de requisição (exemplo: `GET`, `POST`) esta regra bate. A comparação não diferencia maiúsculas/minúsculas (case-insensitive). - [expression|CAccessRule::expression]: especifica uma expressão PHP cujo valor indica se esta regra bate. Na expressão você pode a variável `$user` que se refere a `Yii::app()->user`. Manipulando o Resultado da Autorização ----------------------------- Quando a autorização falha, ou seja, o usuário não tem permissão de realizar a ação especificada, um dos dois cenários a seguir pode ocorrer: - Se o usuário não estiver logado e se a propriedade [loginUrl|CWebUser::loginUrl] do componente User estiver configurada para ser a URL da página de login, o navegador vai ser redirecionado para essa página. Note que, por padrão, [loginUrl|CWebUser::loginUrl] aponta para a página `site/login`. - Caso contrário, uma exceção HTTP com código de erro 403 vai ser exibida. Ao configurar a propriedade [loginUrl|CWebUser::loginUrl], pode-se fornecer uma URL relativa ou absoluta. Pode-se também fornecer um array que vai ser usado para gerar uma URL por chamar [CWebApplication::createUrl]. O primeiro elemento array deve especificar a [rota](/doc/guide/basics.controller#route) para a ação login do controller, e o restante, pares nome-valor de parâmetros GET. Por exemplo: ~~~ [php] array( ...... 'components'=>array( 'user'=>array( // este é, de fato, o valor padrão 'loginUrl'=>array('site/login'), ), ), ) ~~~ Se o navegador for redirecionado para a página de login e o login for bem-sucedido, podemos redirecioná-lo novamente para a página que gerou a falha de autorização. Como podemos saber a URL dessa página? Podemos conseguir essa informação da propriedade [returnUrl|CWebUser::returnUrl] do componente Usuário. Podemos assim fazer o seguinte para executar o redirecionamento: ~~~ [php] Yii::app()->request->redirect(Yii::app()->user->returnUrl); ~~~ Role-Based Access Control Controle de Acesso Baseado em Papéis ------------------------- Controle de Acesso Baseado em Papéis (Role-Based Access Control - RBAC) provê um simples porém poderoso controle de acesso centralizado. Por favor, consulte o [Artigo Wiki] (http://en.wikipedia.org/wiki/Role-based_access_control) para mais detalhes sobre a comparação do RBAC com outras formas de controle de acesso mais tradicionais. O Yii implementa o esquema de hierarquia RBAC através de seu componente de aplicação [authManager|CWebApplication::authManager]. A seguir, nós primeiro introduzimos os conceitos principais usados neste esquema; Após, descrevemos como definir dados de autorização. Por fim, mostramos como fazer uso dos dados de autorização para realizar a verificação de acesso. ### Visão Geral Um conceito fundamental do RBAC no Yii é o *item de autorização*. Um item de autorização é uma permissão de fazer algo (ex: criar novas postagens num blog, gerenciar usuários, etc). De acordo com sua granulidade e audiência, itens de autorização podem ser classificados como *operações*, *tarefas* e *papéis*. Um papel consiste de tarefas, uma tarefa consiste de operações e uma operação é uma permissão que é atômica. Por exemplo, podemos ter um sistema com um papel `administrador` que consista das tarefas `gerenciar postagens` e `gerenciar usuários`. A tarefa `gerenciar usuários` pode consistir das operações `criar usuário`, `atualizar usuário` e `excluir usuário`. Para maior flexibilidade o Yii também permite que um papel seja constituído de outros papéis e/ou operações, uma tarefa seja constituída de outras tarefas e uma operação seja constituída de outras operações. Um item de autorização é identificado exclusivamente por seu nome. Um item de autorização pode ser associado a uma *business rule* (regra de negócio). Uma business rule é um código em PHP que vai ser executado quando ocorrer a verificação de acesso com respeito ao item. O usuário terá a permissão de acesso representada pelo item somente quando a execução retornar true. Por exemplo, ao definir uma operação `updatePost` (atualizarPost), nós gostaríamos de adicionar uma business rule que verifique se ID do usuário é o mesmo que do autor do post de modo que somente o próprio autor possa ter permissão de atualizar um post. Using authorization items, we can build up an *authorization hierarchy*. An item `A` is a parent of another item `B` in the hierarchy if `A` consists of `B` (or say `A` inherits the permission(s) represented by `B`). An item can have multiple child items, and it can also have multiple parent items. Therefore, an authorization hierarchy is a partial-order graph rather than a tree. In this hierarchy, role items sit on top levels, operation items on bottom levels, while task items in between. Once we have an authorization hierarchy, we can assign roles in this hierarchy to application users. A user, once assigned with a role, will have the permissions represented by the role. For example, if we assign the `administrator` role to a user, he will have the administrator permissions which include `post management` and `user management` (and the corresponding operations such as `create user`). Now the fun part starts. In a controller action, we want to check if the current user can delete the specified post. Using the RBAC hierarchy and assignment, this can be done easily as follows: ~~~ [php] if(Yii::app()->user->checkAccess('deletePost')) { // delete the post } ~~~ Configuring Authorization Manager --------------------------------- Before we set off to define an authorization hierarchy and perform access checking, we need to configure the [authManager|CWebApplication::authManager] application component. Yii provides two types of authorization managers: [CPhpAuthManager] and [CDbAuthManager]. The former uses a PHP script file to store authorization data, while the latter stores authorization data in database. When we configure the [authManager|CWebApplication::authManager] application component, we need to specify which component class to use and what are the initial property values for the component. For example, ~~~ [php] return array( 'components'=>array( 'db'=>array( 'class'=>'CDbConnection', 'connectionString'=>'sqlite:path/to/file.db', ), 'authManager'=>array( 'class'=>'CDbAuthManager', 'connectionID'=>'db', ), ), ); ~~~ We can then access the [authManager|CWebApplication::authManager] application component using `Yii::app()->authManager`. Defining Authorization Hierarchy -------------------------------- Defining authorization hierarchy involves three steps: defining authorization items, establishing relationships between authorization items, and assigning roles to application users. The [authManager|CWebApplication::authManager] application component provides a whole set of APIs to accomplish these tasks. To define an authorization item, call one of the following methods, depending on the type of the item: - [CAuthManager::createRole] - [CAuthManager::createTask] - [CAuthManager::createOperation] Once we have a set of authorization items, we can call the following methods to establish relationships between authorization items: - [CAuthManager::addItemChild] - [CAuthManager::removeItemChild] - [CAuthItem::addChild] - [CAuthItem::removeChild] And finally, we call the following methods to assign role items to individual users: - [CAuthManager::assign] - [CAuthManager::revoke] Below we show an example about building an authorization hierarchy with the provided APIs: ~~~ [php] $auth=Yii::app()->authManager; $auth->createOperation('createPost','create a post'); $auth->createOperation('readPost','read a post'); $auth->createOperation('updatePost','update a post'); $auth->createOperation('deletePost','delete a post'); $bizRule='return Yii::app()->user->id==$params["post"]->authID;'; $task=$auth->createTask('updateOwnPost','update a post by author himself',$bizRule); $task->addChild('updatePost'); $role=$auth->createRole('reader'); $role->addChild('readPost'); $role=$auth->createRole('author'); $role->addChild('reader'); $role->addChild('createPost'); $role->addChild('updateOwnPost'); $role=$auth->createRole('editor'); $role->addChild('reader'); $role->addChild('updatePost'); $role=$auth->createRole('admin'); $role->addChild('editor'); $role->addChild('author'); $role->addChild('deletePost'); $auth->assign('reader','readerA'); $auth->assign('author','authorB'); $auth->assign('editor','editorC'); $auth->assign('admin','adminD'); ~~~ Once we have established this hierarchy, the [authManager|CWebApplication::authManager] component (e.g. [CPhpAuthManager], [CDbAuthManager]) will load the authorization items automatically. Therefore, we only need to run the above code one time, and NOT for every request. > Info: While the above example looks long and tedious, it is mainly for > demonstrative purpose. Developers will usually need to develop some administrative user > interfaces so that end users can use to establish an authorization > hierarchy more intuitively. Using Business Rules -------------------- When we are defining the authorization hierarchy, we can associate a role, a task or an operation with a so-called *business rule*. We may also associate a business rule when we assign a role to a user. A business rule is a piece of PHP code that is executed when we perform access checking. The returning value of the code is used to determine if the role or assignment applies to the current user. In the example above, we associated a business rule with the `updateOwnPost` task. In the business rule we simply check if the current user ID is the same as the specified post's author ID. The post information in the `$params` array is supplied by developers when performing access checking. ### Access Checking To perform access checking, we first need to know the name of the authorization item. For example, to check if the current user can create a post, we would check if he has the permission represented by the `createPost` operation. We then call [CWebUser::checkAccess] to perform the access checking: ~~~ [php] if(Yii::app()->user->checkAccess('createPost')) { // create post } ~~~ If the authorization rule is associated with a business rule which requires additional parameters, we can pass them as well. For example, to check if a user can update a post, we would pass in the post data in the `$params`: ~~~ [php] $params=array('post'=>$post); if(Yii::app()->user->checkAccess('updateOwnPost',$params)) { // update post } ~~~ ### Using Default Roles Many Web applications need some very special roles that would be assigned to every or most of the system users. For example, we may want to assign some privileges to all authenticated users. It poses a lot of maintenance trouble if we explicitly specify and store these role assignments. We can exploit *default roles* to solve this problem. A default role is a role that is implicitly assigned to every user, including both authenticated and guest. We do not need to explicitly assign it to a user. When [CWebUser::checkAccess] is invoked, default roles will be checked first as if they are assigned to the user. Default roles must be declared in the [CAuthManager::defaultRoles] property. For example, the following configuration declares two roles to be default roles: `authenticated` and `guest`. ~~~ [php] return array( 'components'=>array( 'authManager'=>array( 'class'=>'CDbAuthManager', 'defaultRoles'=>array('authenticated', 'guest'), ), ), ); ~~~ Because a default role is assigned to every user, it usually needs to be associated with a business rule that determines whether the role really applies to the user. For example, the following code defines two roles, `authenticated` and `guest`, which effectively apply to authenticated users and guest users, respectively. ~~~ [php] $bizRule='return !Yii::app()->user->isGuest;'; $auth->createRole('authenticated', 'authenticated user', $bizRule); $bizRule='return Yii::app()->user->isGuest;'; $auth->createRole('guest', 'guest user', $bizRule); ~~~