В какой-то момент приложение нуждается в авторизации. Это означает, что разные уровни доступа ведут себя по-разному на веб-сайте (или в любом другом месте). Это может быть что угодно: от просмотра данных до целых областей, которые недоступны для группы пользователей.
В традиционных приложениях (не SPA) claim или роль связаны с данными или областью приложения — либо у пользователя есть эта роль или claim, либо нет. В SPA это то же самое, но с большой оговоркой. SPA загружается в браузер. На этом этапе браузер имеет полный контроль над кодом. Злоумышленник может изменить код в своих целях.
Поскольку SPA не могут быть защищены, аутентификация и авторизация в SPA — это просто пользовательский опыт. Вся значимая безопасность должна быть реализована на веб-сервере. Эта статья не охватывает защиту вашего API от атак. Я рекомендую посмотреть видео на Pluralsight или прочитать статью, которая рассматривает безопасность для вашей серверной технологии.
Цель этой статьи — показать вам, как я добавил пользовательский опыт авторизации в мое Angular 1.x SPA.
Области безопасности
Я определил 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;
}
});
Директива Authorize
Директива authorize может быть применена к любому HTML-элементу, который вы хотите скрыть от пользователей без определенного уровня доступа. Если у пользователя есть токен доступа как часть его claims, ему разрешено видеть элемент. Если нет, он скрыт от них.
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);
});
}
};
}]);
Элементы
Я активно использую вкладки в своем приложении. Я применяю директиву authorize к вкладке, которую я хочу скрыть от пользователей без надлежащих claims.
<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’. Если у пользователя есть этот claim, ему разрешено продолжить.
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' }
});
});
Данные
В некоторых случаях вы не хотите делать запрос к серверу для получения данных. Если у пользователя есть claim, ему будет разрешено сделать запрос.
Приведенный выше 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.