2015年3月26日 星期四

Using JSON Web Tokens with Node.js

webber0928

原文:http://www.sitepoint.com/using-json-web-tokens-node-js/

翻譯:(LcjNiL)
諸如Ember,Angular,Backbone之類的前端框架類庫正隨著更加精細的Web應用而日益壯大。正因如此,服務器端的組建也正正在從傳統的任務中解脫,轉而變的更像API。API使得傳統的前端和後端的概念解耦。開發者可以脫離前端,獨立的開發後端,在測試上獲得更大的便利。這種途徑也使得一個移動應用和網頁應用可以使用相同的後端。

當使用一個API時,其中一個挑戰就是認證(authentication)。在傳統的web應用中,服務端成功的返回一個響應(response)依賴於兩件事。一是,他通過一種存儲機制保存了會話信息(Session)。每一個會話都有它獨特的信息(id),常常是一個長的,隨機化的字符串,它被用來讓未來的請求(Request)檢索信息。其次,包含在響應頭(Header)裡面的信息使客戶端保存了一個Cookie。服務器自動的在每個子請求裡面加上了會話ID,這使得服務器可以通​​過檢索Session中的信息來辨別用戶。這就是傳統的web應用逃避HTTP面向無連接的方法(This is how traditional web applications get around the fact that HTTP is stateless)。
API應該被設計成無狀態的(Stateless)。這意味著沒有登陸,註銷的方法,也沒有sessions,API的設計者同樣也不能依賴Cookie,因為不能保證這些req​​uest是由瀏覽器所發出的。自然,我們需要一個新的機制。這篇文章關注於JSON Web Tokens,簡寫為JWTs,一個可能的解決這個問題的機制。這篇文章利用Node的Express框架作為後端,以及Backbone作為前端。
##背景我們來簡短的看一下幾個通常的保護(secure)API的方法。
一個是使用在HTTP規範中所製定的Basic Auth, 它需要在在響應中設定一個驗證身份的Header。客戶端必須在每個子響應是附加它們的憑證(credenbtial),包括它的密碼。如果這些憑證通過了,那麼用戶的信息就會被傳遞到服務端應用。
第二個方面有點類似,但是使用應用自己的驗證機制。通常包括將發送的憑證與存儲的憑證進行檢查。和Basic Auth相比,這種需要在每次請求(call)中發送憑證。
第三種是OAuth(或者OAuth2)。為第三方的認證所設計,但​​是更難配置。至少在服務器端更難。
##使用Token的方法不是在每一次請求時提供用戶名和密碼的憑證。我們可以讓用戶通過token交換憑證(we can allow the client to exchange valid credentials for a token),這個token提供用戶訪問服務器的權限。Token通常比密碼更加長而且複雜。比如說,JWTs通常會應對長達150個字符。一旦獲得了token,在每次調用API的時候都要附加上它。然後,這仍然比直接發送賬戶和密碼更加安全,哪怕是HTTPS。
把token想像成一個安全的護照。你在一個安全的前台驗證你的身份(通過你的用戶名和密碼),如果你成功驗證了自己,你就可以取得這個。當你走進大樓的時候(試圖從調用API獲取資源),你會被要求驗證你的護照,而不是在前台重新驗證。
##關於JWTs JWTs是一份草案,儘管在本質上它是一個老生常談的一種更加具體的認證個授權的機制。一個JWT被周期(period)分寸了三個部分。JWT是URL-safe的,意味著可以​​用來查詢字符參數。(譯者註:也就是可以脫離URL,不用考慮URL的信息)。
JWT的第一部分是一個js對象,表面JWT的加密方法。實例使用了HMAC SHA-266
{ 
"typ" : "JWT" ​​,
"alg" : "HS256"
}
在加密之後,這個對像變成了一個字符串:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
JWT的第二部分是token的核心,他也是一個JS兌現,包含了一些信息。有一些是必須的,有一些是選擇性的。一個實例如下:
{ 
"iss" : "joe" ,
"exp" : 1300819380 ,
"http://example.com/is_root" : true
}
這被稱為JWT Claims Set。因為這篇文章的目的,我們將忽視第三個參數。但是你可以閱讀這篇文章 .這個ississuer的簡寫,表明請求的實體。通常意味著請求API的用戶。expexpires的簡寫,是用來限制token的生命週期。一旦加密,JSON token就像這樣:
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
第三個也是最後一個部分,是JWT根據第一部分和第二部分的簽名(Signature)。像這個樣子:
dBjftJeZ4CVP - mB92K27uhbUJU1p1r_wW1gFWFOEjXk
整個的JWT是這樣的
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 . eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ . dBjftJeZ4CVP - mB92K27uhbUJU1p1r_wW1gFWFOEjXk
在規範中,有一些選擇性的附加 ​​屬性。有iat表明什麼時候token被半吧,nbf去驗證在什麼時間之前token無效,和aud去指明這個token的收件人是誰。
##處理Tokens我們將用JWT simple模塊去處理token,它將使我們從鑽研如何加密解密中解脫出來。如果你有興趣,可以閱讀這篇說明,或者讀這個倉庫的源碼。首先我們將使用下面的命令安裝這個庫。記住你可以在命令中加入--save,讓其自動的讓其加入到你的package.json文件裡面。
npm install jwt - simple
在你應用的初始環節,加入以下代碼。這個代碼引入了Express和JWT simple,而且創建了一個新的Express應用。最後一行設定了app的一個名為jwtTokenSecret的變量,其值為'YOUR_SECRET_STRING'(記得把它換成別的)。
var express =  require ( 'express' ); 
var jwt = require ( 'jwt-simple' );
var app = express ();

