详解Angular开发中的登陆与身份验证


Posted in Javascript onJuly 27, 2016

前言

由于 Angular 是单页应用,会在一开始,就把大部分的资源加载到浏览器中,所以就更需要注意验证的时机,并保证只有通过了验证的用户才能看到对应的界面。

本篇文章中的身份验证,指的是如何确定用户是否已经登陆,并确保在每次与服务器的通信中,都能够满足服务器的验证需求。注意,并不包括对具体是否具有某一个权限的判断。

对于登陆,主要是接受用户的用户名密码输入提交到服务器进行验证处理验证响应在浏览器端构建身份验证数据

实现身份验证的两种方式

目前,实现身份验证的方法,主要有两个大类:

Cookies

传统的浏览器网页,都是使用 Cookies 来验证身份,实际上,浏览器端的应用层里,基本不用去管身份验证的事情,Cookies 的设置,由服务器端完成,在提交请求的时候,由浏览器自动附加对应的 Cookies 信息,所以在 JavaScript 代码中,不需要为此编写专门的代码。但每次请求的时候,都会带上全部的 Cookies 数据,

随着 CDN 的应用,移动端的逐渐兴起, Cookies 越来越不能满足复杂的、多域名下的身份验证需求。

密钥

实际上基于密钥的身份验证并不是最近才兴起,它一直存在,甚至比 Cookies 历史更长。当浏览器在请求服务器的时候,将密钥以特定的方式附加在请求中,比如放在请求的头部( headers )。为此,需要编写专门的客户端代码来管理。

最近出现的基于 JSON 的 Web 密钥(JSON Web Token)标准,便是典型的使用密钥来实现的身份验证。

在 Web 应用中,如果是构造 API ,则应优先考试使用密钥方式。

处理登陆

登陆是身份验证第一步,通过登陆,才能够组织起来对应的身份验证数据。

需要使用单独的登陆页吗?

登陆页的处理,有两种方式:

单独的登陆页,在登陆完成后,跳转到单页应用之中,这样做可以对单页应用的资源进行访问控制,防止非登陆用户访问,适合后台或者管理工具的应用场景。但实际上降低了单页应用的用户体验
在单页应用之内执行登陆,这样更符合单页应用的设计理念,比较适合大众产品的场景,因为恶意的人总是能够拿到你单页应用的前端代码

单独的登陆页

一般情况下,使用单独的登陆页的目的在于保护登陆后跳转的页面不被匿名用户访问。因此,在登陆页里,构造一个表单,直接采用传统的表彰提交方式(非 Ajax),后端验证用户名密码成功后,输出登陆后单面应用页面的 HTML 。

在这种情况下,可以直接将身份验证信息放在输出的 HTML 里,比如,可以使用 Jade 构造一个这样的页面:

<!-- dashboard.jade -->
doctype html
html
 head
  link(rel="stylesheet", href="/assets/app.e1c2c6ea9350869264da10de799dced1.css")
 body
  script.
   window.token = !{JSON.stringify(token)};
  div.md-padding(ui-view)
  script(src="/assets/app.84b1e53df1b4b23171da.js")

后端在用户名密码验证成功之后,可以采用如下的方式来渲染输出 HTML :

return res.render('dashboard', {
 token: token
});

Angular 应用一启动,便可以进行需要使用身份验证的通信。而且还保证了只有登陆成功的用户才可以进入这个页面。

单页应用内登陆的组织

对于多视图的 Angular 应用,一般会采用路由,在页面之内,一般有固定的侧边栏菜单,或者顶部导航菜单,正文区域由路由模块来控制。

下面的示例代码,使用的是 Angular Material 来组织页面,路由模块使用的是 ui-router 。在应用打开的时候,有专门的加载动画,加载完成之后,显示的页面,使用 AppController 这个控制器,对于没有登陆的用户,会显示登陆表单,登陆完成之后,页面分为三大部分,一是顶部面包屑导航二是侧边栏菜单,另外就是路由控制的正文部分

