cookie、session和token
https://zhuanlan.zhihu.com/p/25495290?utm_source=wechat_session&utm_medium=social
一、cookie
眾所周知,HTTP 是一個無狀態協議,所以客戶端每次發出請求時,下一次請求無法得知上一次請求所包含的狀態數據,如何能把一個用戶的狀態數據關聯起來呢?
1、首先產生了 cookie 這門技術來解決這個問題,cookie 是 http 協議的一部分,它的處理分為如下幾步:
1 服務器向客戶端發送 cookie。 2 通常使用 HTTP 協議規定的 set-cookie 頭操作。 3 規範規定 cookie 的格式為 name = value 格式,且必須包含這部分。4 瀏覽器將 cookie 保存。 5 每次請求瀏覽器都會將 cookie 發向服務器。
2、其他可選的 cookie 參數會影響將 cookie 發送給服務器端的過程,主要有以下幾種:
1 path:表示 cookie 影響到的路徑,匹配該路徑才發送這個 cookie。 2 expires 和 maxAge:告訴瀏覽器這個 cookie 什麽時候過期,expires 是 UTC 格式時間,maxAge 是 cookie 多久後過期的相對時間。當不設置這兩個選項時,會產生 session cookie,session cookie 是 transient 的,當用戶關閉瀏覽器時,就被清除。一般用來保存 session 的 session_id。3 secure:當 secure 值為 true 時,cookie 在 HTTP 中是無效,在 HTTPS 中才有效。 4 httpOnly:瀏覽器不允許腳本操作 document.cookie 去更改 cookie。一般情況下都應該設置這個為 true,這樣可以避免被 xss 攻擊拿到 cookie。
3、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 會帶有這個字符串,然後瀏覽器就知道你是上次訪問過的某某某,然後從服務器的存儲中取出上次記錄在你身上的數據。由於字符串是隨機產生的,而且位數足夠多,所以也不擔心有人能夠偽造。偽造成功的概率比坐在家裏編程時被鄰居家的狗突然闖入並咬死的幾率還低。
1、session 可以存放在地方
1)內存
2)cookie本身
3)redis 或 memcached 等緩存中
4)數據庫中。
線上來說,緩存的方案比較常見,存數據庫的話,查詢效率相比前三者都太低,不推薦;cookie session 有安全性問題,下面會提到。
2、express 中操作 session
express 中操作 session 要用到express-session(expressjs/session: Simple session middleware for Express) 這個模塊,主要的方法就是session(options),其中 options 中包含可選參數,主要有:
1 name: 設置 cookie 中,保存 session 的字段名稱,默認為connect.sid。 2 store: session 的存儲方式,默認存放在內存中,也可以使用 redis,mongodb 等。express 生態中都有相應模塊的支持。 3 secret: 通過設置的 secret 字符串,來計算 hash 值並放在 cookie 中,使產生的 signedCookie 防篡改。 4 cookie: 設置存放 session id 的 cookie 的相關選項,默認為(default: { path: ‘/‘, httpOnly: true, secure: false, maxAge: null }) 5 genid: 產生一個新的 session_id 時,所使用的函數, 默認使用uid2這個 npm 包。 6 rolling: 每個請求都重新設置一個 cookie,默認為 false。 7 resave: 即使 session 沒有被修改,也保存 session 值,默認為 true。
3、在內存中存儲 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 中的所有數據在客戶端就可以被修改,數據非常容易被偽造
其實不是這樣的,那只是為了方便理解才那麽寫。要知道,計算機領域有個名詞叫簽名,專業點說,叫信息摘要算法。
三、token
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作為前端。
1、背景
我們來簡短的看一下幾個通常的保護(secure)API的方法。
第一個是使用在HTTP規範中所制定的Basic Auth, 它需要在在響應中設定一個驗證身份的Header。客戶端必須在每個子響應是附加它們的憑證(credenbtial),包括它的密碼。如果這些憑證通過了,那麽用戶的信息就會被傳遞到服務端應用。
第二個方面有點類似,但是使用應用自己的驗證機制。通常包括將發送的憑證與存儲的憑證進行檢查。和Basic Auth相比,這種需要在每次請求(call)中發送憑證。
第三種是OAuth(或者OAuth2)。為第三方的認證所設計,但是更難配置。至少在服務器端更難。
2、使用Token的方法
不是在每一次請求時提供用戶名和密碼的憑證。我們可以讓用戶通過token交換憑證(we can allow the client to exchange valid credentials for a token),這個token提供用戶訪問服務器的權限。Token通常比密碼更加長而且復雜。比如說,JWTs通常會應對長達150個字符。一旦獲得了token,在每次調用API的時候都要附加上它。然後,這仍然比直接發送賬戶和密碼更加安全,哪怕是HTTPS。
把token想象成一個安全的護照。你在一個安全的前臺驗證你的身份(通過你的用戶名和密碼),如果你成功驗證了自己,你就可以取得這個。當你走進大樓的時候(試圖從調用API獲取資源),你會被要求驗證你的護照,而不是在前臺重新驗證。
3、關於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
在規範中,有一些選擇性的附加屬性。有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對象給客戶端。
cookie、session和token