原文:

https://medium.com/opinionated-angularjs/7bbf0346acec

认证

最常用的表单认证就是用户名(或者邮件)和密码登录。这就表示要实现一个用户可以输入他们证书的登录表单。这样的表单可能像这样:

 <form name="loginForm" ng-submit="login(credentials)" novalidate>
<label for="username">Username:</label>
<input id="username" type="text" ng-model="credentials.username"/> <label for="password">Password:</label>
<input id="password" type="password" ng-model="credentials.password"/> <button type="submit">Login</button>
</form>

(注意:下面有一个升级版的,这个只是一个简单示例)

因为Angular提供了form,我们在提交的时候可以用ngSubmit指令触发一个作用域函数。注意到我们传了credentials作为一个参数,而不是用$scope.credentials,这让函数更容易进行单元测试和避免和环境作用域的耦合。其对应的控制器可能像这样:

  // var app = angular.module('myApp', [‘ui.router’]) or
 var app = angular.module('myApp', ['ngRoute'])
app.controller('LoginController', [
'$scope',
'$rootScope',
'AUTH_EVENTS',
'AuthService',
function ($scope, $rootScope, AUTH_EVENTS, AuthService) {
$scope.credentials = {
username: '',
password: ''
};
$scope.login = function (credentials) {
AurhService.login(credentials)
.then(function () {
$rootScope.$broadcast(AUTH_EVENTS.loginSuccess);
}, function () {
$rootScope.$broadcast(AUTH_EVENTS.loginFailed);
});
};
}])

第一我们注意到缺省的真实逻辑代码。这是有意这样做的,为了将表单从实际的认证逻辑中解耦。通常一个好的方式就是将尽可能多的逻辑抽离出你的控制器,以服务来填充。AngularJS的控制器应该只管理$scope对象(通过监视和操作),而不应该变得厚重。

会话改变交流

认证是一种会影响全部应用的东西。因为这样我选择使用events事件(和$broadcast广播)和用户会话交流。将可用的事件代码集中放在一个地方是一个良好的实践。我喜欢将它们作为应用常量

 app.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'
});

关于AngularJS的应用常量很棒的事是它们可以被注入到服务(services)中,这样在你的单元测试中可以轻易用假数据代替。同时以后你可以很轻松地重命名它们(改变值),而不用更改一大坨文件。用户的角色也用了相同的技巧:

 app.constant('USER_ROLES', {
all: '',
admin: 'admin',
editor: 'editor',
guest: 'guest'
});

如果你想要给所有编辑者和管理员一样的权限,只需要简单地将editor的值改为admin。

AuthService认证授权服务

认证和授权的相关逻辑最好放在一个服务里面:

 app.factory('AuthService', [
'$http',
'Session',
function($http, Session){
return {
login: function login(credentials){
return $http.post('/login', credentials)
.then(function(res){
Session.create(res.id, res.userid, res.role);
});
},
isAuthenticated: function(){
return !!Session.userid;
},
isAuthorized: function(authorizedRoles){
if(!angular.isArray(authorizedRoles)){
authorizedRoles = [authorizedRoles];
} return (this.isAuthenticated()
&& authorizedRoles.indexOf(Session.userRole) !== -1);
}
};
}
]);

为了更深的分离有关认证,我用了另一个服务(单体对象,service风格)来保留用户的会话信息。这个对象的细节依赖于你后端的接口,但是我在下面给出了一个通用的例子。你可以还想要用一个包装器封装$http(例如一个“API”provider服务),这样你就可以通过你的app的config函数配置URL。

 app.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;
});

一单一个用户登进来,你会想要在某个地方(可能右上角)显示他的信息。为了做到这个,用户对象必须引用$scope对象,最好是在一个可以被整个应用访问到的地方。毫不犹豫想到的第一个选择就是$rootScope,但是我会避免经常使用$rootScope(事实上我只在全局事件广播的时候才用到它)。代替地,我偏爱在应用的根节点中定义一个控制器,或者至少在某个够高的节点,body标签会是一个好的候选:

<body ng-controller="ApplicationController">
  ...
</body>

ApplicationController是一个有许多全局应用逻辑的容器,而且是Angular的run函数的代替品。因为它在$scope树的根节点,其他所有的作用域都会继承它(除了独立作用域)。是个定义当前用户对象的好地方:

 app.controller('ApplicationController', [
'$scope',
'USER_ROLES',
'AuthService',
function($scope, USER_ROLES, AuthService){
$scope.currentUser = null;
$scope.userRoles = USER_ROLES;
$scope.isAuthorized = AuthService.isAuthorized;
}
]);

