在某个时刻,应用程序需要授权。这意味着不同级别的访问在网站上表现不同(或任何其他情况)。它可以是从查看数据到整个区域都无法被一组用户访问的任何内容。
在非单页应用程序(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 是一位 AI 工程师,拥有近 30 年的软件工程经验。他构建实用的 AI 系统——内容管道、基础设施代理和解决实际问题的工具——并分享他沿途的学习成果。在社交媒体上与他联系:X (@chuckconway) 或访问他的 YouTube 和 SubStack。