代码如下:

<body ng-app="app" layout="row">
 <div id="loading">
  <!--页面加载的提示-->
 </div>
 <div flex layout="row" ng-cloak ng-controller="AppController" ng-init="load()">
  <div ng-if="!isUserAuth", ng-controller="LoginController">
   <!--登陆表单-->
  </div>
  <div ng-if="isUserAuth" flex layout="row">
   <md-sidenav flex="15" md-is-locked-open="true" class="stop-text-select bbmd-sidebar md-whiteframe-4dp">
    <!--侧边栏菜单-->
   </md-sidenav>
   <md-content flex layout="column" role="main">
    <md-toolbar class="stop-text-select md-whiteframe-glow-z1">
     <!--顶部菜单-->
    </md-toolbar>
    <md-content>
     <!--路由-->
     <div ui-view class="md-padding"></div>
    </md-content>
   </md-content>
  </div>
 </div>
</body>

对于 Loading 动画,是在 AppController 之外的,可以在 AppController 的代码中,对其进行隐藏。这样达到了所有 CSS / JavaScript 加载完成之后 Loading 就消失的目的。

AppController 中有一个变量 isUserAuth ,初始化的时候是 false ,当本地存储的会话信息验证有效,或者登陆完成之后,这个值便会置为 ture ,由于 ng-if 的控制,便可以实现隐藏登陆表单、显示应用内容的目的。要注意,这里只有使用 ng-if 而不是 ng-show/ng-hide ,前者才会真正的删除和增加 DOM 元素,而后者只是修改某个 DOM 元素的 CSS 属性,这点很重要,只有这样,才能够保证登陆完成之后,再加载单页应用中的内容,防止还没有登陆,当前路由中的控制器代码就直接执行了。

为什么客户端也要加密密码

一个比较理想的基于用户名和密码的登陆流程是这样的:

    1.浏览器端获取用户输入的密码,使用 MD5 一类的哈希算法,生成固定长度的新密码,如 md5(username + md5(md5(password))) ,再将密码哈希值和用户名提交给后端

    2.后端根据用户名获取对应的盐,使用用户名和密码哈希值,算出一个密文,根据用户名和密文去数据库查询

    3.如果查询成功,则生成密钥,返回给浏览器,并执行第 4 步

    4.后端生成新的盐,根据新的盐和浏览器提交的密码哈希值,生成新的密文。在数据库中更新盐和密文

可能有 80% 的人无法理解为什么要把一个登陆做得这么复杂。这可能要写一篇专门的文章才解释得清楚。在这里先解释一下为什么浏览器端要对密码做哈希,原因如下:

    1.从源头上保护用户的密码,保证只有做按键记录才可以拿到用户的原始密码
    2.就算网络被窃听,又没有使用 https ,那么被偷走的密码,也只是哈希之后的,最多影响用户在这个服务器里的数据,而不影响使用相同密码的其它网站
    3.就算是服务器的所有者,都无法获取用户的原始密码
这种做法,使得用户的最大风险,也只是当前这个应用中的数据被窃取。不会扩大损失范围,绝不会出现 CSDN 之流的问题。

登陆成功的通知

对于有些应用,并不是所有的页面都需要用户登陆的,可能是进行某些操作的时候,才需要登陆。在这种情况下,登陆完成之后,必须要通知整个应用。这可以使用广播这个功能。

简易代码如下:

angular
 .module('app')
 .controller('LoginController', ['$rootScope', LoginController]);

function LoginController($rootScope) {
 // 登陆成功之后调用的函数
 function afterLoginSuccess() {
  $rootScope.$broadcast('user.login.success', {
   // 需要传输的数据
  });
 }
}

在其它的控制器中,便可以监听这个广播,并执行登陆成功之后需要进行的操作,如获取列表或者详情:

