
Em algum momento uma aplicação precisa de autorização. Isso significa que diferentes níveis de acesso se comportam de forma diferente em um site (ou qualquer coisa, na verdade). Pode ser qualquer coisa, desde ver dados até áreas inteiras que não são acessíveis por um grupo de usuários.
Em aplicações que não são Single Page Applications (SPA), um claim ou role é associado com dados ou uma área da aplicação, ou o usuário tem esse role ou claim ou não tem. Em uma SPA é a mesma coisa, mas com um grande aviso. Uma SPA é baixada para o navegador. Neste ponto o navegador tem controle total sobre o código. Uma pessoa mal-intencionada pode alterar o código para fazer o que quiser.
Como SPAs não podem ser protegidas, autenticação e autorização em uma SPA é simplesmente experiência do usuário. Toda segurança significativa deve ser feita no servidor web. Este artigo não cobre como proteger sua API contra ataques. Eu recomendo assistir um vídeo do Pluralsight ou ler um artigo que aborde segurança para sua tecnologia de servidor.
A intenção deste artigo é mostrar como adicionei uma experiência de usuário de autorização à minha SPA Angular 1.x.
Escopos de Segurança
Identifiquei 3 áreas da UI que precisam de autorização: Elementos (HTML), Rotas, e Dados.
Apenas um lembrete, proteger uma SPA não é substituto para proteger o servidor. Permissões no cliente é simplesmente para manter as pessoas honestas honestas e para fornecer ao usuário uma boa experiência.
As 3 áreas em detalhes:
Elementos
Você precisará ocultar elementos HTML específicos. Pode ser um rótulo, uma tabela com dados, um botão, ou qualquer elemento na página.
Rotas
Você vai querer ocultar rotas inteiras. Em certos casos você não quer que o usuário acesse uma view. Ao proteger a rota, um usuário não pode navegar para a view. Em vez disso, será mostrada uma mensagem “Você não está autorizado a navegar para esta view”.
Dados
Às vezes ocultar os elementos na view não é suficiente. Um usuário astuto pode simplesmente ver o código fonte e ver os dados ocultos no código HTML ou assistir o streaming para o navegador. O que queremos é que os dados não sejam recuperados em primeiro lugar.
Adicionar segurança é complicado. No início tentei restringir o acesso na API HTTP (no cliente). Rapidamente percebi que isso não funcionaria. Um usuário pode não ter acesso direto aos dados, mas isso não significa que não tenha acesso indireto aos dados. Na camada da API HTTP (geralmente uma das mais baixas na aplicação) não podemos dizer o contexto da chamada e, portanto, não podemos aplicar preocupações de segurança a ela.
Abaixo forneci exemplos de código:
Código
Criei um serviço para o código de verificação de autorização. Este é o coração da autorização. Todas as solicitações de autorização usam este serviço para verificar se o usuário está autorizado para a ação específica.
angular.module('services')
.service('AuthorizationContext',function(_, Session){
this.authorizedExecution = function(key, action){
//Looking for the claim key that was passed in. If it exists in the claim set, then execute the action.
Session.claims(function(claims){
var claim = findKey(key, claims);
//If Claim was found then execute the call.
//If it was not found, do nothing
if(claim !== undefined){
action();
}
});
};
this.authorized = function(key, callback){
//Looking for the claim key that was passed in. If it exists in the claim set, then execute the action.
Session.claims(function(claims){
var claim = findKey(key, claims);
//If they don't have any security key, then move forward and authorization.
var valid = claim !== undefined;
callback(valid);
});
};
//this.agencyViewKey = '401D91E7-6EA0-46B4-9A10-530E3483CE15';
function findKey(key, claims){
var claim = _.find(claims, function(item){
return item.value === key;
});
return claim;
}
});
Diretiva Authorize
A diretiva authorize pode ser aplicada a qualquer elemento HTML que você queira ocultar de usuários sem um nível específico de acesso. Se o usuário tem o token de acesso como parte de seus claims, ele pode ver o elemento. Se não tem, é ocultado dele.
angular.module('directives')
.directive('authorize', ['$compile', 'AuthorizationContext', function($compile, AuthorizationContext) {
return {
restrict: 'A',
replace: true,
//can't have isolated the scope in a shared directive
link:function ($scope, element, attributes) {
var securityKey = attributes.authorize;
AuthorizationContext.authorized(securityKey, function(authorized){
var el = angular.element(element);
el.attr('ng-show', authorized);
//remove the attribute, otherwise it creates an infinite loop.
el.removeAttr('authorize');
$compile(el)($scope);
});
}
};
}]);
Elementos
Dependo muito de abas na minha aplicação. Aplico a diretiva authorize à aba que quero ocultar de usuários sem os claims apropriados.
<tabset>
<tab ng-cloak heading="Users" authorize="{{allowUserManagement}}">
...html content
</tab>
</tabset>
Rotas
Estou usando o ui-router
. Infelizmente para aqueles que não estão, não tenho código para o roteador padrão do AngularJS.
No $stateChangeStart
eu autentico a rota. Este é o código nesse evento.
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams){
AuthenticationManager.authenticate(event, toState, toParams);
});
A função que autoriza a rota. Se está autorizada, a rota pode continuar. Se não está autorizada, uma mensagem é exibida ao usuário e ele é direcionado para a página inicial.
function authorizedRoute(toState, location, toaster, breadCrumbs){
if(toState.authorization !== undefined){
AuthorizationContext.authorized(toState.authorization, function(authorized){
if(!authorized){
toaster.pop('error', 'Error', 'You are not authorized to view this page.');
location.path("/search");
} else {
breadCrumbs();
}
});
} else{
breadCrumbs();
}
}
Nesta definição de rota você notará uma propriedade chamada ‘authorization’. Se o usuário tem este claim, ele pode prosseguir.
angular.module('agency',
[
'ui.router',
'services'
])
.config(function config($stateProvider){
$stateProvider.state( 'agency', {
url: '/agency',
controller: 'agency.index',
templateUrl: 'agency/agency.tpl.html',
authenticate: true,
authorization:'401d91e7-6ea0-46b4-9a10-530e3483ce15',
data:{ pageTitle: 'Agency' }
});
});
Dados
Em alguns casos, você não quer fazer uma solicitação ao servidor para os dados. Se o usuário tem o claim, ele será autorizado a fazer a solicitação.
O AuthorizationContext
acima no início do artigo mostra o código para authoriedExecution
. Aqui você vê seu uso.
AuthorizationContext.authorizedExecution(Keys.authorization.allowUserManagement, function(){
//execute code, if the loggedin user has rights.
});
Como mencionei acima, isso não é substituto para proteger o servidor. Este código funciona para fornecer uma experiência de usuário maravilhosa.
Autor: Chuck Conway é especialista em engenharia de software e IA Generativa. Conecte-se com ele nas redes sociais: X (@chuckconway) ou visite-o no YouTube.