前言css
因为 Angular 是单页应用,会在一开始,就把大部分的资源加载到浏览器中,因此就更须要注意验证的时机,并保证只有经过了验证的用户才能看到对应的界面。html
本篇文章中的身份验证,指的是如何肯定用户是否已经登录,并确保在每次与服务器的通讯中,都可以知足服务器的验证需求。注意,并不包括对具体是否具备某一个权限的判断。前端
对于登录,主要是接受用户的用户名密码输入,提交到服务器进行验证,处理验证响应,在浏览器端构建身份验证数据。算法
实现身份验证的两种方式数据库
目前,实现身份验证的方法,主要有两个大类:后端
Cookies浏览器
传统的浏览器网页,都是使用 Cookies 来验证身份,实际上,浏览器端的应用层里,基本不用去管身份验证的事情,Cookies 的设置,由服务器端完成,在提交请求的时候,由浏览器自动附加对应的 Cookies 信息,因此在 JavaScript 代码中,不须要为此编写专门的代码。但每次请求的时候,都会带上所有的 Cookies 数据,缓存
随着 CDN 的应用,移动端的逐渐兴起, Cookies 愈来愈不能知足复杂的、多域名下的身份验证需求。服务器
密钥网络
实际上基于密钥的身份验证并非最近才兴起,它一直存在,甚至比 Cookies 历史更长。当浏览器在请求服务器的时候,将密钥以特定的方式附加在请求中,好比放在请求的头部( headers )。为此,须要编写专门的客户端代码来管理。
最近出现的基于 JSON 的 Web 密钥(JSON Web Token)
标准,即是典型的使用密钥来实现的身份验证。
在 Web 应用中,若是是构造 API ,则应优先考试使用密钥方式。
处理登录
登录是身份验证第一步,经过登录,才可以组织起来对应的身份验证数据。
须要使用单独的登录页吗?
登录页的处理,有两种方式:
单独的登录页,在登录完成后,跳转到单页应用之中,这样作能够对单页应用的资源进行访问控制,防止非登录用户访问,适合后台或者管理工具的应用场景。但实际上下降了单页应用的用户体验
在单页应用以内执行登录,这样更符合单页应用的设计理念,比较适合大众产品的场景,由于恶意的人老是可以拿到你单页应用的前端代码
单独的登录页
通常状况下,使用单独的登录页的目的在于保护登录后跳转的页面不被匿名用户访问。所以,在登录页里,构造一个表单,直接采用传统的表彰提交方式(非 Ajax),后端验证用户名密码成功后,输出登录后单面应用页面的 HTML 。
在这种状况下,能够直接将身份验证信息放在输出的 HTML 里,好比,可使用 Jade 构造一个这样的页面:
1
2
3
4
5
6
7
8
9
10
|
<!-- 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 :
1
2
3
|
return
res.render(
'dashboard'
, {
token: token
});
|
Angular 应用一启动,即可以进行须要使用身份验证的通讯。并且还保证了只有登录成功的用户才能够进入这个页面。
单页应用内登录的组织
对于多视图的 Angular 应用,通常会采用路由,在页面以内,通常有固定的侧边栏菜单,或者顶部导航菜单,正文区域由路由模块来控制。
下面的示例代码,使用的是 Angular Material 来组织页面,路由模块使用的是 ui-router 。在应用打开的时候,有专门的加载动画,加载完成以后,显示的页面,使用 AppController
这个控制器,对于没有登录的用户,会显示登录表单,登录完成以后,页面分为三大部分,一是顶部面包屑导航,二是侧边栏菜单,另外就是路由控制的正文部分。
代码以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<
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 之流的问题。
登录成功的通知
对于有些应用,并非全部的页面都须要用户登录的,多是进行某些操做的时候,才须要登录。在这种状况下,登录完成以后,必需要通知整个应用。这可使用广播这个功能。
简易代码以下:
1
2
3
4
5
6
7
8
9
10
11
12
|
angular
.module(
'app'
)
.controller(
'LoginController'
, [
'$rootScope'
, LoginController]);
function
LoginController($rootScope) {
// 登录成功以后调用的函数
function
afterLoginSuccess() {
$rootScope.$broadcast(
'user.login.success'
, {
// 须要传输的数据
});
}
}
|
在其它的控制器中,即可以监听这个广播,并执行登录成功以后须要进行的操做,如获取列表或者详情:
1
2
3
|
$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
,经过它能够实现对每个请求和响应的统一处理。
添加拦截器的方式以下:
1
2
3
4
5
|
angular
.module(
'app'
)
.config([
'$httpProvider'
,
function
($httpProvider){
$httpProvider.interceptors.push(HttpInterceptor);
}]);
|
HttpInterceptor
的定义方式以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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
中即可以统一处理:
1
2
3
4
5
6
7
8
9
10
|
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有所帮助。
原文连接:http://chensd.com/2016-07/Authorzation-and-Login-in-Angular.html?utm_source=tuicool&utm_medium=referral