使用 AngularJS & NodeJS 實現基於token 的認證應用(轉)

認證是任何 web 應用中不可或缺的一部分。在這個教程中,咱們會討論基於 token 的認證系統以及它和傳統的登陸系統的不一樣。這篇教程的末尾,你會看到一個使用 AngularJS 和 NodeJS 構建的完整的應用。php

1、認證系統html

傳統的認證系統前端

在開始說基於 token 的認證系統以前,咱們先看一下傳統的認證系統。java

  • 用戶在登陸域輸入 用戶名密碼 ,而後點擊 登陸node

  • 請求發送以後,經過在後端查詢數據庫驗證用戶的合法性。若是請求有效,使用在數據庫獲得的信息建立一個 session,而後在響應頭信息中返回這個 session 的信息,目的是把這個 session ID 存儲到瀏覽器中;
  • 在訪問應用中受限制的後端服務器時提供這個 session 信息;
  • 若是 session 信息有效,容許用戶訪問受限制的後端服務器,而且把渲染好的 HTML 內容返回。

在這以前一切都很美好。web 應用正常工做,而且它可以認證用戶信息而後能夠訪問受限的後端服務器;然而當你在開發其餘終端時發生了什麼呢,好比在 Android 應用中?你還能使用當前的應用去認證移動端而且分發受限制的內容麼?真相是,不能夠。有兩個主要的緣由:jquery

  • 在移動應用上 session 和 cookie 行不通。你沒法與移動終端共享服務器建立的 session 和 cookie。git

  • 在這個應用中,渲染好的 HTML 被返回。但在移動端,你須要包含一些相似 JSON 或者 XML 的東西包含在響應中。

在這個例子中,須要一個獨立客戶端服務。angularjs

基於 token 的認證github

在基於 token 的認證裏,再也不使用 cookie 和session。token 可被用於在每次向服務器請求時認證用戶。咱們使用基於 token 的認證來從新設計剛纔的設想。web

將會用到下面的控制流程:

  • 用戶在登陸表單中輸入 用戶名密碼 ,而後點擊 登陸

  • 請求發送以後,經過在後端查詢數據庫驗證用戶的合法性。若是請求有效,使用在數據庫獲得的信息建立一個 token,而後在響應頭信息中返回這個的信息,目的是把這個 token 存儲到瀏覽器的本地存儲中;
  • 在每次發送訪問應用中受限制的後端服務器的請求時提供 token 信息;
  • 若是從請求頭信息中拿到的 token 有效,容許用戶訪問受限制的後端服務器,而且返回 JSON 或者 XML。

在這個例子中,咱們沒有返回的 session 或者 cookie,而且咱們沒有返回任何 HTML 內容。那意味着咱們能夠把這個架構應用於特定應用的全部客戶端中。你能夠看一下面的架構體系:

那麼,這裏的 JWT 是什麼?

2、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 生成器函數,由於它對於不少經常使用的語言已經有多個版本的實現。下面給出了一些:

3、一個實例

在討論了關於基於 token 認證的一些基礎知識後,咱們接下來看一個實例。看一下下面的幾點,而後咱們會仔細的分析它:


  1. 多個終端,好比一個 web 應用,一個移動端等向 API 發送特定的請求。

  2. 相似 []() 這樣的請求發送到服務層。若是不少人使用了這個應用,須要多個服務器來響應這些請求操做。
  3. 這時,負載均衡被用於平衡請求,目的是達到最優化的後端應用服務。當你向 []() 發送請求,最外層的負載均衡會處理這個請求,而後重定向到指定的服務器。
  4. 一個應用可能會被部署到多個服務器上(server-1, server-2, ..., server-n)。當有請求發送到[]() 時,後端的應用會攔截這個請求頭部而且從認證頭部中提取到 token 信息。使用這個 token 查詢數據庫。若是這個 token 有效而且有請求終端數據所必須的許可時,請求會繼續。若是無效,會返回 403 狀態碼(代表一個拒絕的狀態)。

優點

基於 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 認證的理論和架構就說到這裏。下面上實例。

4、應用實例

你會看到兩個用於展現基於 token 認證的應用:

  1. token-based-auth-backend
  2. token-based-auth-frontend

在後端項目中,包括服務接口,服務返回的 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 頭部。
app.post('/authenticate', 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: true,
                    data: user,
                    token: user.token
                }); 
            } else {
                res.json({
                    type: false,
                    data: "Incorrect email/password"
                });    
            }
        }
    });
});
View Code

咱們已經引入了所需的所有模塊而且定義了配置文件,因此是時候來定義請求處理函數了。在上面的代碼中,當你提供了用戶名和密碼向 /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 中,你可能已經看到了有相似 的函數。這裏的Main 服務已經注入到控制器,而且在它內部,屬於這個服務的其餘服務直接被調用。

這些函數式僅僅是簡單地向咱們部署的服務器集羣發送 Ajax 請求。不要忘記在上面的代碼中把服務的 URL 放到 baseUrl。當你把服務部署到 Heroku,你會獲得一個相似 的服務 URL。在上面的代碼中,你要設置 var baseUrl = ""。

在應用的註冊或者登陸部分,不記名 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

相關文章
相關標籤/搜索