認證是任何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 認證的一些基礎知識后,我們接下來看一個實例。看一下下面的幾點,然后我們會仔細的分析它:
多個終端,比如一個 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。
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