我们并没有指定当前用户对象,我们只是初始化作用域的属性。这里的作用是创建一个占位符,供我们以后(从任意子作用域)存储对象。这是一个技巧将一个可用的变量定义在高层次的作用域,而不是你实际指定的地方。如果你想要了解更多,首先得明白原型继承的概念。

除了初始化currentUser属性,我还添加了USER_ROLES和isAuthorized函数。这些应该只用在模板表达式中,而不是在其它控制器中,因为那样会使控制器的测试性变得复杂。

Access control访问控制

授权a.k.a访问控制在AngularJS中并不真实存在。因为我们谈论的是一个客户端应用,所有的源码都是有关客户端的。我们并没有阻止用户篡改相关代码来获得访问特定的视图和界面元素。我们只是进行了简单的显示控制。如果你需要真实的授权你不得不在服务端进行,但那不在本文之类。

estricting element visibility限制元素显示

AngularJS有几种指令可以显示隐藏一个元素,这些指令都是基于作用与属性或者表达式:ngShow, ngHide, ngIf和ngSwitch。前两个会使用style属性来隐藏元素,后两个则会将元素从DOM中删除。

前两种最好用在当表达式频繁改变,而且元素不包含很多模板逻辑和作用域引用。原因是任何模板逻辑内部有隐藏元素,将会导致每次digest循环的时候被重新评估,使应用变得缓慢。后两种方案将会完全移除DOM元素,包括事件处理程序和作用域绑定。把工作交给浏览器来干,但大多数时候都较效率。因为用户访问不会经常改变,使用ngIF或者ngSwitch是最好的选择:

 <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>

Switch的例子是假设一个用户只能有一个角色。

Restricting route access限制路由访问

大多数时候你想要禁止访问整个页面而不是隐藏单个元素。在路由中用自定义的数据。我们可以指定哪个角色可以通过访问。这里使用了UI Router(https://github.com/angular-ui/ui-router):

 app.config(function ($stateProvider, USER_ROLES) {
$stateProvider.state('dashboard', {
url: '/dashboard',
templateUrl: 'dashboard/index.html',
data: {
authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor]
}
});
});

下面为使用ngRoute的例子

 app.config([
'$routeProvider',
'USER_ROLES',
function($routeProvider, USER_ROLES){
$routeProvider.when('/dashboard', {
templateUrl: 'dashboard/index.html',
resolve: {
authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor]
}
});
}
]);

下一步当路由改变的时候,我们需要每次检查这个属性。这里涉及到监听$locationChangeStart(for ngRoute)或者$statechangeStart(for ui.router)事件:

 app.run([
'$rootScope',
'AUTH_EVENTS',
'AuthService',
function($rootScope, AUTH_EVENTS, AuthService){
$rootScope.$on('$locationChangeStart', function(event, next){
var authorizedRoles = next.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);
}
}
});
}
]);

当一个用户未被授权访问一个页面(因为他没有登录或者没有权限),当下一个页面的过渡将会被阻止,所以用户仍会停留在当前页面。下一步,我们广播一个事件给其他模块。我建议在页面中加上loginDialog指令,当未认证事件被触发时显示,还有当未授权事件触发时应该显示一个错误信息。

Session expiration会话过期

认证几乎是服务端的事了。不管你怎么实现,你的后端都要验证用户证书,处理像会话过期和撤销访问的事。这表示你的API有时将会响应认证错误。标准的方式是通过HTTP状态码来交流。常用的认证错误状态码有:

1.401 Unanthorized(未授权)----用户没有登入

2.403 Forbidden(禁止) ---- 用户登入了但不允许访问。

3.419 Authentication Timeout(认证超时) ---- 会话过期

5.440 Login Timeout(登入超时,只有微软才会) ---- 会话过期

最后两个并不是标准的部分,但可能会被用到。最好的官方办法就是通过401交流会话超时。不管怎样,你的loginDialog应该在API返回401,419或者440自动出现,还有当403时,错误应该被显示出来。首先我们想要广播同样的基于HTTp响应状态码的notAuthenticated/notAuthorized事件。为了达到目的,我们给$httpProvider添加一个拦截器:

 app.config(['$httpProvider', function($httpProvider){
$httpProvider.intercepters.push([
'$injector',
function($injector){
return $injector.get('AuthInterceptor');
}
]);
}])
.factory('AuthInterceptor', [
'$rootScope',
'$q',
'AUTH_EVENTS',
function($rootScope, $q, AUTH_EVENTS){
return {
responseError: function(response){
var event;
switch(response.status){
case 401:
event = AUTH_EVENTS.notAuthenticated;
break;
case 403:
event = AUTH_EVENTS.notAuthorized;
break;
case 419:
case 440:
event = AUTH_EVENTS.sessionTimeout
} $rootScope.$broadcast(event, response);
return $q.reject(response);
}
};
}
  ]);

