Authentication
The most common way in the design of permissions is RBAC's role-based access control. The basic idea is that various permissions for system operations are not directly granted to specific users, but rather a role collection is established between the user set and the permission set. Each role corresponds to a corresponding set of permissions.
Once the user has been assigned the appropriate role, the user has all operational permissions for the role. The advantage of this is that there is no need to assign permissions every time a user is created, just assign the corresponding role of the user, and the role's permission changes are much less than that of the user's permission changes, which will simplify user's permission management and reduce system overhead.
In a single-page application built by Angular, we need to do some extra things to implement such an architecture. From the overall project perspective, there are about 3 places that front-end engineers need to deal with.
1. UI processing (judging whether some content on the page is displayed based on the permissions owned by the user)
2. Routing processing (when the user accesses a URL that it does not have permission to access, it jumps to a page with an error message)
3. HTTP request processing (when we send a data request, if the returned status is 401 or 403, it is usually redirected to a page with an error message)
Implementation of access identity control
First, you need to obtain all permissions of the current user before Angular starts, and then the more elegant way is to store this mapping relationship through a service. For whether the content on a page is displayed according to permissions by the UI. After processing these, we need to add an additional "permission" attribute to add a route when adding a route, and assign it a value to indicate which roles with permissions can jump to this URL, and then listen to the routeChangeStart event through Angular to verify whether the current user has access to this URL. Finally, an HTTP interceptor is needed to monitor when the status returned by a request is 401 or 403, jump to the page to an error prompt page. The roughly this is what it is, it seems a bit too many, but it is actually very easy to deal with.
Return 401, execute loginCtrl, return 403, execute PermissionCtrl.
Get the mapping relationship of permission before Angular runs
The Angular project is started through ng-app, but in some cases we hope that the Angular project will be under our control. For example, in this case, I hope to obtain all permission mapping relationships of the currently logged in user and then start the Angular App. Fortunately, Angular itself provides this method, that is, angular.bootstrap().
var permissionList; angular.element(document).ready(function() { $.get('/api/UserPermission', function(data) { permissionList = data; angular.bootstrap(document, ['App']); }); });Those who read carefully may notice that the use of $.get() is used here, and there is no error using jQuery instead of Angular's $resource or $http, because at this time, Angular has not started yet, and we cannot use its function.
Further use the above code to put the obtained mapping relationship into a service as a global variable.
// app.js var app = angular.module('myApp', []), permissionList; app.run(function(permissions) { permissions.setPermissions(permissionList) }); angular.element(document).ready(function() { $.get('/api/UserPermission', function(data) { permissionList = data; angular.bootstrap(document, ['App']); }); }); // common_service.js angular.module('myApp') .factory('permissions', function ($rootScope) { var permissionList; return { setPermissions: function(permissions) { permissionList = permissions; $rootScope.$broadcast('permissionsChanged') } }; });After obtaining the permissions set of the current user, we archive this set into the corresponding service, and then did 2 more things:
(1) Store permissions in factory variables so that they are always in memory, realizing the role of global variables, but not polluting the namespace.
(2) Broadcast event through $broadcast, when permissions change.
1. How to determine the UI component's visible and hidden power based on permissions
Here we need to write a directive ourselves, which will display or hide elements based on permission relationships.
<!-- If the user has edit permission the show a link --> <div has-permission='Edit'> <a href="/#/courses/{{ id }}/edit"> {{ name }}</a> </div> <!-- If the user doesn't have edit permission then show text only (Note the "!" before "Edit") --> <div has-permission='!Edit'> {{ name }} </div>Here I see an ideal situation that you can pass the has-permission property verification permission name, and if the current user has it, it will be displayed, and if there is no, it will be hidden.
angular.module('myApp').directive('hasPermission', function(permissions) { return { link: function(scope, element, attrs) { if(!_.isString(attrs.hasPermission)) throw "hasPermission value must be a string"; var value = attrs.hasPermission.trim(); var notPermissionFlag = value[0] === '!'; if(notPermissionFlag) { value = value.slice(1).trim(); } function toggleVisibilityBasedOnPermission() { var hasPermission = permissions.hasPermission(value); if(hasPermission && !notPermissionFlag || !hasPermission && notPermissionFlag) element.show(); else element.hide(); } toggleVisabilityBasedOnPermission(); scope.$on('permissionsChanged', toggleVisabilityBasedOnPermission); } }; });Expand the previous factory:
angular.module('myApp') .factory('permissions', function ($rootScope) { var permissionList; return { setPermissions: function(permissions) { permissionList = permissions; $rootScope.$broadcast('permissionsChanged') }, hasPermission: function (permission) { permission = permission.trim(); return _.some(permissionList, function(item) { if(_.isString(item.Name)) return item.Name.trim() === permission }); } }; });2. Permission-based access on the route
The idea of this part of implementation is as follows: When we define a route, we add a permission attribute, and the value of the attribute is what permissions we have to access the current url. Then, we keep listening to the URL changes through the routeChangeStart event. Each time we change the URL, check whether the URL to be redirected meets the conditions, and then decide whether it will be redirected successfully or to the error prompt page.
app.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/viewCourses.html', controller: 'viewCoursesCtrl' }) .when('/unauthorized', { templateUrl: 'views/error.html', controller: 'ErrorCtrl' }) .when('/courses/:id/edit', { templateUrl: 'views/editCourses.html', controller: 'editCourses', permission: 'Edit' }); });mainController.js or indexController.js (in short, it is the parent layer controller)
app.controller('mainAppCtrl', function($scope, $location, permissions) { $scope.$on('$routeChangeStart', function(scope, next, current) { var permission = next.$$route.permission; if(_.isString(permission) && !permissions.hasPermission(permission)) $location.path('/unauthorized'); }); });The hasPermission written before is still used here, and these things are highly reusable. This is done. Before each view route jump, just judge whether it has permission to jump in the controller of the parent container.
3. HTTP request processing
This should be relatively easy to deal with, and the idea is very simple. Because Angular applications recommend RESTful style excuses, the use of HTTP protocol is very clear. If the status code returned by the request is 401 or 403, it means there is no permission, so you can jump to the corresponding error prompt page.
Of course, we cannot manually verify and forward each request once, so we must definitely need a total filter. The code is as follows:
angular.module('myApp') .config(function($httpProvider) { $httpProvider.responseInterceptors.push('securityInterceptor'); }) .provider('securityInterceptor', function() { this.$get = function($location, $q) { return function(promise) { return promise.then(null, function(response) { if(response.status === 403 || response.status === 401) { $location.path('/unauthorized'); } return $q.reject(response); }); }; }; });By writing this, you can almost realize the permission management and control of the front-end part in this front-end separation mode.
Form Verification
AngularJS front-end verification directive
var rcSubmitDirective = { 'rcSubmit': function ($parse) { return { restrict: "A", require: [ "rcSubmit", "?form" ], controller: function() { this.attempted = false; var formController = null; this.setAttempted = function() { this.attempted = true; }; this.setFormController = function(controller) { formController = controller; }; this.needsAttention = function(fieldModelController) { if (!formController) return false; if (fieldModelController) { return fieldModelController.$invalid && (fieldModelController.$dirty || this.attempted); } else { return formController && formController.$invalid && (formController.$dirty || this.attempted); } }; }, compile: function() { return { pre: function(scope, formElement, attributes, controllers) { var submitController = controllers[0]; var formController = controllers.length > 1 ? controllers[1] : null; submitController.setFormController(formController); scope.rc = scope.rc || {}; scope.rc[attributes.name] = submitController; }, post: function(scope, formElement, attributes, controllers) { var submitController = controllers[0]; var formController = controllers.length > 1 ? controllers[1] : null; var fn = $parse(attributes.rcSubmit); formElement.bind("submit", function(event) { submitController.setAttempted(); if (!scope.$$phase) scope.$apply(); if (!formController.$valid) return; scope.$apply(function() { fn(scope, { $event: event }); }); }); } }; } }; } }; } }; } }; } }; } }; } }; } };Verification passed
<form name="loginForm" novalidate ng-app="LoginApp" ng-controller="LoginController" rc-submit="login()"> <div ng-class="{'has-error': rc.loginForm.needsAttention(loginForm.username)}"> <input name="username" type="text" placeholder="Username" required ng-model="session.username" /> <span ng-show="rc.form.needsAttention(loginForm.username) && loginForm.username.$error.required">Required</span> </div> <div ng-class="{'has-error': rc.loginForm.needsAttention(loginForm.password)}"> <input name="password" type="password" placeholder="Password" required ng-model="session.password" /> <span ng-show="rc.form.needsAttention(loginForm.password) && loginForm.password.$error.required">Required</span> </div> <div> <button type="submit" value="Login"> <span>Login</span> </button> </div> </form>The style is as follows
Login() will be called after the front-end verification passes.