$scope.$on('user.login.success', function(handle, data){
 // 处理
});

身份验证信息

登陆成功之后,服务器返回了密钥,之后的 API 请求都需要带上密钥,而且请求返回的响应,还需要检查是否是关于身份信息失效的错误。这一系列的工作比较繁琐,应该是自动完成才行。

保存

密钥的保存,大概有如下几个办法:

    1.Cookies:前面已经提到了,这个并不推荐使用。同时,它还有最大 4k 的限制

    2.sessionStorage:tab 页内有效,一旦关闭,或者打开了新的 tab 页,sessionStorage 是不能共享的

    3.localStorage:较为理想的存储方式,除非清理浏览器数据,否则 localStorage 存储的数据会一直存在

    4.Angular 单例 Service:存储在应用之内得话,刷新后数据会丢失,当然也不能 tab 页之间共享
比较好的办法是,身份验证信息存储在 localStorage 里,但在应用启动时,初始化到 Angular 的单例 Service 中。

在请求中加入身份验证信息

身份验证信息的目的,是为了向服务器表明身份,获取数据。所以,在请求中需要附加身份验证信息。

一般的应用中,身份验证信息都是放在请求的 headers 头部中。如果在每次请求的时候,一一设置 headers ,那就太费时费力了。Angular 中的 $httpProvider 提供了一个拦截器 interceptors ,通过它可以实现对每一个请求和响应的统一处理。

添加拦截器的方式如下:

angular
 .module('app')
 .config(['$httpProvider', function($httpProvider){
  $httpProvider.interceptors.push(HttpInterceptor);
 }]);

HttpInterceptor 的定义方式如下:

angular
 .module('app')
 .factory('HttpInterceptor', ['$q', HttpInterceptor]);

function HttpInterceptor($q) {
 return {
  // 请求发出之前,可以用于添加各种身份验证信息
  request: function(config){
   if(localStorage.token) {
    config.headers.token = localStorage.token;
   }
   return config;
  },
  // 请求发出时出错
  requestError: function(err){
   return $q.reject(err);
  },
  // 成功返回了响应
  response: function(res){
   return res;
  },
  // 返回的响应出错,包括后端返回响应时,设置了非 200 的 http 状态码
  responseError: function(err){
   return $q.reject(err);
  }
 };
}

拦截器提供了对发出请求到返回响应的全生命周期处理,一般可以用来做下面几个事情:

    1.统一在发出的请求中添加数据,如添加身份验证信息

    2.统一处理错误,包括请求发出时出的错(如浏览器端的网络不通),还有响应时返回的错误

    3.统一处理响应,比如缓存一些数据等

    4.显示请求进度条

在上面的示例代码中,当 localStorage 中包括 token 这个值时,就在每一个请求的头部,添加一个 token 值。

失效及处理

一般的,后端应该在 token 验证失败时,将响应的 http 状态码设置为 401 ,这样,在拦截器的 responseError 中便可以统一处理:

responseError: function(err){
 if(-1 === err.status) {
  // 远程服务器无响应
 } else if(401 === err.status) {
  // 401 错误一般是用于身份验证失败,具体要看后端对身份验证失败时抛出的错误
 } else if(404 === err.status) {
  // 服务器返回了 404
 }
 return $q.reject(err);
}

总结

其实,只要服务器返回的状态码不是 200 ,都会调用 responseError ,可以在这里,统一处理并显示错误。

以上内容是关于Angular开发应用中的登陆与身份验证的相关知识,希望对大家学习Angular有所帮助。