这是一个简单的Auth拦截器实现。在Github上有一个更好的项目也做同样事情,但是还包括了一个httpBuffer服务,当HTTP错误返回,将会阻止API请求,直到用户再次登入,才会顺序触发它们。

Issues with the login form登录表单的一些问题

许多用户将会以来一个密码管理来保存他们的证书,这样他们以后就可以轻松登录了。如果你使用文章开头的简单示例,你会发现密码管理器很难和AngularJS表单一起工作。有两个问题将会发生:

1.表单提交并没有被检测到,所以证书没有被存储。

2.自动填充域并没有被AngularJS提取。

这些问题可以被绕开,但是这涉及到一个iframe和一个超时函数。如果你更偏爱你的代码整洁,而不是丑陋的hack,你可能想要将登录表单完全的从AngularJS独立出去,转而替代地依赖过时的服务端渲染,否则还是使用这个升级版的表单:

 <iframe src="sink.html" frameborder="0" name="sink" style="display:none;"></iframe>

 <form name="loginForm" action="sink.html" target="sink" method="post"
ng-submit="login(credentials)"
novalidate
form-autofill-fix> <label for="username">Username:</label>
<input id="username" type="text" ng-model="credentials.username"/> <label for="password">Password:</label>
<input id="password" type="password" ng-model="credentials.password"/> <button type="submit">Login</button> </form>

不同之处在于我们新加了<iframe>和给<form>元素新加了一些属性。通过提供action,target和method,当调用ngSubmit函数时,表单会被post到iframe的sink.html中。这种方式浏览器的正常表单处理逻辑会被剔除,地址栏不会跳转到sink.html(至少在主窗口中)。密码管理器会认识到这是一个正常的表单提交,然后请求存储证书到密钥中。Sink.html页面只是一个空的HTML文档,在你的index.html同级。我会在这样写上注释解释为什么该文件存在(这样其他开发者就不会删除它)。

注意:不要忘了method=”post”,f否则你的证书会被添加到查询字符串参数后面,这样会暴露到浏览器的历史记录中。

现在密码管理器可以存储证书了,我们不得不支持恢复这些证书。这叫做自动填充(又叫自动完成)。自动填充的问题是大多数浏览器不会触发被填充的输入域的事件。因为这样,AngularJS没有办法知道域内的内容是否被改变了,然后就不会更新$scope对象。结果当提交一个按理来说完成的登录表单的时候,只会因为提供了无效的证书而被拒绝访问。这就是formAutofillFix指令存在的意义:

 app.directive('formAutofillFix', [
'$timeout',
function($timeout){
return function(scope, element, attrs){
element.prop('method', 'post'); if(!attrs.length) return; $timeout(function(){
element.off('submit')
.on('submit', function(event){
event.preventDefault();
element.find('input, textarea, select')
.trigger('input')
.trigger('change')
.trigger('keydown'); scope.$apply(attrs.ngSubmit);
});
});
};
}
]);

