详解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 相关文章推荐
使用prototype.js进行异步操作
Feb 07 Javascript
一直复略了的一个问题,关于表单重复提交
Feb 15 Javascript
js 操作select和option常用代码整理
Dec 13 Javascript
得到form下的所有的input的js代码
Nov 07 Javascript
JavaScript动态操作表格实例(添加,删除行,列及单元格)
Nov 25 Javascript
JavaScript整除运算函数ceil和floor的区别分析
Apr 14 Javascript
js获取滚动距离的方法
May 30 Javascript
jquery实现简单合拢与展开网页面板的方法
Sep 01 Javascript
Bootstrap基本插件学习笔记之Tooltip提示工具(18)
Dec 08 Javascript
jQuery实现点击下拉框中的值累加到文本框中的方法示例
Oct 28 jQuery
swiper在vue项目中loop循环轮播失效的解决方法
Sep 15 Javascript
JavaScript实现H5接金币功能(实例代码)
Feb 22 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 DataGrid 实现代码
2009/08/12 PHP
php实现把url转换迅雷thunder资源下载地址的方法
2014/11/07 PHP
PHP常用函数之格式化时间操作示例
2019/10/21 PHP
laravel数据库查询结果自动转数组修改实例
2021/02/27 PHP
JavaScript 申明函数的三种方法 每个函数就是一个对象(一)
2009/12/04 Javascript
通过javascript设置css属性的代码
2009/12/28 Javascript
JS仿flash上传头像效果实现代码
2011/07/18 Javascript
非主流的textarea自增长实现js代码
2011/12/20 Javascript
js实现可拖动DIV的方法
2013/12/17 Javascript
jquery mobile动态添加元素之后不能正确渲染解决方法说明
2014/03/05 Javascript
jQuery中prepend()方法用法实例
2014/12/25 Javascript
jQuery控制Div拖拽效果完整实例分析
2015/04/15 Javascript
原生js实现移动端瀑布流式代码示例
2015/12/18 Javascript
基于css3新属性transform及原生js实现鼠标拖动3d立方体旋转
2016/06/12 Javascript
js选择器全面解析
2016/06/27 Javascript
jQuery选择器实例应用
2017/01/05 Javascript
jquery实现楼层滚动效果
2018/01/01 jQuery
关于Vue中$refs的探索浅析
2020/11/05 Javascript
Python安装第三方库的3种方法
2015/06/21 Python
python实现SMTP邮件发送功能
2020/06/16 Python
python中is与双等于号“==”的区别示例详解
2017/11/21 Python
Flask框架响应、调度方法和蓝图操作实例分析
2018/07/24 Python
如何使用Python处理HDF格式数据及可视化问题
2020/06/24 Python
简单了解如何封装自己的Python包
2020/07/08 Python
解决pytorch 数据类型报错的问题
2021/03/03 Python
使用html5 canvas绘制圆环动效
2019/06/03 HTML / CSS
加拿大最大的箱包及旅游配件零售商:Bentley Leathers
2017/07/19 全球购物
Java中实现多态的机制是什么?
2014/12/07 面试题
师范生教师实习自我鉴定
2013/09/27 职场文书
《中国的气候》教学反思
2014/02/23 职场文书
文化宣传方案
2014/03/13 职场文书
教师教学评估方案
2014/05/09 职场文书
2014年小学教学工作总结
2014/11/13 职场文书
大学班干部竞选稿
2015/11/20 职场文书
2016年员工政治思想表现评语
2015/12/02 职场文书
pandas中DataFrame数据合并连接(merge、join、concat)
2021/05/30 Python