认证是任何Web应用中不可或缺的一部分。在这个教程中,咱们会讨论基于token的认证系统以及它和传统的登陆系统的不一样。这篇教程的末尾,你会看到一个使用 AngularJS 和 NodeJS 构建的完整的应用。php
在开始说基于token的认证系统以前,咱们先看一下传统的认证系统。html
用户在登陆域输入用户名和密码,而后点击登陆;前端
请求发送以后,经过在后端查询数据库验证用户的合法性。若是请求有效,使用在数据库获得的信息建立一个 session,而后在响应头信息中返回这个 session 的信息,目的是把这个 session ID 存储到浏览器中;java
在访问应用中受限制的后端服务器时提供这个 session 信息;node
若是 session 信息有效,容许用户访问受限制的后端服务器,而且把渲染好的 HTML 内容返回。jquery
在这以前一切都很美好。Web应用正常工做,而且它可以认证用户信息而后能够访问受限的后端服务器;然而当你在开发其余终端时发生了什么呢,好比在Android应用中?你还能使用当前的应用去认证移动端而且分发受限制的内容么?真相是,不能够。有两个主要的缘由:git
在移动应用上 session 和 cookie 行不通。你没法与移动终端共享服务器建立的 session 和 cookie。angularjs
在这个应用中,渲染好的 HTML 被返回。但在移动端,你须要包含一些相似 JSON 或者 XML 的东西包含在响应中。github
在这个例子中,须要一个独立客户端服务。web
在基于 token 的认证里,再也不使用 cookie 和session。token 可被用于在每次向服务器请求时认证用户。咱们使用基于 token 的认证来从新设计刚才的设想。
将会用到下面的控制流程:
用户在登陆表单中输入 用户名 和 密码 ,而后点击 登陆 ;
请求发送以后,经过在后端查询数据库验证用户的合法性。若是请求有效,使用在数据库获得的信息建立一个 token,而后在响应头信息中返回这个的信息,目的是把这个 token 存储到浏览器的本地存储中;
在每次发送访问应用中受限制的后端服务器的请求时提供 token 信息;
若是从请求头信息中拿到的 token 有效,容许用户访问受限制的后端服务器,而且返回 JSON 或者 XML。
在这个例子中,咱们没有返回的 session 或者 cookie,而且咱们没有返回任何 HTML 内容。那意味着咱们能够把这个架构应用于特定应用的全部客户端中。你能够看一下面的架构体系:
那么,这里的 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 认证的一些基础知识后,咱们接下来看一个实例。看一下下面的几点,而后咱们会仔细的分析它:
多个终端,好比一个 web 应用,一个移动端等向 API 发送特定的请求。
相似https://api.yourexampleapp.com这样的请求发送到服务层。若是不少人使用了这个应用,须要多个服务器来响应这些请求操做。
这时,负载均衡被用于平衡请求,目的是达到最优化的后端应用服务。当你向https://api.yourexampleapp.com发送请求,最外层的负载均衡会处理这个请求,而后重定向到指定的服务器。
一个应用可能会被部署到多个服务器上(server-1, server-2, ..., server-n)。当有请求发送到https://api.yourexampleapp.com时,后端的应用会拦截这个请求头部而且从认证头部中提取到 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 认证的理论和架构就说到这里。下面上实例。
你会看到两个用于展现基于 token 认证的应用:
token-based-auth-backend
token-based-auth-frontend
在后端项目中,包括服务接口,服务返回的 JSON 格式。服务层不会返回视图。在前端项目中,会使用 AngularJS 向后端服务发送请求。
token-based-auth-backend
在后端项目中,有三个主要文件:
package.json 用于管理依赖;
models\User.js 包含了可能被用于处理关于用户的数据库操做的用户模型;
server.js 用于项目引导和请求处理。
就是这样!这个项目很是简单,你没必要深刻研究就能够了解主要的概念。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"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 服务颇有用。咱们也会在另一节中包含那个话题。
1
2
3
4
5
6
7
8
|
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 模型被定义好,如今咱们把那些构想成一个服务用于处理特定的请求。
1
2
3
4
5
6
7
|
// 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 包含一个模块到你的项目中。第一步,咱们须要把必要的模块引入到项目中:
1
2
3
4
|
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。
1
2
3
4
5
6
7
8
9
|
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 头部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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 一块儿返回。可是,若是没有用户名或者密码不正确,要怎么处理呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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 变量。认证部分已经完成。咱们访问一个受限的后端服务器会怎么样呢?咱们又要如何访问那个后端服务器呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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 函数将会执行。
1
2
3
4
5
6
7
8
9
10
11
12
|
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 都是惟一的。
这个简单的例子中已经有三个事件处理函数。而后,你将看到;
1
2
3
|
process.on( 'uncaughtException' , function (err) {
console.log(err);
});
|
当程序出错时 NodeJS 应用可能会崩溃。添加上面的代码能够拯救它而且一个错误日志会打到控制台上。最终,咱们可使用下面的代码片断启动服务。
1
2
3
4
|
// Start Server
app.listen(port, function () {
console.log( "Express server listening on port " + port);
});
|
总结一下:
引入模块
正确配置
定义请求处理函数
定义用来拦截受限终点数据的中间件
启动服务
咱们已经完成了后端服务。到如今,应用已经能够被多个终端使用,你能够部署这个简单的应用到你的服务器上,或者部署在 Heroku。有一个叫作 Procfile 的文件在项目的根目录下。如今把服务部署到 Heroku。
你能够在这个GitHub库下载项目的后端代码。
我不会教你如何在 Heroku 如何建立一个应用;若是你尚未作过这个,你能够查阅这篇文章。建立完 Heroku 应用,你可使用下面的命令为你的项目添加一个地址:
1
|
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 文件提供服务。
1
2
3
4
5
6
7
8
9
10
11
|
...
[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 文件都被包含,包括自定义的控制器,服务和应用文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
'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 能够在登陆表单中初始化一个登陆按钮;
signup 用于处理注册操做;
me 能够在 layout 中生生一个 Me 按钮;
在 全局 layout 和主菜单列表中,你能够看到 data-ng-controller 这个属性,它的值是 HomeCtrl。那意味着这个菜单的 dom 元素能够和 HomeCtrl 共享做用域。当你点击表单里的 sign-up 按钮时,控制器文件中的 sign-up 函数将会执行,而且在这个函数中,使用的登陆服务来自于已经注入到这个控制器的 Main 服务。
主要的结构是 view -> controller -> service。这个服务向后端发送了简单的 Ajax 请求,目的是获取指定的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
'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.me 的函数。这里的Main 服务已经注入到控制器,而且在它内部,属于这个服务的其余服务直接被调用。
这 些函数式仅仅是简单地向咱们部署的服务器集群发送 Ajax 请求。不要忘记在上面的代码中把服务的 URL 放到 baseUrl。当你把服务部署到 Heroku,你会获得一个相似 appname.herokuapp.com 的服务 URL。在上面的代码中,你要设置 var baseUrl = "appname.herokuapp.com"。
在应用的注册或者登陆部分,不记名 token 响应了这个请求而且这个 token 被存储到本地存储中。当你向后端请求一个服务时,你须要把这个 token 放在头部中。你可使用 AngularJS 的拦截器实现这个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
$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 中看到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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 或者一个桌面客户端这类服务。
REFER:
原文:Token-Based Authentication With AngularJS & NodeJS
使用 AngularJS & NodeJS 实现基于 token 的认证应用
http://zhuanlan.zhihu.com/FrontendMagazine/19920223