The most common way of identity authentication is to use the user name (or email) and password to do the login operation. This means implementing a landing form so that users can log in with their personal information. The form looks like this:
<form name="LoginForm" ng-controller="Logincontroller" Ng-submit="Login (Credentials)" novalidate> <label for ="username">Username:</label> <input type="text" id="username"ng-model=" Credentials.username "> <label for ="password">Password:</label> <input type= "password" id= "password"ng-model=" Credentials.password "> <button type="Submit">Login</button></form>
Since this is a angular-powered form, we use the NGSUBMIT directive to trigger the function when uploading the form. Note that we pass on the personal information to the function that uploads the form, instead of directly using the $scope. Credentials this object. This makes the function easier to unit-test and reduces the coupling between this function and the current Controller scope. The Controller looks like this:
.controller(‘LoginController‘, function ($scope, $rootScope, AUTH_EVENTS, AuthService) { $scope.credentials = { username: ‘‘, password: ‘‘ }; $scope.login = function (credentials) { AuthService.login(credentials).then(function (user) { $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); $scope.setCurrentUser(user); }, function () { $rootScope.$broadcast(AUTH_EVENTS.loginFailed); }); };javascript:void(0);})
We note that there is a lack of practical logic here. This Controller was made so that the logic of identity authentication was decoupled from the form. It's a good idea to pull logic out of our controllers as much as possible and put them all in the services. AngularJS controllers should only manage objects in the $scope (either by watching or manually) rather than taking on too much weight.
Notice the change of Session
Identity authentication can affect the state of the entire application. For this reason I recommend using events ($broadcast) to notify the user session of the change. It's a good idea to define all the possible event codes in a middle area. I like to use constants to do this thing:
.constant(‘AUTH_EVENTS‘, { loginSuccess: ‘auth-login-success‘, loginFailed: ‘auth-login-failed‘, logoutSuccess: ‘auth-logout-success‘, sessionTimeout: ‘auth-session-timeout‘, notAuthenticated: ‘auth-not-authenticated‘, notAuthorized: ‘auth-not-authorized‘})
The good thing about constants is that they can be injected anywhere else, just like services. This makes it easy for constants to be called by our unit-test. Constants also allows you to easily rename them later without having to change a bunch of files. The same trick was applied to user roles:
.constant(‘USER_ROLES‘, { all: ‘*‘, admin: ‘admin‘, editor: ‘editor‘, guest: ‘guest‘})
If you want to give editors and administrators the same permissions, you just need to simply change ' editor ' to ' admin '.
The Authservice
The logic associated with identity authentication and authorization (access control) is best placed on the same service:
. Factory (' Authservice ', function ($http, Session) {var authservice = {}; Authservice.login = function (credentials) {return $http. Post ('/login ', credentials). Then (function (res) {session.create (res.data.id, Res.data.user.id,res.data.user.role);return res.data.user; });}; authservice.isauthenticated = function () {return!! Session.userid;}; authservice.isauthorized = function (authorizedroles) {if (!angular.isarray (authorizedroles)) {authorizedroles = [Authorizedroles]; }return (authservice.isauthenticated () &&Authorizedroles.indexof (Session.userrole)!==-1);}; return authservice;})
To further stay away from the concerns of identity authentication, I use another service (a singleton, using the service style) to save the user's session information. The details of the session are dependent on the backend implementation, but I'll give you a more general example:
.service(‘Session‘, function () { this.create = function (sessionId, userId, userRole) { this.id = sessionId; this.userId = userId; this.userRole = userRole; }; this.destroy = function () { this.id = null; this.userId = null; this.userRole = null; }; return this;})
Once the user has logged in, his message should be displayed in some places (such as the top right-hand corner of the user's avatar, etc.). To achieve this, the user object must be referenced $scope object, preferably a place that can be called globally. Although $rootScope is the first choice that is obviously easy to see, I try to restrain myself and not use too much $rootScope (in fact I only use $rootScope in global event broadcasts). To do this in the way I like, is to define a controller in the root node of the application, or at least somewhere above the Dom tree. The label is a good choice:
<body ng-controller="ApplicationController"> ...</body>
Applicationcontroller is the container for the global logic of the application and a choice for running the Angular run method. So it will be at the root of the $scope tree, and all other scopes will inherit it (in addition to isolating scope). This is a good place to define the CurrentUser object:
. Controller (' Applicationcontroller ', function ($scope,User_roles,Authservice) {$scope. CurrentUser = null; $scope. userroles = User_roles; $scope. isauthorized = authservice.isauthorized; $scope. Setcurrentuser = function (user) { $scope. currentUser = user; };}) Specific examples: var app = Angular.module (' managerapp.localstorecontrollers1 ', [' tm.pagination ']); App.controller (' Localstorctrl ', function ($scope, $http, $window, locals) {// alert (1);// //store data// $scope. CurrentUser = null; Locals.set ("username", "Zhuliu"); Locals.set ("password", User.password); $scope. CurrentUser = Locals.get ("username", ""); //reading data //Console.log (locals.get ("username", ""); // $window. Localstorage.name = 12;// Console.log ($ Window.localStorage.name);// localstorage.usenamesss= "Wangwu";// Console.log (LOCALSTORAGE.USENAMESSS); });//Data storage ================app.factory (' Locals ', [' $window ', function ( $window) { return { //Store individual properties: sEt:function (key,value) { $window. Localstorage[key] = value;& nbsp; }, //reading a single property get:function (key,defaultvalue) { return $ Window.localstorage[key] | | defaultvalue; }, //storage objects, JSON format setobject:function (key,value) { $window. Localstorage[key] = json.stringify (value); }, //Read Objects getobject:function (key) { return json.parse ($window. Localstorage[key] | | ' {} '); }  }]); binding data can be directly bound to {{CurrentUser}}} can obtain the numerical value directly;
We do not actually allocate currentUser objects, we simply initialize the properties on the scope so that CurrentUser can be accessed later. Unfortunately, we cannot simply assign a new value to CurrentUser in the sub-scope because that would result in shadow property. This is used to pass the original type (strings, numbers, booleans,undefined and null) in place of the result of passing the original type as a reference. To prevent shadow property, we want to use the setter function. If you want to learn more about Angular scopes and prototype inheritance, read understanding Scopes.
Access control
Authentication, that is, access control, in fact, AngularJS does not exist. Because we are the client application, all the source code is in the user's hand. There is no way to prevent users from tampering with the code to obtain an authenticated interface. All we can do is show control. If you need real authentication, you need to do it on the server side, but this is beyond the scope of this article.
Restricting the display of elements
AngularJS has directives that control the display or hiding of elements based on scopes or expressions: Ngshow, Nghide, Ngif, and Ngswitch. The first two use a <style>
property to hide the element, but the latter two remove the element from the DOM.
The first, which is the hidden element, is best used when the expression is frequently changed and does not contain too many template logic and scope references on the element. The reason is that in the hidden elements, the template logic of these elements will still be recalculated in each digest loop, allowing the application performance to degrade. The second way, removing an element removes all handler and scope bindings on this element. Changing the DOM is a lot of work for browsers (in some scenarios, compared to ngshow/nghide), but in many cases the cost is worth it. Because user access information does not change frequently, using Ngif or Ngshow is the best choice:
<div ng-if="CurrentUser">Welcome, {{currentuser.name}}</div><div ng-if="isauthorized (userroles.admin)">You ' re admin.</div><div ng-switch on ="Currentuser.role"> <div ng-switch-when="Userroles.admin">You ' re admin.</div> <div ng-switch-when="Userroles.editor">You ' re editor.</div> <div ng-switch-default>You ' re something else.</div></div>
Restricting routing access
Most of the time you want to make the whole page inaccessible, not just hide an element. If you can reroute (in UI Router, routing is also called state) using a custom data structure, we can identify which user roles are allowed to access what. The following example uses the UI Router style, but these also apply to Ngroute.
.config(function ($stateProvider, USER_ROLES) { $stateProvider.state(‘dashboard‘, { url: ‘/dashboard‘, templateUrl: ‘dashboard/index.html‘, data: { authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor] } });})
Next, we need to check each route change (that is, when the user jumps to another page). This requires listening for $routeChangStart (in Ngroute) or $stateChangeStart (UI Router) events:
.run(function ($rootScope, AUTH_EVENTS, AuthService) { $rootScope.$on(‘$stateChangeStart‘, function (event, next) { var authorizedRoles = next.data.authorizedRoles; if (!AuthService.isAuthorized(authorizedRoles)) { event.preventDefault(); if (AuthService.isAuthenticated()) { // user is not allowed $rootScope.$broadcast(AUTH_EVENTS.notAuthorized); } else { // user is not logged in $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); } } });})
Session Aging
Identity authentication is mostly server-side things. No matter how you implement it, your backend will do real validation and processing of user information such as Session Aging and access control. This means that your API will sometimes return some authentication errors. The standard error code is the HTTP status. These error codes are commonly used:
- 401 Unauthorized?—? The user is not a logged in
- 403 Forbidden?—? The user is logged on but isn ' t allowed access
- 419 Authentication Timeout (non standard)?—? Session has expired
- The. Login Timeout (Microsoft only)?—? Session has expired
The latter two are not standard content, but may be widely used. The best official judgment session expired error code is 401. In any case, your login dialog should appear immediately when the API returns 401, 419, 440, or 403. In general, we want to broadcast and time based on these HTTP return codes, for which we add an interceptor to the $httpProvider:
. config (function ($httpProvider) {$httpProvider. Interceptors.push ([' $injector ',function ($injector) {return $injector. Get (' Authinterceptor '); }]);}). Factory (' Authinterceptor ', function ($rootScope, $q,auth_events) {return {responseerror:function (response) {$rootScope. $broadcast ({401:auth_events.notauthenticated,403:auth_events.notauthorized,419:auth_events.sessiontimeout,440:auth_events.sessiontimeout}[response.status], response);return $q. Reject (response); }};})
This is just a simple implementation of a certified interceptor. There's a great project on Github, it does the same thing, and uses the Httpbuffer service. When an HTTP error code is returned, it blocks further requests from the user until the user logs on again, and then resumes the request.
logon dialog box directives
When a session expires, we need the user to re-enter his account. To prevent him from losing his current job, the best way to do this is to eject the login dialog box instead of jumping to the login page. This dialog needs to listen for the notauthenticated and Sessiontimeout events, so when one of the events is triggered, the dialog box opens:
.directive(‘loginDialog‘, function (AUTH_EVENTS) { return { restrict: ‘A‘, template: ‘<div ng-if="visible" ng-include="\‘login-form.html\‘">‘, link: function (scope) { var showDialog = function () { scope.visible = true; }; scope.visible = false; scope.$on(AUTH_EVENTS.notAuthenticated, showDialog); scope.$on(AUTH_EVENTS.sessionTimeout, showDialog) } };})
As long as you like, this dialog box can be expanded casually. The main idea is to reuse existing login form templates and Logincontroller. You need to write the following code on each page:
<div login-dialog ng-if="!isLoginPage"></div>
Note the Isloginpage check. A failed login will trigger notauthenticated time, but we do not want to display this dialog box on the landing page because it is superfluous and strange. That's why we don't put the Login dialog box on the landing page. So define a $scope in Applicationcontroller. Isloginpage is reasonable.
Save User State
In the user refresh their page, still save the logged in user information is a single page application certification inside a tricky link. Because the client is present in all States, the refresh clears the user information. To fix this problem, I usually implement an API that returns data from the logged-in current user (such as/profile), which is launched in the AngularJS application (for example, in the "Run" function). The user data is then saved in the Session service or $rootScope, as if the user had logged in. Alternatively, you can embed the user data directly into the index.html so that no additional requests are available. The third way is to put user data in a cookie or localstorage, but it can make it difficult to log out or empty user data.
Angular identity authentication issues