1. 程式人生 > >cookie、session和token區別

cookie、session和token區別

cookie 和 session

眾所周知,HTTP 是一個無狀態協議,所以客戶端每次發出請求時,下一次請求無法得知上一次請求所包含的狀態資料,如何能把一個使用者的狀態資料關聯起來呢?

比如在淘寶的某個頁面中,你進行了登陸操作。當你跳轉到商品頁時,服務端如何知道你是已經登陸的狀態?

cookie

首先產生了 cookie 這門技術來解決這個問題,cookie 是 http 協議的一部分,它的處理分為如下幾步:

伺服器向客戶端傳送 cookie。

通常使用 HTTP 協議規定的 set-cookie 頭操作。

規範規定 cookie 的格式為 name = value 格式,且必須包含這部分。

瀏覽器將 cookie 儲存。

每次請求瀏覽器都會將 cookie 發向伺服器。

其他可選的 cookie 引數會影響將 cookie 傳送給伺服器端的過程,主要有以下幾種:

path:表示 cookie 影響到的路徑,匹配該路徑才傳送這個 cookie。

expires 和 maxAge:告訴瀏覽器這個 cookie 什麼時候過期,expires 是 UTC 格式時間,maxAge 是 cookie 多久後過期的相對時間。當不設定這兩個選項時,會產生 session cookie,session cookie 是 transient 的,當用戶關閉瀏覽器時,就被清除。一般用來儲存 session 的 session_id。

secure:當 secure 值為 true 時,cookie 在 HTTP 中是無效,在 HTTPS 中才有效。

httpOnly:瀏覽器不允許指令碼操作 document.cookie 去更改 cookie。一般情況下都應該設定這個為 true,這樣可以避免被 xss 攻擊拿到 cookie。

express 中的 cookie

express 在 4.x 版本之後,session管理和cookies等許多模組都不再直接包含在express中,而是需要單獨新增相應模組。

var express = require('express');

// 首先引入 cookie-parser 這個模組

var cookieParser = require('cookie-parser');

var app = express();

app.listen(3000);

// 使用 cookieParser 中介軟體,cookieParser(secret, options)

app.use(cookieParser());

app.get('/', function (req, res) {

// 如果請求中的 cookie 存在 isVisit, 則輸出 cookie

// 否則,設定 cookie 欄位 isVisit, 並設定過期時間為1分鐘

if (req.cookies.jack) {

console.log(req.cookies.jack);

res.send("welcome");

} else {

res.cookie('jack', 'content', {maxAge: 60 * 1000});

res.send("no cookie");

}

});

這裡開發除錯的時候用supervisor來啟動,程式碼有改動,它會自動重啟,避免不必要的手動重啟工作。

新版本的開發者工具介面,在application裡面可以看到cookies 這些儲存

現在我們看到一個cookie名字為jack 內容為content的cookie就儲存了,時間期限也有。

如果沒有設定時間(maxage/expires),

那就是session cookie,

瀏覽器關閉的時候cookie就沒了。

session

cookie 雖然很方便,但是使用 cookie 有一個很大的弊端,cookie 中的所有資料在客戶端就可以被修改,資料非常容易被偽造,那麼一些重要的資料就不能存放在 cookie 中了,而且如果 cookie 中資料欄位太多會影響傳輸效率。為了解決這些問題,就產生了 session,session 中的資料是保留在伺服器端的。

session 的運作通過一個session_id來進行。session_id通常是存放在客戶端的 cookie 中,比如在 express 中,預設是connect.sid這個欄位,當請求到來時,服務端檢查 cookie 中儲存的 session_id 並通過這個 session_id 與伺服器端的 session data 關聯起來,進行資料的儲存和修改。

這意思就是說,當你瀏覽一個網頁時,服務端隨機產生一個 1024 位元長的字串,然後存在你 cookie 中的connect.sid欄位中。當你下次訪問時,cookie 會帶有這個字串,然後瀏覽器就知道你是上次訪問過的某某某,然後從伺服器的儲存中取出上次記錄在你身上的資料。由於字串是隨機產生的,而且位數足夠多,所以也不擔心有人能夠偽造。偽造成功的概率比坐在家裡程式設計時被鄰居家的狗突然闖入並咬死的機率還低。

session 可以存放在 1)記憶體、2)cookie本身、3)redis 或 memcached 等快取中,或者4)資料庫中。線上來說,快取的方案比較常見,存資料庫的話,查詢效率相比前三者都太低,不推薦;cookie session 有安全性問題,下面會提到。

