Cookie與Session

並不是真實項目案例,僅爲個人學習筆記,僅供學習參考javascript

Cookie簡介

爲何有cookie

如下內容摘自維基百科:html

爲何有Cookie?
由於HTTP協議是無狀態的,即服務器不知道用戶上一次作了什麼,這嚴重阻礙了交互式Web應用程序的實現。
在典型的網上購物場景中,用戶瀏覽了幾個頁面,買了一盒餅乾和兩瓶飲料。
最後結賬時,因爲HTTP的無狀態性,不經過額外的手段,服務器並不知道用戶到底買了什麼。
因此Cookie就是用來繞開HTTP的無狀態性的「額外手段」之一。
服務器能夠設置或讀取Cookies中包含信息,藉此維護用戶跟服務器會話中的狀態。

在剛纔的購物場景中,當用戶選購了第一項商品,服務器在向用戶發送網頁的同時,還發送了一段Cookie,記錄着那項商品的信息。
當用戶訪問另外一個頁面,瀏覽器會把Cookie發送給服務器,因而服務器知道他以前選購了什麼。
用戶繼續選購飲料,服務器就在原來那段Cookie裏追加新的商品信息。
結賬時,服務器讀取發送來的Cookie就好了。

Cookie另外一個典型的應用是當登陸一個網站時,網站每每會請求用戶輸入用戶名和密碼,而且用戶能夠勾選「下次自動登陸」。
若是勾選了,那麼下次訪問同一網站時,用戶會發現沒輸入用戶名和密碼就已經登陸了。
這正是由於前一次登陸時,服務器發送了包含登陸憑據(用戶名加密碼的某種加密形式)的Cookie到用戶的硬盤上。
第二次登陸時,若是該Cookie還沒有到期,瀏覽器會發送該Cookie,服務器驗證憑據,因而沒必要輸入用戶名和密碼就讓用戶登陸了。
複製代碼

以上內容是維基百科對Cookie的解釋,爲何有Cookie? 說白了: 由於瀏覽器與人須要一種交互,Cookie則實現了這種交互;服務器經過向用戶發送Cookie給予了用戶某種「權限」,這比如是一張門票,當瀏覽器攜帶着「門票」向服務器發起請求時,服務器經過驗證「門票」信息就能夠賦予用戶某種「權限」。接下來我用註冊與登錄的示例來簡單地介紹下 Cookie的具體應用場景。前端

註冊登錄

代碼連接
java

本案例中,註冊登錄作的並不全面,可是基本思想和真實的註冊登錄還算吻合。案例中並無真實的數據庫,而是使用了json文件充當數據庫,json文件中僅有一個數組,用來存儲用戶的信息。當用戶註冊成功後,則將用戶的帳號密碼存儲到咱們的"數據庫"中,在此以前須要經過前端校驗代碼以及後端的校驗代碼,本案例中僅有前端的校驗,然後端校驗代碼並無在案例中展現。在校驗成功時,頁面會由註冊界面自動跳轉到登錄界面,而且用戶的信息會被添加到json文件中,若是用戶再次使用相同的郵箱進行註冊時,界面則會提示「用戶已存在」的信息。示例效果以下:
node


若是註冊成功,json文件中則會新增用戶的信息:


再次使用該郵箱進行註冊時:


跳轉到登錄界面後,若是用戶輸入的郵箱及密碼與數據庫的數據信息匹配,則登錄成功,頁面會跳轉至index頁面,同時服務器會在返回給客戶端的響應頭中添加cookie,在cookie中存入了用戶的信息:

if(isEmailMatch && isPasswordMatch){
    response.setHeader('Set-Cookie',[`mysite_email=${userMessage.email};Max-Age=300;HttpOnly`,`mysite_password=${userMessage.password};Max-Age=300;HttpOnly`]);
    response.statusCode = 200;
}else if(isEmailMatch && !isPasswordMatch){

    //  錯誤代碼 401 : 未受權,登錄失敗
    response.statusCode = 401;
    response.setHeader('Content-Type','application/json;charset=utf-8');
    response.write(`
        {
            "errors":{
                "error":"passwordWrong"
            }
        }
    `);
}else if (!isEmailMatch){
    response.statusCode = 401;
    response.setHeader('Content-Type','application/json;charset=utf-8');
    response.write(`
        {
            "errors":{
                "error":"notRegister"
            }
        }
    `);
}
複製代碼

本段代碼中若是用戶登錄的郵箱及密碼均正確則服務器會在響應頭中「Set-Cookie」,本示例中演示瞭如何設置多個cookie的方法即即:git

