认证是任何 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