認證是任何 web 應用中不可或缺的一部分。在這個教程中,咱們會討論基於 token 的認證系統以及它和傳統的登陸系統的不一樣。這篇教程的末尾,你會看到一個使用 AngularJS 和 NodeJS 構建的完整的應用。php
1、認證系統html
傳統的認證系統前端
在開始說基於 token 的認證系統以前,咱們先看一下傳統的認證系統。java
用戶在登陸域輸入 用戶名 和 密碼 ,而後點擊 登陸 ;node
在這以前一切都很美好。web 應用正常工做,而且它可以認證用戶信息而後能夠訪問受限的後端服務器;然而當你在開發其餘終端時發生了什麼呢,好比在 Android 應用中?你還能使用當前的應用去認證移動端而且分發受限制的內容麼?真相是,不能夠。有兩個主要的緣由:jquery
在移動應用上 session 和 cookie 行不通。你沒法與移動終端共享服務器建立的 session 和 cookie。git
在這個例子中,須要一個獨立客戶端服務。angularjs
基於 token 的認證github
在基於 token 的認證裏,再也不使用 cookie 和session。token 可被用於在每次向服務器請求時認證用戶。咱們使用基於 token 的認證來從新設計剛纔的設想。web
將會用到下面的控制流程:
用戶在登陸表單中輸入 用戶名 和 密碼 ,而後點擊 登陸 ;
在這個例子中,咱們沒有返回的 session 或者 cookie,而且咱們沒有返回任何 HTML 內容。那意味着咱們能夠把這個架構應用於特定應用的全部客戶端中。你能夠看一下面的架構體系:
那麼,這裏的 JWT 是什麼?
2、JWT
JWT 表明 JSON Web Token ,它是一種用於認證頭部的 token 格式。這個 token 幫你實現了在兩個系統之間以一種安全的方式傳遞信息。出於教學目的,咱們暫且把 JWT 做爲「不記名 token」。一個不記名 token 包含了三部分:header,payload,signature。
header 是 token 的一部分,用來存放 token 的類型和編碼方式,一般是使用 base-64 編碼。
你能夠在下面看到 JWT 剛要和一個實例 token:
你沒必要關心如何實現不記名 token 生成器函數,由於它對於不少經常使用的語言已經有多個版本的實現。下面給出了一些:
3、一個實例
在討論了關於基於 token 認證的一些基礎知識後,咱們接下來看一個實例。看一下下面的幾點,而後咱們會仔細的分析它:
多個終端,好比一個 web 應用,一個移動端等向 API 發送特定的請求。
優點
基於 token 的認證在解決棘手的問題時有幾個優點:
這些就是基於 token 的認證和通訊中最明顯的優點。基於 token 認證的理論和架構就說到這裏。下面上實例。
4、應用實例
你會看到兩個用於展現基於 token 認證的應用:
在後端項目中,包括服務接口,服務返回的 JSON 格式。服務層不會返回視圖。在前端項目中,會使用 AngularJS 向後端服務發送請求。
在後端項目中,有三個主要文件:
package.json 用於管理依賴;
就是這樣!這個項目很是簡單,你沒必要深刻研究就能夠了解主要的概念。
{ "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 容許全部的域名。
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" }); } } }); });
咱們已經引入了所需的所有模塊而且定義了配置文件,因此是時候來定義請求處理函數了。在上面的代碼中,當你提供了用戶名和密碼向 /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 }); }); }) } } }); });
當你使用用戶名和密碼向 /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 }); } }); });
當你向 /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); } }
在這個函數中,請求頭部被攔截而且 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。
你能夠在這個 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 協議訪問這個服務。
在前端項目中,將會使用 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; }])
在上面的代碼中,HomeCtrl 控制器被定義而且一些所需的模塊被注入(好比 $rootScope 和 $scope)。依賴注入是 AngularJS 最強大的屬性之一。 $scope 是 AngularJS 中的一個存在於控制器和視圖之間的中間變量,這意味着你能夠在視圖中使用 test,前提是你在特定的控制器中定義了 $scope.test=....。
在控制器中,一些工具函數被定義,好比:
signin 能夠在登陸表單中初始化一個登陸按鈕;
在全局 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