express 中操作 session 要用到express-session(https://github.com/expressjs/session) 這個模組,主要的方法就是session(options),其中 options 中包含可選引數,主要有:

name: 設定 cookie 中,儲存 session 的欄位名稱,預設為connect.sid。

store: session 的儲存方式,預設存放在記憶體中,也可以使用 redis,mongodb 等。express 生態中都有相應模組的支援。

secret: 通過設定的 secret 字串,來計算 hash 值並放在 cookie 中,使產生的 signedCookie 防篡改。

cookie: 設定存放 session id 的 cookie 的相關選項,預設為

(default: { path: '/', httpOnly: true, secure: false, maxAge: null })

genid: 產生一個新的 session_id 時,所使用的函式, 預設使用uid2這個 npm 包。

rolling: 每個請求都重新設定一個 cookie,預設為 false。

resave: 即使 session 沒有被修改,也儲存 session 值,預設為 true。

1) 在記憶體中儲存 session

express-session預設使用記憶體來存 session,對於開發除錯來說很方便。

var express = require('express');

// 首先引入 express-session 這個模組

var session = require('express-session');

var app = express();

app.listen(5000);

app.use(cookieParser('jack2016'));   

//解析cookie secret為‘jack2016’的cookie,可不可以不寫secret?不寫會報錯

}));

// 按照上面的解釋,設定 session 的可選引數

app.use(session({

    secret:'jack2016',

// 建議使用 128 個字元的隨機字串,這裡不寫secret的話cookie儲存的是不加密的sessionid

    name:'jacks',    //cookie名字,這裡cookie存的內容是用secret加密的sessionid,

    cookie: {maxAge:60*2000},//cookie設定,maxAge設定時間好像受到限制,太小直接沒效,設定的夠大無論是60*60還是60*60*24*12好像都是固定的4小時,這裡有點疑惑。

}));

/* GET home page. */

router.get('/',function(req,res,next) {

console.log(req.sessionID,req.cookies.jack,req.signedCookies.jack);

res.render('index',{title:'Express'});

});

signedCookie

cookie 雖然很方便,但是使用 cookie 有一個很大的弊端,cookie 中的所有資料在客戶端就可以被修改,資料非常容易被偽造

其實不是這樣的,那只是為了方便理解才那麼寫。要知道,計算機領域有個名詞叫簽名,專業點說,叫資訊摘要演算法。

比如我們現在面臨著一個菜鳥開發的網站,他用 cookie 來記錄登陸的使用者憑證。相應的 cookie 長這樣:dotcom_user=alsotang,它說明現在的使用者是 alsotang 這個使用者。如果我在瀏覽器中裝個外掛,把它改成dotcom_user=ricardo,伺服器一讀取,就會誤認為我是 ricardo。然後我就可以進行 ricardo 才能進行的操作了。之前 web 開發不成熟的時候,用這招甚至可以黑個網站下來,把 cookie 改成dotcom_user=admin就行了,唉,那是個玩黑客的黃金年代啊。

OK,現在我有一些資料,不想存在 session 中,想存在 cookie 中,怎麼保證不被篡改呢?答案很簡單,籤個名。

假設我的伺服器有個祕密字串,是this_is_my_secret_and_fuck_you_all,我為使用者 cookie 的dotcom_user欄位設定了個值alsotang。cookie 本應是

{dotcom_user: 'alsotang'}

這樣的。

而如果我們籤個名,比如把dotcom_user的值跟我的 secret_string 做個 sha1

sha1('this_is_my_secret_and_fuck_you_all' + 'alsotang') === '4850a42e3bc0d39c978770392cbd8dc2923e3d1d'

然後把 cookie 變成這樣

{

dotcom_user: 'alsotang',

'dotcom_user.sig': '4850a42e3bc0d39c978770392cbd8dc2923e3d1d',

}

這樣一來,使用者就沒法偽造資訊了。一旦它更改了 cookie 中的資訊,則伺服器會發現 hash 校驗的不一致。

畢竟他不懂我們的 secret_string 是什麼,而暴力破解雜湊值的成本太高。

console.log(req.sessionID,req.headers.cookie.split('=')[1],req.cookies.jack,req.signedCookies.jack);

24iyzhhm1c3xbtaZR6ZTwBiN6Y6oXtvA         s%3ADBAuR10qvoQzVl_LCA6BYOfcyDobptqH.mIKackk41TL7vpkpBWXnbARR5tx%2BT8CbFIsIn%2F40J6w           undefined            DBAuR10qvoQzVl_LCA6BYOfcyDobptqH

