
在某个时候,应用程序需要授权。这意味着不同级别的访问权限在网站上(或任何其他方面)表现不同。它可以是从查看数据到整个区域对一组用户不可访问的任何内容。
在非单页应用程序(SPA)中,声明或角色与数据或应用程序的某个区域相关联,用户要么拥有此角色或声明,要么没有。在 SPA 中也是如此,但有一个巨大的免责声明。SPA 会下载到浏览器中。此时浏览器对代码拥有完全控制权。恶意用户可以更改代码来达到他的目的。
由于 SPA 无法被保护,SPA 中的身份验证和授权仅仅是用户体验。所有有意义的安全性都必须在 Web 服务器上完成。本文不涵盖保护您的 API 免受攻击。我建议观看 Pluralsight 的视频或阅读针对您的服务器技术的安全性论文。
本文的目的是向您展示我如何为我的 Angular 1.x SPA 添加授权用户体验。
安全范围
我已经确定了需要授权的 UI 的 3 个区域:元素(HTML)、路由和数据。
提醒一下,保护 SPA 不能替代保护服务器。客户端上的权限仅仅是为了让诚实的人保持诚实,并为用户提供良好的体验。
3 个区域的详细信息:
元素
您需要隐藏特定的 HTML 元素。它可以是标签、包含数据的表格、按钮或页面上的任何元素。
路由
您需要隐藏整个路由。在某些情况下,您不希望用户访问视图。通过保护路由,用户无法导航到该视图。相反,他们将看到”您无权导航到此视图”的消息。
数据
有时仅仅隐藏视图中的元素是不够的。精明的用户可以简单地查看源代码并在 HTML 源代码中看到隐藏的数据,或者观察它流向浏览器。我们想要的是首先不检索数据。
添加安全性很棘手。起初我尝试在 HTTP API(在客户端)上限制访问。我很快意识到这不会起作用。用户可能无法直接访问数据,但这并不意味着他们无法间接访问数据。在 HTTP API 层(通常是应用程序中最低的层之一),我们无法判断调用的上下文,因此无法对其应用安全考虑。
下面我提供了编码示例:
代码
我为授权检查代码创建了一个服务。这是授权的核心。所有授权请求都使用此服务来检查用户是否被授权执行特定操作。
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;
}
});
授权指令
授权指令可以应用于任何您想要对没有特定访问级别的用户隐藏的 HTML 元素。如果用户拥有访问令牌作为其声明的一部分,他们被允许看到该元素。如果他们没有,它就会对他们隐藏。
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);
});
}
};
}]);
元素
我在应用程序中大量依赖选项卡。我将授权指令应用于我想要对没有适当声明的用户隐藏的选项卡。
<tabset>
<tab ng-cloak heading="Users" authorize="{{allowUserManagement}}">
...html content
</tab>
</tabset>
路由
我使用的是 ui-router
。不幸的是,对于那些不使用的人,我没有开箱即用的 AngularJS 路由器的代码。
在 $stateChangeStart
中我验证路由。这是该事件中的代码。
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams){
AuthenticationManager.authenticate(event, toState, toParams);
});
授权路由的函数。如果它被授权,路由被允许继续。如果它没有被授权,会向用户显示消息,并将他们引导到主页。
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();
}
}
在这个路由器定义中,您会注意到一个名为 ‘authorization’ 的属性。如果用户拥有此声明,他们被允许继续。
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' }
});
});
数据
在某些情况下,您不希望向服务器请求数据。如果用户拥有声明,他们将被允许发出请求。
上面文章开头的 AuthorizationContext
显示了 authoriedExecution
的代码。这里您可以看到它的用法。
AuthorizationContext.authorizedExecution(Keys.authorization.allowUserManagement, function(){
//execute code, if the loggedin user has rights.
});
如我上面提到的,这不能替代保护服务器。此代码用于提供出色的用户体验。
作者:Chuck Conway 专注于软件工程和生成式人工智能。在社交媒体上与他联系:X (@chuckconway) 或访问他的 YouTube。