Angular學習碎片 先後端分離用戶驗證方法

Angular 常常會被用到後臺和管理工具的開發,這兩類都會須要對用戶進行鑑權。而鑑權的第一步,就是進行身份驗證。因爲 Angular 是單頁應用,會在一開始,就把大部分的資源加載到瀏覽器中,因此就更須要注意驗證的時機,並保證只有經過了驗證的用戶才能看到對應的界面。css

功能邊界

本篇文章中的身份驗證,指的是如何肯定用戶是否已經登陸,並確保在每次與服務器的通訊中,都可以知足服務器的驗證需求。注意,並不包括對具體是否具備某一個權限的判斷。html

對於登陸,主要是接受用戶的用戶名密碼輸入,提交到服務器進行驗證,處理驗證響應,在瀏覽器端構建身份驗證數據。前端

實現身份驗證的兩種方式

目前,實現身份驗證的方法,主要有兩個大類:git

Cookies

傳統的瀏覽器網頁,都是使用 Cookies 來驗證身份,實際上,瀏覽器端的應用層裏,基本不用去管身份驗證的事情,Cookies 的設置,由服務器端完成,在提交請求的時候,由瀏覽器自動附加對應的 Cookies 信息,因此在 JavaScript 代碼中,不須要爲此編寫專門的代碼。但每次請求的時候,都會帶上所有的 Cookies 數據,angularjs

隨着 CDN 的應用,移動端的逐漸興起, Cookies 愈來愈不能知足複雜的、多域名下的身份驗證需求。github

密鑰

實際上基於密鑰的身份驗證並非最近才興起,它一直存在,甚至比 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 屬性,這點很重要,只有這樣,纔可以保證登陸完成以後,再加載單頁應用中的內容,防止尚未登陸,當前路由中的控制器代碼就直接執行了。

爲何客戶端也要加密密碼

一個比較理想的基於用戶名和密碼的登陸流程是這樣的:

  • 瀏覽器端獲取用戶輸入的密碼,使用 MD5 一類的哈希算法,生成固定長度的新密碼,如 md5(username + md5(md5(password))) ,再將密碼哈希值和用戶名提交給後端
  • 後端根據用戶名獲取對應的鹽,使用用戶名和密碼哈希值,算出一個密文,根據用戶名和密文去數據庫查詢
  • 若是查詢成功,則生成密鑰,返回給瀏覽器,並執行第 4 步
  • 後端生成新的鹽,根據新的鹽和瀏覽器提交的密碼哈希值,生成新的密文。在數據庫中更新鹽和密文

可能有 80% 的人沒法理解爲何要把一個登陸作得這麼複雜。這可能要寫一篇專門的文章才解釋得清楚。在這裏先解釋一下爲何瀏覽器端要對密碼作哈希,緣由以下:

  • 從源頭上保護用戶的密碼,保證只有作按鍵記錄才能夠拿到用戶的原始密碼
  • 就算網絡被竊聽,又沒有使用 https ,那麼被偷走的密碼,也只是哈希以後的,最多影響用戶在這個服務器裏的數據,而不影響使用相同密碼的其它網站
  • 就算是服務器的全部者,都沒法獲取用戶的原始密碼

這種作法,使得用戶的最大風險,也只是當前這個應用中的數據被竊取。不會擴大損失範圍,毫不會出現 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 請求都須要帶上密鑰,並且請求返回的響應,還須要檢查是不是關於身份信息失效的錯誤。這一系列的工做比較繁瑣,應該是自動完成才行。

保存

密鑰的保存,大概有以下幾個辦法:

  • Cookies:前面已經提到了,這個並不推薦使用。同時,它還有最大 4k 的限制
  • sessionStorage:tab 頁內有效,一旦關閉,或者打開了新的 tab 頁,sessionStorage 是不能共享的
  • localStorage:較爲理想的存儲方式,除非清理瀏覽器數據,不然 localStorage 存儲的數據會一直存在
  • 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);

}

};

}

攔截器提供了對發出請求到返回響應的全生命週期處理,通常能夠用來作下面幾個事情:

  • 統一在發出的請求中添加數據,如添加身份驗證信息
  • 統一處理錯誤,包括請求發出時出的錯(如瀏覽器端的網絡不通),還有響應時返回的錯誤
  • 統一處理響應,好比緩存一些數據等
  • 顯示請求進度條

在上面的示例代碼中,當 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 ,能夠在這裏,統一處理並顯示錯誤

相關文章
相關標籤/搜索