app
. set ( 'jwtTokenSecret' , 'YOUR_SECRET_STRING' );
##獲取一個Token我們需要做的第一件事就是讓客戶端通過他們的賬號密碼交換token。這裡有2種可能的方法在RESTful API裡面。第一種是使用POST請求來通過驗證,使服務端發送帶有token的響應。除此之外,你可以使用GET請求,這需要他們使用參數提供憑證(指URL),或者更好的使用請求頭。
這篇文章的目的是為了解釋token驗證的方法而不是基本的用戶名/密碼驗證機制。所以我們假設我們已經通過請求得到了用戶名和密碼:
User . findOne ({ username : username },  function ( err , user )  { 
if ( err ) {
// user not found
return res . send ( 401 );
}

if (! user ) {
// incorrect username
return res . send ( 401 );
}

if (! user . validPassword ( password )) {
// incorrect password
return res . send ( 401 );
}

// User has authenticated OK
res
. send ( 200 );
});
下一步,我們就需要返回JWT token通過一個驗證成功的響應。
var expires = moment (). add ( 'days' ,  7 ). valueOf (); 
var token = jwt . encode ({
iss
: user . id ,
exp
: expires
}, app . get ( 'jwtTokenSecret' ));

res
. json ({
token
: token ,
expires
: expires ,
user
: user . toJSON ()
});
注意到jwt.encode()函數有2個參數。第一個就是一個需要加密的對象,第二個是一個加密的密鑰。這個token是由我們之前提到的issexp組成的。注意到Moment.js被用來設置token將在7天之後失效。而res.json()方法用來傳遞這個JSON對像給客戶端。
##驗證Token 為了驗證JWT,我們需要寫出一些可以完成這些功能的中間件(Middleware):
  • 檢查附上的token
  • 試圖解密
  • 驗證token的可用性
  • 如果token是合法的,檢索里面用戶的信息,以及附加到請求的對像上
我們來寫一個中間件的框架
// [@file](/user/file) jwtauth.js

var UserModel = require ( '../models/user' );
var jwt = require ( 'jwt-simple' );