response.setHeader('Set-Cookie',['cookieName1=val1','cookieName2=val2']);
複製代碼

同時在Set-Cookie中 Max-Age表明在 cookie 失效以前須要通過的秒數;設置了 HttpOnly 屬性的 cookie 不能使用 JavaScript 經由 Document.cookie 屬性、XMLHttpRequest 和 Request APIs 進行訪問,以防範跨站腳本攻擊(XSS)。若是須要了解具體詳細的Set-Cookie設置信息能夠參考 MDN的Set-Cookie。 在代碼中,我也作出了一些最簡單的後臺判斷,例如郵箱輸入正確但密碼輸入錯誤時,服務器reponse一個json格式的數據流,前端代碼經過判斷會將錯誤信息反饋至頁面上,部分代碼以下:github

// ajax post
$.post('/login',userMessage)
    .then(
        // success
        ()=>{
        // 跳轉至 index 頁面
        window.location.href = '/'
        },
        // fail
        (request)=>{
            let errorMessage = request.responseJSON.errors.error;
                        
            if(errorMessage === 'passwordWrong'){
                errorText("password","密碼錯誤");
                return
            }else if(errorMessage === 'notRegister'){
                errorText("email","無此用戶信息");
                return
            }
        }
    )
複製代碼

具體的效果以下:
密碼錯誤:
ajax


無此用戶信息:


在登錄成功後,跳轉至index主頁,當瀏覽器向服務器發起get請求獲取主頁時,由於瀏覽器中攜帶了同域名的cookie信息,也就是攜帶了"入場票據",在服務器檢查了"票據"後,響應給用戶一個經處理過的頁面:


若是咱們在未攜帶cookie信息訪問index頁面時,則會獲取到這樣的內容~


在點擊退出登錄後,頁面會跳轉到login頁面,同時也會清除cookie的信息,固然咱們須要 先將後臺代碼的 Set-Cookie中的HttpOnly刪除掉!! 若是不將HttpOnly刪除,那麼咱們是沒法調用document.cookie的~ 退出登錄button事件的代碼以下:

// 退出登錄時清空 cookie
$('#clearCookie').on('click',()=>{
    deleteCookie('mysite_password');
    deleteCookie('mysite_email');
    window.location.href = '/login';
})
function deleteCookie (name){
    // 設置讓 cookie 無效的方法 讓cookie的期限 expires 爲 如今
    document.cookie = name + '=; expires='+new Date().toGMTString();
}
複製代碼

咱們看到了Cookie在註冊登錄中的做用,當用戶攜帶着某域名能夠識別的Cookie時,服務器就能夠經過用戶的"票據"返回給用戶不一樣的頁面,若是咱們未註冊登錄直接訪問 index主頁,得到到的信息與攜帶Cookie訪問主頁時得到的信息是大相徑庭的。這就是Cookie應用最多的一個場景,可是這樣作實際上也是有問題的,用戶徹底能夠仿造Cookie,只要用戶有一點點的HTTP知識,修改Cookie僞造用戶是徹底能夠的,這就不免帶來了安全問題。算法

Session

代碼連接
數據庫

Cookie帶來的問題就是安全問題,它能夠被任意地修改和僞造,因而Session就出現了,在說明什麼是Session以前,咱們先試圖去改進代碼帶來的安全性問題:
首先在 server.js 中 聲明一個全局變量 mySession

var mySession = {};
複製代碼

在用戶登錄成功以後,傳給用戶的cookie就不是用戶的信息了,咱們傳給用戶一個sessionId,sessionId的值是一個隨機數:

if(isEmailMatch && isPasswordMatch){
    // add Session
    let sessionId = Math.random();
    mySession[`${sessionId}`] = {'mysite_email':userMessage.email,'mysite_password':userMessage.password};

    response.setHeader('Set-Cookie',`sessionId=${sessionId}`);
    response.statusCode = 200;
}
複製代碼

當用戶訪問index首頁時,咱們須要對用戶cookie中 sessionId的值進行判斷:

if(path === '/'){
    let string = fs.readFileSync('./index.html','utf8');
    //  若是請求頭中 攜帶 cookie 信息
    if(request.headers.cookie){
    let hash = {};
    request.headers.cookie.split('; ') .forEach((item)=>{ 
        if(item.split('=')[0] === 'sessionId' && mySession[item.split('=')[1]]){
            hash['mysite_email'] = mySession[item.split('=')[1]].mysite_email;
            hash['mysite_password'] = mySession[item.split('=')[1]].mysite_password;
        }
    });
    ... ...
}
複製代碼

咱們僅僅修改了幾行代碼,cookie中再也不存入用戶的隱私信息而是改成了sessionId 而sessionId對應的value是一串隨機數,後臺代碼只須要驗證這串隨機數便可,這樣依賴即使用戶擁有修改cookie的能力,也無能爲力,由於cookie存儲在客戶端,而session存儲的隱私信息都是在服務器上的。cookie攜帶的"票證"僅僅是sessionId,服務器則是經過cookie攜帶的sessionId 來進行判斷的。咱們來總結下:

Cookie:
1. 服務器經過Response-Header 的 Set-Cookie給客戶端一串字符串
2. 客戶端每次訪問相同域名的網頁時,帶上這串字符串,服務器能夠經過這串字符串去讀取客戶端的信息
3. 客戶端要在一段時間內保存這個Cookie
4. Cookie默認在用戶關閉頁面後就會失效,可是後臺代碼能夠任意設置Cookie的過時時間

Session:
1. 將SessionId經過Cookie發給客戶端
2. 客戶端訪問服務器時,服務器讀取SessionId
3. 服務器有一塊內存保存了全部的Session
4. 經過SessionId能夠獲得對應用戶的隱私信息
5. 這塊內存就是服務器上的全部session

Session與Cookie的聯繫與區別:
1. Session是依賴於Cookie實現的
2. Cookie存儲在客戶端,Session則存儲在服務器上

複製代碼

LocalStorage與 SessionStorage

如今假設咱們的網站更新了,須要在首頁提示用戶更新的內容。咱們只須要很簡單的代碼就能夠實現這一項功能:

// update message
alert('咱們的網站更新啦~更新的內容有:...');
複製代碼

可是,從用戶體驗上來說,用戶只但願能被提醒一次,有什麼方法能夠作到呢?答案就是LocalStorage。MDN: LocalStorage.
與LocalStorage類似的是SessionStorage。兩者的區別是:存儲在 localStorage 的數據能夠長期保留;而當頁面會話結束——也就是說,當頁面被關閉時,存儲在 sessionStorage 的數據會被清除 。如下是MDN LocalStorage的示例:

  1. setItem
    localStorage.setItem('myCat', 'Tom');
    複製代碼
  2. getItem
    let cat = localStorage.getItem('myCat');
    複製代碼
  3. removeItem
    localStorage.removeItem('myCat');
    複製代碼
  4. clear
    // 移除全部
    localStorage.clear();
    複製代碼

咱們使用LocalStorage對本案例的更新提示功能作一個優化:

// update message
let already = localStorage.getItem('already',true);
if(!already){
    let updateMessage = localStorage.setItem('already',true);
    alert('咱們的網站更新啦~更新的內容有:...');
}
複製代碼

這樣一來,就能保證每一個用戶都看到更新提示,且更新提示只出現了一次。
LocalStorage 及 SessionStorage 總結:

LocalStorage:
1. LocalStorage 同 HTTP無關
2. HTTP不會帶上LocalStorage的值
3. 只有相同域名的頁面才能互相讀取LocalStorage(沒有同源策略那麼嚴格)
4. 每一個域名的LocalStorage 最大存儲量 爲 5Mb左右
5. 經常使用場景:記錄有沒有提示過用戶
6. LocalStorage 永久有效,除非主動清除數據

SessionStorage與LocalStorage的區別:
1. SessionStorage在關閉頁面後(會話結束)就會失效
2. LocalStorage則永久有效(除非用戶本身清除)

Cookie與LocalStorage的區別:
1. LocalStorage和HTTP無關,Cookie則是服務器在
響應頭中設置了Set-Cookie後傳給瀏覽器,每次瀏覽器訪問
服務器都要帶上Cookie
2. Cookie的存儲量通常有4kb LocalStorage則有 5mb左右
複製代碼

Cache-Control 與 ETag

MDN: Cache-Control
Cache-Control能夠對用戶體驗進行優化~ 假設我這個項目用到了Vue,(事實上我將Vue.js代碼下載到了main.js當中,而且在node.js中新增了/main.js的路由,並在index頁面中的script標籤中引入了main.js),試想一下,用戶每次刷新頁面都會去向服務器發起請求 main.js這個文件 。在個人電腦上,請求的時間大約爲:


一個文件請求到下載則須要13ms的時間,試想一下當有多個文件須要下載時,時間必然更爲長久,那麼有什麼辦法能夠進行優化呢?首先可使用Cache-Control。

if(path === '/main.js'){
    let string = fs.readFileSync('./main.js','utf8');
    response.setHeader('Content-Type','application/javascript;charset=utf-8');
    response.setHeader('Cache-Control','max-age=31536000'); // 設置緩存時間爲1年
    response.statusCode = 200;
    response.write(string);
    response.end();
}
複製代碼

在/main.js的路由下,咱們添加了這一行代碼:

response.setHeader('Cache-Control','max-age=31536000');
複製代碼

當咱們再次刷新頁面時:


能夠看到在開發者工具的文件大小這一欄下:本來369KB的文件變爲了"from memor cache"的字樣,而時間則由本來的13ms變爲了0 ms。這是由於,在第一次請求時main.js這個文件時,main.js已經被客戶端的本地緩存了,當用戶向index頁面發其請求時,瀏覽器由於已經緩存了main.js這個文件,因此就不會再向服務器發起main.js文件的請求。那麼問題來了,我雖然設置了 response.setHeader('Cache-Control','max-age=31536000');這行代碼,而且用戶能夠緩存文件一年之久,可是若是我在中途更新了文件,用戶怎麼才能請求到呢?其實很簡單,咱們只須要再index的script標籤的src中添加query參數便可:

<script src="./main.js?version=2"></script>
複製代碼

這樣一來,每當咱們的 main.js 文件更新須要用戶進行下載的時候,咱們在src的path後面使用query參數指定版本號便可。除了可使用Cache-Control外,咱們還可使用ETag來進行優化,那麼什麼是ETag呢?MDN的解釋以下:

ETagHTTP響應頭是資源的特定版本的標識符。
這可讓緩存更高效,並節省帶寬,由於若是內容沒有改變,Web服務器不須要發送完整的響應。
而若是內容發生了變化,使用ETag有助於防止資源的同時更新相互覆蓋(「空中碰撞」)。
複製代碼

在瞭解到底什麼是ETag以前,咱們首先須要瞭解下 md5 消息摘要算法。使用npm install md5 下載md5後,在node.js中添加MD5模塊後咱們就可使用md5了,在文件中添加:var md5 = require('md5');便可。首先,先簡單瞭解下md5的應用場景,MDN的解釋以下:MD5已經普遍使用在爲文件傳輸提供必定的可靠性方面。例如,服務器預先提供一個MD5校驗和,用戶下載完文件之後,用MD5算法計算下載文件的MD5校驗和,而後經過檢查這兩個校驗和是否一致,就能判斷下載的文件是否出錯。說白了md5的本質是用來檢測文件下載是否一致的,當文件出現了絲毫的變更,經過md5算法獲得的字符串也會出現很大的差別。示例代碼以下:

if(path === '/main.js'){
    let string = fs.readFileSync('./main.js','utf8');
    response.setHeader('Content-Type','application/javascript;charset=utf-8');
    // md5 ETag 緩存
    let fileMD5 = md5(string);
    response.setHeader('ETag',fileMD5);
    if(request.headers['if-none-match'] === fileMD5){
        // 304: not modified
        response.statusCode = 304;
    }else{
        response.statusCode = 200;
        response.write(string);
    }
        response.end();
    }
複製代碼

首先咱們將fs.readFileSync讀取的文件字符串經過md5算法變成一個字符串長串,在響應頭中設置了response.setHeader('ETag',fileMD5);,瀏覽器的請求頭中會帶有'if-none-match'這樣一個屬性,它的內容與示例中的fileMD5內容是同樣的。第一次用戶向服務器發起請求:


main.js文件大小爲 366KB,請求到下載完成的時間爲119ms,好,咱們刷新頁面:


在main.js文件未改動的狀況下,大小變爲了182B,而且時間上也有了顯著的縮短。仔細想想程序所做的事情:若是文件未發生變更 那麼就向用戶發送 304 即 not modified ;反之 則將 改動的文件響應給用戶。那麼看到這裏,想必你已經知道了Cache-Control與ETag的緩存有什麼區別了;個人總結以下:

如:response.setHeader('Cache-Control','max-age=31536000'); 
    的含義是請求到文件後,將文件緩存1年,當用戶刷新頁面時瀏覽器則不會再向
    服務器發起請求
對於 ETag的緩存則是:
    瀏覽器會向服務器發起請求,但服務器則會判斷
    若是服務器發現要響應給瀏覽器的內容的md5字符串與request.headers['if-none-match']
    相等則說明服務器以前響應給用戶的文件信息是一致的,若是不相等
    則說明須要從新響應給用戶
    瀏覽器仍是會向服務器發起請求,但服務器不必定會響應給瀏覽器

複製代碼

文章結束~感謝您的耐心閱讀,若是有錯誤,還望批評指出。

相關文章
相關標籤/搜索