Javascript 相关文章推荐
jQuery去掉字符串起始和结尾的空格(多种方法实现)
Apr 01 Javascript
JS.getTextContent(element,preformatted)使用介绍
Sep 21 Javascript
JavaScript中的ArrayBuffer详细介绍
Dec 08 Javascript
Javascript中的Callback方法浅析
Mar 15 Javascript
JavaScript中用getDate()方法返回指定日期的教程
Jun 09 Javascript
浅析jQuery 遍历函数,javascript中的each遍历
May 25 Javascript
javascript中的后退和刷新实现方法
Nov 10 Javascript
详解在WebStorm中添加Vue.js单文件组件的高亮及语法支持
Oct 21 Javascript
Node.js中DNS模块学习总结
Feb 28 Javascript
使用 Vue 实现一个虚拟列表的方法
Aug 20 Javascript
JavaScript中的this原理及6种常见使用场景详解
Feb 14 Javascript
node中短信api实现验证码登录的示例代码
Jan 20 Javascript
jQuery+css实现非常漂亮的水平导航菜单效果
Jul 27 #Javascript
jQuery+CSS实现简单切换菜单示例
Jul 27 #Javascript
AngularJS 指令详细介绍
Jul 27 #Javascript
js中遍历Map对象的方法
Jul 27 #Javascript
angular.js分页代码的实例
Jul 27 #Javascript
jQuery基于函数重载实现自定义Alert函数样式的方法
Jul 27 #Javascript
jquery动态遍历Json对象的属性和值的方法
Jul 27 #Javascript
You might like
PHP下常用正则表达式整理
2010/10/26 PHP
XHProf报告字段含义的解析
2016/05/17 PHP
php 变量引用与变量销毁机制详细介绍
2016/12/05 PHP
PHP实现的一致性Hash算法详解【分布式算法】
2018/03/31 PHP
javascript Split方法,indexOf方法、lastIndexOf 方法和substring 方法
2009/03/21 Javascript
学习ExtJS Column布局
2009/10/08 Javascript
jquery 简单应用示例总结
2013/08/09 Javascript
$.each与$().each的区别示例介绍
2014/03/20 Javascript
javascript实现点击后变换按钮显示文字的方法
2015/05/13 Javascript
jQuery.trim() 函数及trim()用法详解
2015/10/26 Javascript
jQuery插件DataTables分页开发心得体会
2017/08/22 jQuery
Vue-cli Eslint在vscode里代码自动格式化的方法
2018/02/23 Javascript
浅析前端路由简介以及vue-router实现原理
2018/06/01 Javascript
node+express框架中连接使用mysql(经验总结)
2018/11/10 Javascript
优雅的使用javascript递归画一棵结构树示例代码
2019/09/22 Javascript
python爬虫中get和post方法介绍以及cookie作用
2018/02/08 Python
Python实现的网页截图功能【PyQt4与selenium组件】
2018/07/12 Python
Django 导出项目依赖库到 requirements.txt过程解析
2019/08/23 Python
Python封装成可带参数的EXE安装包实例
2019/08/24 Python
Django框架下静态模板的继承操作示例
2019/11/08 Python
Python 中@property的用法详解
2020/01/15 Python
python3定位并识别图片验证码实现自动登录功能
2021/01/29 Python
Whittard官方海外旗舰店:英国百年茶叶品牌
2018/02/22 全球购物
新加坡一家在线男士皮具品牌:Faire Leather Co.
2019/12/01 全球购物
本科生求职简历的自我评价
2013/10/21 职场文书
应届生的求职推荐信范文
2013/11/30 职场文书
工程招投标邀请书
2014/01/26 职场文书
《月光启蒙》教学反思
2014/03/01 职场文书
《桂花雨》教学反思
2014/04/12 职场文书
退休教师欢送会致辞
2015/07/31 职场文书
医院中层管理人员培训心得体会
2016/01/11 职场文书
小学生法制教育心得体会
2016/01/14 职场文书
2016廉政教育学习心得体会
2016/01/25 职场文书
经典格言警句:没有热忱,世间便无进步
2019/11/13 职场文书
javascript的setTimeout()使用方法总结
2021/11/20 Javascript
Django+Nginx+uWSGI 定时任务的实现方法
2022/01/22 Python