module . exports = function ( req , res , next ) {
// code goes here
};
為了獲得最大的可擴展性,我們允許客戶端使用一下3個方法附加我們的token:作為請求鏈接(query)的參數,作為主體的參數(body),和作為請求頭(Header)的參數。對於最後一個,我們將使用Header x-access-token
下面是我們的允許在中間件的代碼,試圖去檢索token:
var token =  ( req . body && req . body . access_token )  ||  ( req . query && req . query . access_token )  || req . headers [ 'x-access-token' ];
注意到他為了訪問req.body,我們需要首先使用express.bodyParser()中間件(譯者註,這個是Express 3.x的中間件)。
下一步,我們講解析JWT:
if  ( token )  { 
try {
var decoded = jwt . decode ( token , app . get ( 'jwtTokenSecret' ));

// handle token here

} catch ( err ) {
return next ();
}
} else {
next ();
}
如果解析的過程失敗,那麼JWT Simple組件將會拋出一段異常。如果異常發生了,或者沒有token,我們將會調用next()來繼續處理請求。這代表喆我們無法確定用戶。如果一個合格的token合法並且被解碼,我們應該得到2個屬性,iss包含著用戶ID以及exp包含token過期的時間戳。我們將首先處理後者,如果它過期了,我們就拒絕它:
if  ( decoded . exp <=  Date . now ())  { 
res
. end ( 'Access token has expired' , 400 );
}
如果token依舊合法,我們可以從中檢索出用戶信息,並且附加到請求對象裡面去:
User . findOne ({ _id : decoded . iss },  function ( err , user )  { 
req
. user = user ;
});
最後,將這個中間件附加到路由里面:
var jwtauth =  require ( './jwtauth.js' );

app
. get ( '/something' , [ express . bodyParser (), jwtauth ], function ( req , res ){
// do something
});
或者匹配一些路由
app . all ( '/api/*' ,  [ express . bodyParser (), jwtauth ]);
##客戶端我們提供了一個簡單的get端去獲得一個遠端的token。這非常直接了,所以我們不用糾結細節,就是發起一個請求,傳遞用戶名和密碼,如果請求成功了,我們就會得到一個包含著token的響應。
我們現在研究的是後續的請求。一個方法是通過JQuery的ajaxSetup()方法。這可以直接用來做Ajax請求,或者通過前端框架使用包裝過的Ajax方法。比如,假設我們將我們的請求使用window.localStorage.setItem('token', 'the-long-access-token');放在本地存儲(Local Storage)裡面,我們可以通過這種方法將token附加到請求頭里面:
var token = window . localStorage . getItem ( 'token' );

if ( token ) {
$
. ajaxSetup ({
headers
: {
'x-access-token' : token
}
});
}
很簡單,但是這會劫持所有Ajax請求,如果這裡有一個token在本地存儲裡面。它將會附加到一個名為x-access-token的Header裡面。
##使用Backbone我們將前一個方法換成Backbone應用。最簡單的方法就是使用全局的Backbone.sync(),如下面所示
// Store "old" sync function 
var backboneSync = Backbone . sync

// Now override
Backbone . sync = function ( method , model , options ) {

/*
* "options" represents the options passed to the underlying $.ajax call
*/

var token = window . localStorage . getItem ( 'token' );

if ( token ) {
options
. headers = {
'x-access-token' : token
}
}

// call the original function
backboneSync
( method , model , options );
};
##更多的安全你可以保存簽證過的token記錄在服務器上,來添加一個附加的安全層,,然後在每一步驗證token的時候驗證這個記錄。這將會組織第三方偽裝一個token,也將會使得服務器可以失效一個token。我不會提到這個方面,但是它應當被直接的實現。
##總結在這篇文章,我們探究了一個API驗證的方法,仔細的查看了JSON WEB Tokens。我們使用了Node和Express來寫一個簡單的實現,而且也在客戶端使用了Backbone作為一個示例。代碼可以在GitHub上找到。
這裡還有很多我們都沒有完全實現到,比如資源的請求(claim),但是我們已經做瞭如何使用簡單的方法來構建一個獲取token的機制。在這個例子裡面,客戶端和服務端都是JS應用。
當然你可以通過其他技術使用這個方法,比如Ruby或者PHP作為後端,或者Ember或者AngularJS。除此之外,你還可以在移動應用中使用。比如,使用web技術和PhoneGap之類的結合,使用類似於Sencha之類的工具,或者完全的本地應用。

By webber0928

一個小菜鳥工程師,對籃球還有夢想的男孩。

0 意見:

張貼留言

Coprights @ 2016, Blogger Templates Designed By Templateism | Distributed By Gooyaabi Templates