前言
由於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 htmlhtml 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"> <!--側邊欄菜單--> </md-sidenav> <md-content flex layout="column" role="main"> <md-toolbar> <!--頂部菜單--> </md-toolbar> <md-content> <!--路由--> <div ui-view></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有所幫助。