app.use(cookieParser())    vs app.use(cookieParser('jack2016'))

寫了secret後req.cookies.jack就undefined

只能通過req.headers.cookie分割獲取了

我們比較req.sessionID和req.signedCookies.jack發現是加密前的一樣的  我們在瀏覽器村的cookie是機密的,也就是signed cookie,

分割線,傳統的身份驗證方法從最早的cookie到session以及給session cookie做個加密,接下來我們來看看token認證。

諸如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,因為不能保證這些request是由瀏覽器所發出的。自然,我們需要一個新的機制。這篇文章關注於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。因為這篇文章的目的,我們將忽視第三個引數。但是你可以閱讀這篇文章.這個iss是issuer的簡寫,表明請求的實體。通常意味著請求API的使用者。exp是expires的簡寫,是用來限制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');varjwt=require('jwt-simple');

var app=express();app.set('jwtTokenSecret','YOUR_SECRET_STRING');

獲取一個Token我們需要做的第一件事就是讓客戶端通過他們的賬號密碼交換token。這裡有2種可能的方法在RESTful API裡面。第一種是使用POST請求來通過驗證,使服務端傳送帶有token的響應。除此之外,你可以使用GET請求,這需要他們使用引數提供憑證(指URL),或者更好的使用請求頭。

這篇文章的目的是為了解釋token驗證的方法而不是基本的使用者名稱/密碼驗證機制。所以我們假設我們已經通過請求得到了使用者名稱和密碼:

/**

* 驗證token

*/

functionauthToken(credentialsRequired) {

returncompose()

.use(function(req,res,next) {

if(req.query && req.query.hasOwnProperty('access_token')) {

req.headers.authorization='Bearer '+ req.query.access_token;

}

next();

})

.use(expressJwt({

secret: config.session.secrets,

credentialsRequired:credentialsRequired//是否丟擲錯誤

}))

}

/**

* 驗證使用者是否登入

*/

functionisAuthenticated() {

returncompose()

.use(authToken(true))

.use(function(err,req,res,next) {

//expressJwt 錯誤處理中介軟體

if(err.name==='UnauthorizedError') {

returnres.status(401).send();

}

next();

})

.use(function(req,res,next) {

User.findById(req.user._id,function(err,user) {

if(err)returnres.status(500).send();

if(!user)returnres.status(401).send();

req.user= user;

next();

});

});

}

/**

* 驗證使用者許可權

*/

functionhasRole(roleRequired) {

if(!roleRequired)throw newError('Required role needs to be set');

returncompose()

.use(isAuthenticated())

.use(functionmeetsRequirements(req,res,next) {

if(config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) {

next();

}

else{

returnres.status(403).send();

}

});

}

下一步,我們就需要返回JWT token通過一個驗證成功的響應。

注意到jwt.encode()函式有2個引數。第一個就是一個需要加密的物件,第二個是一個加密的金鑰。這個token是由我們之前提到的iss和exp組成的。注意到Moment.js被用來設定token將在7天之後失效。而res.json()方法用來傳遞這個JSON物件給客戶端。

驗證Token

為了驗證JWT,我們需要寫出一些可以完成這些功能的中介軟體(Middleware):

檢查附上的token

試圖解密

驗證token的可用性

如果token是合法的,檢索裡面使用者的資訊,以及附加到請求的物件上

我們來寫一箇中間件的框架

為了獲得最大的可擴充套件性,我們允許客戶端使用一下3個方法附加我們的token:作為請求連結(query)的引數,作為主體的引數(body),和作為請求頭(Header)的引數。對於最後一個,我們將使用Headerx-access-token。

下面是我們的允許在中介軟體的程式碼,試圖去檢索token:

下一步,我們講解析JWT:

如果解析的過程失敗,那麼JWT Simple元件將會丟擲一段異常。如果異常發生了,或者沒有token,我們將會呼叫next()來繼續處理請求。這代表喆我們無法確定使用者。如果一個合格的token合法並且被解碼,我們應該得到2個屬性,iss包含著使用者ID以及exp包含token過期的時間戳。我們將首先處理後者,如果它過期了,我們就拒絕它:

最後,將這個中介軟體附加到路由裡面:

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 functionvarbackboneSync=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 functionbackboneSync(method,model,options);};

更多的安全

你可以儲存簽證過的token記錄在伺服器上,來新增一個附加的安全層,,然後在每一步驗證token的時候驗證這個記錄。這將會組織第三方偽裝一個token,也將會使得伺服器可以失效一個token。我不會提到這個方面,但是它應當被直接的實現