这个指令通过一个函数将会重新绑定submit事件,这个函数会触发每个输入域的事件(强制Angular更新作用域),等待Angular完成digest循环,就会调用ngSubmit函数。当表单被提交的时候,任何作用域都会被更新成自动填充值。这种方式的一个缺陷就是表单验证不会工作了,因为自动填充值在表单提交之前对于作用于来说是无效的。如果你需要表单验证正常工作,这里有一个polyfill(https://github.com/tbosch/autofill-event)可以提前触发change事件。

有无数变数和其他的选择,最终你几乎要依赖后端的实现。真正安全的办法还是在于服务端,而且记住总是使用HTTPS。

翻译:AngularJS应用的认证技术的更多相关文章

  1. www-authenticate与BASE-64认证技术

    www-authenticate是一种简单的用户身份认证技术.很多验证都采用这种验证方式,尤其在嵌入式领域中.优点:方便缺点:这种认证方式在传输过程中采用的用户名密码加密方式为BASE-64,其解码过 ...

  2. 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_03-用户认证技术方案-Oauth2协议

    2.2 Oauth2认证 2.2.1 Oauth2认证流程 第三方认证技术方案最主要是解决认证协议的通用标准 问题,因为要实现 跨系统认证,各系统之间要遵循一定的 接口协议. OAUTH协议为用户资源 ...

  3. 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_02-用户认证技术方案-单点登录

    2 用户认证技术方案 2.1 单点登录技术方案 分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如: MySQL.Redis,考虑性能要求,通常存储在R ...

  4. 【angularJS】前后台分离,angularJS使用Token认证

    参考资料: [AngularJS系列(4)] 那伤不起的provider们啊~ (Provider, Value, Constant, Service, Factory, Decorator):htt ...

  5. Coding 两步认证技术介绍

    什么是两步认证 在介绍两步认证之前,首先来看下目前主流的几种认证方式. 上图中的认证方式大体上可以分为三大类 1.You know : 比如密码,这种只有我们知道的 2.You are : 比如指纹, ...

  6. 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_04-用户认证技术方案-SpringSecurityOauth2

    2.3 Spring security Oauth2认证解决方案 本项目采用 Spring security + Oauth2完成用户认证及用户授权,Spring security 是一个强大的和高度 ...

  7. AC认证技术

    一.认证方式 Dkey认证(数字密钥认证) 1)免认证key,形同usb,插入即通过认证 2)免审计key,也是上网不被记录审计. 单点登录 登录了某点,其他点都能访问:例如登录了支付宝淘宝就不用登录 ...

  8. 看看有哪些 Web 认证技术.

    BASIC 认证 BASIC 认证(基本认证)是从 HTTP/1.0 就定义的认证方式. BASIC 认证会将"用户名:密码"经过 Base64 加密后放入请求头部的 Author ...

  9. 《parsing techniques》中文翻译和正则引擎解析技术入门

    http://parsing-techniques.duguying.net/ (中文版) https://swtch.com/~rsc/regexp/ https://blog.csdn.net/m ...

随机推荐

  1. ESB数据采集思路

    昨天接到一个任务,使用公司的ESB,调用别人的接口,把得到的数据存储到mysql数据库当中,这里简单记录解决思路,方便以后查看. 1.拿到一个网站的地址,使用火狐浏览器的firebug工具,查看其传递 ...

  2. [zz] Install VSFTP

    The first two letters of vsftpd stand for "very secure" and the program was built to have ...

  3. UML类图新手入门级介绍

    UML类图新手入门级介绍 举一个简单的例子,来看这样一副图,其中就包括了UML类图中的基本图示法. 首先,看动物矩形框,它代表一个类(Class).类图分三层,第一层显示类的名称,如果是抽象类,则就用 ...

  4. Virtualizing WrapPanel VS toolkit:WrapPanel

    用toolkit:WrapPanel的时候,LIST太大,内存不行,等下我试试 Virtualizing WrapPanel这个 http://www.codeproject.com/Articles ...

  5. Mysql表基本操作

    一. 创建表的方法 语法:create table 表名( 属性名数据类型完整约束条件, 属性名数据类型条完整约束件, ......... 属性名数据类型 ); (1)举例:1 create tabl ...

  6. sql存在一个表而不在另一个表中的数据

    (转)A.B两表,找出ID字段中,存在A表,但是不存在B表的数据.A表总共13w数据,去重后大约3W条数据,B表有2W条数据,且B表的ID字段有索引. 方法一 使用 not in ,容易理解,效率低  ...

  7. setEllipsize(TruncateAt where)

    void android.widget.TextView.setEllipsize(TruncateAt where) public void setEllipsize (TextUtils.Trun ...

  8. IE下无法保存Cookie和Session问题

    最近在做新的Web项目时,因为一个验证码无法保存在Cookie中,或者更确切地说是IE下无法保存Cookie的问题纠结了整整一天时间,考虑了多种原因,单步调试了不下三十次,也没有结果.甚至在无奈之下改 ...

  9. SetTimeOut jquery的作用

    1. SetTimeOut() 1.1 SetTimeOut()语法例子 1.2 用SetTimeOut()执行Function 1.3 SetTimeOut()语法例子 1.4 设定条件使SetTi ...

  10. iOS学习笔记之typedef

    typedef unsigned long long weiboId; typedef 定义一个使用方便的类型,谓之为“宏定义“. unsigned long long 是一种无符号的长长整型.本应该 ...