javascript
使用 AngularJS NodeJS 实现基于token 的认证应用(转)
認證是任何 web 應用中不可或缺的一部分。在這個教程中,我們會討論基于 token 的認證系統以及它和傳統的登錄系統的不同。這篇教程的末尾,你會看到一個使用 AngularJS 和 NodeJS 構建的完整的應用。
一、認證系統
傳統的認證系統
在開始說基于 token 的認證系統之前,我們先看一下傳統的認證系統。
-
用戶在登錄域輸入 用戶名 和 密碼 ,然后點擊 登錄 ;
- 請求發送之后,通過在后端查詢數據庫驗證用戶的合法性。如果請求有效,使用在數據庫得到的信息創建一個 session,然后在響應頭信息中返回這個 session 的信息,目的是把這個 session ID 存儲到瀏覽器中;
- 在訪問應用中受限制的后端服務器時提供這個 session 信息;
- 如果 session 信息有效,允許用戶訪問受限制的后端服務器,并且把渲染好的 HTML 內容返回。
在這之前一切都很美好。web 應用正常工作,并且它能夠認證用戶信息然后可以訪問受限的后端服務器;然而當你在開發其他終端時發生了什么呢,比如在 Android 應用中?你還能使用當前的應用去認證移動端并且分發受限制的內容么?真相是,不可以。有兩個主要的原因:
-
在移動應用上 session 和 cookie 行不通。你無法與移動終端共享服務器創建的 session 和 cookie。
- 在這個應用中,渲染好的 HTML 被返回。但在移動端,你需要包含一些類似 JSON 或者 XML 的東西包含在響應中。
在這個例子中,需要一個獨立客戶端服務。
基于 token 的認證
在基于 token 的認證里,不再使用 cookie 和session。token 可被用于在每次向服務器請求時認證用戶。我們使用基于 token 的認證來重新設計剛才的設想。
將會用到下面的控制流程:
-
用戶在登錄表單中輸入 用戶名 和 密碼 ,然后點擊 登錄 ;
- 請求發送之后,通過在后端查詢數據庫驗證用戶的合法性。如果請求有效,使用在數據庫得到的信息創建一個 token,然后在響應頭信息中返回這個的信息,目的是把這個 token 存儲到瀏覽器的本地存儲中;
- 在每次發送訪問應用中受限制的后端服務器的請求時提供 token 信息;
- 如果從請求頭信息中拿到的 token 有效,允許用戶訪問受限制的后端服務器,并且返回 JSON 或者 XML。
在這個例子中,我們沒有返回的 session 或者 cookie,并且我們沒有返回任何 HTML 內容。那意味著我們可以把這個架構應用于特定應用的所有客戶端中。你可以看一下面的架構體系:
那么,這里的 JWT 是什么?
二、JWT
JWT 代表 JSON Web Token ,它是一種用于認證頭部的 token 格式。這個 token 幫你實現了在兩個系統之間以一種安全的方式傳遞信息。出于教學目的,我們暫且把 JWT 作為“不記名 token”。一個不記名 token 包含了三部分:header,payload,signature。
-
header 是 token 的一部分,用來存放 token 的類型和編碼方式,通常是使用 base-64 編碼。
- payload 包含了信息。你可以存放任一種信息,比如用戶信息,產品信息等。它們都是使用 base-64 編碼方式進行存儲。
- signature 包括了 header,payload 和密鑰的混合體。密鑰必須安全地保存儲在服務端。
你可以在下面看到 JWT 剛要和一個實例 token:
你不必關心如何實現不記名 token 生成器函數,因為它對于很多常用的語言已經有多個版本的實現。下面給出了一些:
- NodeJS: auth0/node-jsonwebtoken · GitHub
- PHP: firebase/php-jwt · GitHub
- Java: auth0/java-jwt · GitHub
- Ruby: progrium/ruby-jwt · GitHub
- .NET: AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet · GitHub
- Python: progrium/pyjwt · GitHub
三、一個實例
在討論了關于基于 token 認證的一些基礎知識后,我們接下來看一個實例??匆幌孪旅娴膸c,然后我們會仔細的分析它:
多個終端,比如一個 web 應用,一個移動端等向 API 發送特定的請求。
優勢
基于 token 的認證在解決棘手的問題時有幾個優勢:
- Client Independent Services 。在基于 token 的認證,token 通過請求頭傳輸,而不是把認證信息存儲在 session 或者 cookie 中。這意味著無狀態。你可以從任意一種可以發送 HTTP 請求的終端向服務器發送請求。
- CDN 。在絕大多數現在的應用中,view 在后端渲染,HTML 內容被返回給瀏覽器。前端邏輯依賴后端代碼。這中依賴真的沒必要。而且,帶來了幾個問題。比如,你和一個設計機構合作,設計師幫你完成了前端的 HTML,CSS 和 JavaScript,你需要拿到前端代碼并且把它移植到你的后端代碼中,目的當然是為了渲染。修改幾次后,你渲染的 HTML 內容可能和設計師完成的代碼有了很大的不同。在基于 token 的認證中,你可以開發完全獨立于后端代碼的前端項目。后端代碼會返回一個 JSON 而不是渲染 HTML,并且你可以把最小化,壓縮過的代碼放到 CDN 上。當你訪問 web 頁面,HTML 內容由 CDN 提供服務,并且頁面內容是通過使用認證頭部的 token 的 API 服務所填充。
- No Cookie-Session (or No CSRF) 。CSRF 是當代 web 安全中一處痛點,因為它不會去檢查一個請求來源是否可信。為了解決這個問題,一個 token 池被用在每次表單請求時發送相關的 token。在基于 token 的認證中,已經有一個 token 應用在認證頭部,并且 CSRF 不包含那個信息。
- Persistent Token Store 。當在應用中進行 session 的讀,寫或者刪除操作時,會有一個文件操作發生在操作系統的temp 文件夾下,至少在第一次時。假設有多臺服務器并且 session 在第一臺服務上創建。當你再次發送請求并且這個請求落在另一臺服務器上,session 信息并不存在并且會獲得一個“未認證”的響應。我知道,你可以通過一個粘性 session 解決這個問題。然而,在基于 token 的認證中,這個問題很自然就被解決了。沒有粘性 session 的問題,因為在每個發送到服務器的請求中這個請求的 token 都會被攔截。
這些就是基于 token 的認證和通信中最明顯的優勢?;?token 認證的理論和架構就說到這里。下面上實例。
四、應用實例
你會看到兩個用于展示基于 token 認證的應用:
在后端項目中,包括服務接口,服務返回的 JSON 格式。服務層不會返回視圖。在前端項目中,會使用 AngularJS 向后端服務發送請求。
token-based-auth-backend
在后端項目中,有三個主要文件:
-
package.json 用于管理依賴;
- models\User.js 包含了可能被用于處理關于用戶的數據庫操作的用戶模型;
- server.js 用于項目引導和請求處理。
就是這樣!這個項目非常簡單,你不必深入研究就可以了解主要的概念。
{"name": "angular-restful-auth","version": "0.0.1","dependencies": {"express": "4.x","body-parser": "~1.0.0","morgan": "latest","mongoose": "3.8.8","jsonwebtoken": "0.4.0"},"engines": {"node": ">=0.10.0"} }package.json包含了這個項目的依賴:express 用于 MVC,body-parser 用于在 NodeJS 中模擬 post 請求操作,morgan 用于請求登錄,mongoose 用于為我們的 ORM 框架連接 MongoDB,最后 jsonwebtoken 用于使用我們的 User 模型創建 JWT 。如果這個項目使用版本號 >= 0.10.0 的 NodeJS 創建,那么還有一個叫做 engines 的屬性。這對那些像 HeroKu 的 PaaS 服務很有用。我們也會在另外一節中包含那個話題。
var mongoose = require('mongoose'); var Schema = mongoose.Scema; var UserSchema = new Schema({email: String,password: String,token: String }); module.exports = mongoose.model('User', UserSchema);上面提到我們可以通過使用用戶的 payload 模型生成一個 token。這個模型幫助我們處理用戶在 MongoDB 上的請求。在User.js,user-schema 被定義并且 User 模型通過使用 mogoose 模型被創建。這個模型提供了數據庫操作。
我們的依賴和 user 模型被定義好,現在我們把那些構想成一個服務用于處理特定的請求。
// Required Modules var express = require("express"); var morgan = require("morgan"); var bodyParser = require("body-parser"); var jwt = require("jsonwebtoken"); var mongoose = require("mongoose"); var app = express();在 NodeJS 中,你可以使用 require 包含一個模塊到你的項目中。第一步,我們需要把必要的模塊引入到項目中:
var port = process.env.PORT || 3001; var User = require('./models/User'); // Connect to DB mongoose.connect(process.env.MONGO_URL);服務層通過一個指定的端口提供服務。如果沒有在環境變量中指定端口,你可以使用那個,或者我們定義的 3001 端口。然后,User 模型被包含,并且數據庫連接被建立用來處理一些用戶操作。不要忘記定義一個 MONGO_URL 環境變量,用于數據庫連接 URL。
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(morgan("dev")); app.use(function(req, res, next) {res.setHeader('Access-Control-Allow-Origin', '*');res.setHeader('Access-Control-Allow-Methods', 'GET, POST');res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type, Authorization');next(); });上一節中,我們已經做了一些配置用于在 NodeJS 中使用 Express 模擬一個 HTTP 請求。我們允許來自不同域名的請求,目的是建立一個獨立的客戶端系統。如果你沒這么做,可能會觸發瀏覽器的 CORS(跨域請求共享)錯誤。
-
Access-Control-Allow-Origin 允許所有的域名。
- 你可以向這個設備發送 POST 和 GET 請求。
- 允許 X-Requested-With 和 content-type 頭部。
我們已經引入了所需的全部模塊并且定義了配置文件,所以是時候來定義請求處理函數了。在上面的代碼中,當你提供了用戶名和密碼向 /authenticate 發送一個 POST 請求時,你將會得到一個 JWT。首先,通過用戶名和密碼查詢數據庫。如果用戶存在,用戶數據將會和它的 token 一起返回。但是,如果沒有用戶名或者密碼不正確,要怎么處理呢?
app.post('/signin', function(req, res) {User.findOne({email: req.body.email, password: req.body.password}, function(err, user) {if (err) {res.json({type: false,data: "Error occured: " + err});} else {if (user) {res.json({type: false,data: "User already exists!"});} else {var userModel = new User();userModel.email = req.body.email;userModel.password = req.body.password;userModel.save(function(err, user) {user.token = jwt.sign(user, process.env.JWT_SECRET);user.save(function(err, user1) {res.json({type: true,data: user1,token: user1.token});});})}}}); }); View Code當你使用用戶名和密碼向 /signin 發送 POST 請求時,一個新的用戶會通過所請求的用戶信息被創建。在 第 19 行,你可以看到一個新的 JSON 通過 jsonwebtoken 模塊生成,然后賦值給 jwt 變量。認證部分已經完成。我們訪問一個受限的后端服務器會怎么樣呢?我們又要如何訪問那個后端服務器呢?
app.get('/me', ensureAuthorized, function(req, res) {User.findOne({token: req.token}, function(err, user) {if (err) {res.json({type: false,data: "Error occured: " + err});} else {res.json({type: true,data: user});}}); }); View Code當你向 /me 發送 GET 請求時,你將會得到當前用戶的信息,但是為了繼續請求后端服務器, ensureAuthorized 函數將會執行。
function ensureAuthorized(req, res, next) {var bearerToken;var bearerHeader = req.headers["authorization"];if (typeof bearerHeader !== 'undefined') {var bearer = bearerHeader.split(" ");bearerToken = bearer[1];req.token = bearerToken;next();} else {res.send(403);} } View Code在這個函數中,請求頭部被攔截并且 authorization 頭部被提取。如果頭部中存在一個不記名 token,通過調用 next()函數,請求繼續。如果 token 不存在,你會得到一個 403(Forbidden)返回。我們回到 /me 事件處理函數,并且使用req.token 獲取這個 token 對應的用戶數據。當你創建一個新的用戶,會生成一個 token 并且存儲到數據庫的用戶模型中。那些 token 都是唯一的。
這個簡單的例子中已經有三個事件處理函數。然后,你將看到;
process.on('uncaughtException', function(err) { console.log(err); });當程序出錯時 NodeJS 應用可能會崩潰。添加上面的代碼可以拯救它并且一個錯誤日志會打到控制臺上。最終,我們可以使用下面的代碼片段啟動服務。
// Start Server app.listen(port, function () {console.log( "Express server listening on port " + port); });總結一下:
-
引入模塊
- 正確配置
- 定義請求處理函數
- 定義用來攔截受限終點數據的中間件
- 啟動服務
我們已經完成了后端服務。到現在,應用已經可以被多個終端使用,你可以部署這個簡單的應用到你的服務器上,或者部署在 Heroku。有一個叫做 Procfile 的文件在項目的根目錄下。現在把服務部署到 Heroku。
Heroku 部署
你可以在這個 GitHub 庫下載項目的后端代碼。
我不會教你如何在 Heroku 如何創建一個應用;如果你還沒有做過這個,你可以查閱這篇文章。創建完 Heroku 應用,你可以使用下面的命令為你的項目添加一個地址:
git remote add heroku <your_heroku_git_url>現在,你已經克隆了這個項目并且添加了地址。在 git add 和 git commit 后,你可以使用 git push heroku master 命令將你的代碼推到 Heroku。當你成功將項目推送到倉庫,Heroku 會自動執行 npm install 命令將依賴文件下載到 Heroku 的 temp 文件夾。然后,它會啟動你的應用,因此你就可以使用 HTTP 協議訪問這個服務。
token-based-auth-frontend
在前端項目中,將會使用 AngularJS。在這里,我只會提到前端項目中的主要內容,因為 AngularJS 的相關知識不會包括在這個教程里。
你可以在這個 GitHub 庫下載源碼。在這個項目中,你會看下下面的文件結構:
ngStorage.js 是一個用于操作本地存儲的 AngularJS 類庫。此外,有一個全局的 layout 文件 index.html 并且在 partials 文件夾里還有一些用于擴展全局 layout 的部分。 controllers.js 用于在前端定義我們 controller 的 action。 services.js 用于向我們在上一個項目中提到的服務發送請求。還有一個 app.js 文件,它里面有配置文件和模塊引入。最后,client.js 用于服務靜態 HTML 文件(或者僅僅 index.html,在這里例子中);當你沒有使用 Apache 或者任何其他的 web 服務器時,它可以為靜態的 HTML 文件提供服務。
... <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular-route.min.js"></script> <script src="/lib/ngStorage.js"></script> <script src="/lib/loading-bar.js"></script> <script src="/scripts/app.js"></script> <script src="/scripts/controllers.js"></script> <script src="/scripts/services.js"></script> </body>在全局的 layout 文件中,AngularJS 所需的全部 JavaScript 文件都被包含,包括自定義的控制器,服務和應用文件。
'use strict';/* Controllers */angular.module('angularRestfulAuth').controller('HomeCtrl', ['$rootScope', '$scope', '$location', '$localStorage', 'Main', function($rootScope, $scope, $location, $localStorage, Main) {$scope.signin = function() {var formData = {email: $scope.email,password: $scope.password}Main.signin(formData, function(res) {if (res.type == false) {alert(res.data) } else {$localStorage.token = res.data.token;window.location = "/"; }}, function() {$rootScope.error = 'Failed to signin';})};$scope.signup = function() {var formData = {email: $scope.email,password: $scope.password}Main.save(formData, function(res) {if (res.type == false) {alert(res.data)} else {$localStorage.token = res.data.token;window.location = "/" }}, function() {$rootScope.error = 'Failed to signup';})};$scope.me = function() {Main.me(function(res) {$scope.myDetails = res;}, function() {$rootScope.error = 'Failed to fetch details';})};$scope.logout = function() {Main.logout(function() {window.location = "/"}, function() {alert("Failed to logout!");});};$scope.token = $localStorage.token;}]) View Code在上面的代碼中,HomeCtrl 控制器被定義并且一些所需的模塊被注入(比如 $rootScope 和 $scope)。依賴注入是 AngularJS 最強大的屬性之一。 $scope 是 AngularJS 中的一個存在于控制器和視圖之間的中間變量,這意味著你可以在視圖中使用 test,前提是你在特定的控制器中定義了 $scope.test=....。
在控制器中,一些工具函數被定義,比如:
-
signin 可以在登錄表單中初始化一個登錄按鈕;
- signup 用于處理注冊操作;
- me 可以在 layout 中生生一個 Me 按鈕;
在全局 layout 和主菜單列表中,你可以看到 data-ng-controller 這個屬性,它的值是 HomeCtrl。那意味著這個菜單的 dom 元素可以和 HomeCtrl 共享作用域。當你點擊表單里的 sign-up 按鈕時,控制器文件中的 sign-up 函數將會執行,并且在這個函數中,使用的登錄服務來自于已經注入到這個控制器的 Main 服務。
主要的結構是 view -> controller -> service。這個服務向后端發送了簡單的 Ajax 請求,目的是獲取指定的數據。
'use strict'; angular.module('angularRestfulAuth').factory('Main', ['$http', '$localStorage', function($http, $localStorage){var baseUrl = "your_service_url";function changeUser(user) {angular.extend(currentUser, user);}function urlBase64Decode(str) {var output = str.replace('-', '+').replace('_', '/');switch (output.length % 4) {case 0:break;case 2:output += '==';break;case 3:output += '=';break;default:throw 'Illegal base64url string!';}return window.atob(output);}function getUserFromToken() {var token = $localStorage.token;var user = {};if (typeof token !== 'undefined') {var encoded = token.split('.')[1];user = JSON.parse(urlBase64Decode(encoded));}return user;}var currentUser = getUserFromToken();return {save: function(data, success, error) {$http.post(baseUrl + '/signin', data).success(success).error(error)},signin: function(data, success, error) {$http.post(baseUrl + '/authenticate', data).success(success).error(error)},me: function(success, error) {$http.get(baseUrl + '/me').success(success).error(error)},logout: function(success) {changeUser({});delete $localStorage.token;success();}};} ]);在上面的代碼中,你會看到服務函數請求認證。在 controller.js 中,你可能已經看到了有類似 http://Main.me 的函數。這里的Main 服務已經注入到控制器,并且在它內部,屬于這個服務的其他服務直接被調用。
這些函數式僅僅是簡單地向我們部署的服務器集群發送 Ajax 請求。不要忘記在上面的代碼中把服務的 URL 放到 baseUrl。當你把服務部署到 Heroku,你會得到一個類似 http://appname.herokuapp.com 的服務 URL。在上面的代碼中,你要設置 var baseUrl = "http://appname.herokuapp.com"。
在應用的注冊或者登錄部分,不記名 token 響應了這個請求并且這個 token 被存儲到本地存儲中。當你向后端請求一個服務時,你需要把這個 token 放在頭部中。你可以使用 AngularJS 的攔截器實現這個。
$httpProvider.interceptors.push(['$q', '$location', '$localStorage', function($q, $location, $localStorage) {return {'request': function (config) {config.headers = config.headers || {};if ($localStorage.token) {config.headers.Authorization = 'Bearer ' + $localStorage.token;}return config;},'responseError': function(response) {if(response.status === 401 || response.status === 403) {$location.path('/signin');}return $q.reject(response);}};}]);在上面的代碼中,每次請求都會被攔截并且會把認證頭部和值放到頭部中。
在前端項目中,會有一些不完整的頁面,比如 signin,signup,profile details 和 vb。這些頁面與特定的控制器相關。你可以在 app.js 中看到:
angular.module('angularRestfulAuth', [ 'ngStorage', 'ngRoute']) .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) {$routeProvider.when('/', {templateUrl: 'partials/home.html',controller: 'HomeCtrl'}).when('/signin', {templateUrl: 'partials/signin.html',controller: 'HomeCtrl'}).when('/signup', {templateUrl: 'partials/signup.html',controller: 'HomeCtrl'}).when('/me', {templateUrl: 'partials/me.html',controller: 'HomeCtrl'}).otherwise({redirectTo: '/'});}]);
如上面代碼所示,當你訪問 /,home.html 將會被渲染。再看一個例子:如果你訪問 /signup,signup.html 將會被渲染。渲染操作會在瀏覽器中完成,而不是在服務端。
結論
你可以通過檢出這個實例看到我們在這個教程中所討論的項目是如何工作的。
基于 token 的認證系統幫你建立了一個認證/授權系統,當你在開發客戶端獨立的服務時。通過使用這個技術,你只需關注于服務(或者 API)。
認證/授權部分將會被基于 token 的認證系統作為你的服務前面的層來處理。你可以訪問并且使用來自于任何像 web 瀏覽器,Android,iOS 或者一個桌面客戶端這類服務。
原文:Token-Based Authentication With AngularJS & NodeJS
http://zhuanlan.zhihu.com/FrontendMagazine/19920223
總結
以上是生活随笔為你收集整理的使用 AngularJS NodeJS 实现基于token 的认证应用(转)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: unigui 导入导出数据
- 下一篇: [Eclipse] - 解决导入flas