https://juejin.im/post/5de4c3c76fb9a071b86cc482#heading-0javascript
本文首發於我的博客css
好久好久以前, Web基本都是文檔的瀏覽而已。既然是瀏覽, 做爲服務器, 不須要記錄在某一段時間裏都瀏覽了什麼文檔, 每次請求都是一個新的HTTP協議,就是請求加響應。不用記錄誰剛剛發了HTTP請求, 每次請求都是全新的。html
隨着交互式Web應用的興起, 像在線購物網站,須要登陸的網站等,立刻面臨一個問題,就是要管理回話,記住那些人登陸過系統,哪些人往本身的購物車中放商品,也就是說我必須把每一個人區分開。java
本文主要講解cookie,session, token 這三種是如何管理會話的;git
cookie 是一個很是具體的東西,指的就是瀏覽器裏面能永久存儲的一種數據。跟服務器沒啥關係,僅僅是瀏覽器實現的一種數據存儲功能。github
cookie由服務器生成,發送給瀏覽器,瀏覽器把cookie以KV形式存儲到某個目錄下的文本文件中,下一次請求同一網站時會把該cookie發送給服務器。因爲cookie是存在客戶端上的,因此瀏覽器加入了一些限制確保cookie不會被惡意使用,同時不會佔據太多磁盤空間。因此每一個域的cookie數量是有限制的。web
document.cookie = "name=xiaoming; age=12 " 複製代碼
設置cookie => cookie被自動添加到request header中 => 服務端接收到cookieajax
無論你是請求一個資源文件(如html/js/css/圖片), 仍是發送一個ajax請求, 服務端都會返回response.而response header中有一項叫set-cookie
, 是服務端專門用來設置cookie的;算法
set-cookie
HTML5提供了兩種本地存儲的方式 sessionStorage 和 localStorage;數據庫
session從字面上講,就是會話。這個就相似你和一我的交談,你怎麼知道當時和你交談的是張三而不是李四呢?對方確定有某種特徵(長相等)代表他是張三; session也是相似的道理,服務器要知道當前請求發給本身的是誰。爲了作這種區分,服務器就是要給每一個客戶端分配不一樣的"身份標識",而後客戶端每次向服務器發請求的時候,都帶上這個」身份標識「,服務器就知道這個請求來自與誰了。 至於客戶端怎麼保存這個」身份標識「,能夠有不少方式,對於瀏覽器客戶端,你們都採用cookie的方式。
session_id
, 寫入用戶的cookie
cookie
, 將session_id
傳回服務器session_id
, 找到前期保存的數據, 由此得知用戶的身份單機固然沒問題, 若是是服務器集羣, 或者是跨域的服務導向架構, 這就要求session數據共享,每臺服務器都可以讀取session。
舉例來講, A網站和B網站是同一家公司的關聯服務。如今要求,用戶只要在其中一個網站登陸,再訪問另外一個網站就會自動登陸,請問怎麼實現?這個問題就是如何實現單點登陸的問題
另外一種方案是服務器索性不保存session數據了,全部數據就保存在客戶端,每次請求都發回服務器。這種方案就是接下來要介紹的基於Token的驗證;
這個方式的技術其實很早就已經有不少實現了,並且還有現成的標準可用,這個標準就是JWT;
實際的JWT大概就像下面這樣:
JSON Web Tokens由dot(.)分隔的三個部分組成,它們是:
所以,JWT一般以下展現:
xxxxx.yyyyy.zzzz
Header 是一個 JSON 對象
{
"alg": "HS256", // 表示簽名的算法,默認是 HMAC SHA256(寫成 HS256) "typ": "JWT" // 表示Token的類型,JWT 令牌統一寫爲JWT } 複製代碼
Payload 部分也是一個 JSON 對象,用來存放實際須要傳遞的數據
{
// 7個官方字段 "iss": "a.com", // issuer:簽發人 "exp": "1d", // expiration time: 過時時間 "sub": "test", // subject: 主題 "aud": "xxx", // audience: 受衆 "nbf": "xxx", // Not Before:生效時間 "iat": "xxx", // Issued At: 簽發時間 "jti": "1111", // JWT ID:編號 // 能夠定義私有字段 "name": "John Doe", "admin": true } 複製代碼
JWT 默認是不加密的,任何人均可以讀到,因此不要把祕密信息放在這個部分。
Signature 是對前兩部分的簽名,防止數據被篡改。
首先,須要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。而後,使用Header裏面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 複製代碼
算出簽名後,把 Header、Payload、Signature 三個部分拼成一個字符串,每一個部分之間用"點"(.)分隔,就能夠返回給用戶。
JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature 複製代碼
如何保證安全?
客戶端收到服務器返回的 JWT,能夠儲存在 Cookie 裏面,也能夠儲存在 localStorage。此後,客戶端每次與服務端通訊,都要帶上這個JWT。你能夠把它放在Cookie裏面自動發送,可是這樣不能跨域,因此更好的作法是放在HTTP請求的頭信息 Authorization 字段裏面。
Authorization: Bearer <token>
複製代碼
另外一種作法是, 跨域的時候, JWT就放在POST請求的數據體裏。
JWT最開始的初衷是爲了實現受權和身份認證做用的,能夠實現無狀態,分佈式的Web應用受權。大體實現的流程以下
這裏須要注意:不是每次請求都要申請一次Token,這是須要注意,若是不是對於安全性要求的狀況,不建議每次都申請,由於會增長業務耗時;好比只在登錄時申請,而後使用JWT的過時時間或其餘手段來保證JWT的有效性;
JWT最大的優點是服務器再也不須要存儲Session,使得服務器認證鑑權業務能夠方便擴展。這也是JWT最大的缺點因爲服務器不須要存儲Session狀態,所以使用過程當中沒法廢棄某個Token,或者更改Token的權限。也就是說一旦JWT簽發了,到期以前就會始終有效。 咱們能夠基於上面提到的問題作一些改進。
前面講的Token,都是Acesss Token,也就是訪問資源接口時所須要的Token,還有另一種Token,Refresh Token。通常狀況下,Refresh Token的有效期會比較長。而Access Token的有效期比較短,當Acesss Token因爲過時而失效時,使用Refresh Token就能夠獲取到新的Token,若是Refresh Token也失效了,用戶就只能從新登陸了。Refresh Token及過時時間是存儲在服務器的數據庫中,只有在申請新的Acesss Token時纔會驗證,不會對業務接口響應時間形成影響,也不須要向Session同樣一直保持在內存中以應對大量的請求。
npm i --save koa koa-route koa-bodyparser @koa/cors jwt-simple
複製代碼
const Koa = require("koa"); const app = new Koa(); const route = require('koa-route'); var bodyParser = require('koa-bodyparser'); const jwt = require('jwt-simple'); const cors = require('@koa/cors'); const secret = 'your_secret_string'; // 加密用的SECRET字符串,可隨意更改 app.use(bodyParser()); // 處理post請求的參數 const login = ctx => { const req = ctx.request.body; const userName = req.userName; const expires = Date.now() + 1000 * 60; // 爲了方便測試,設置超時時間爲一分鐘後 const payload = { iss: userName, exp: expires }; const Token = jwt.encode(payload, secret); ctx.response.body = { data: Token, msg: '登錄成功' }; } const getUserName = ctx => { const token = ctx.get('authorization').split(" ")[1]; const payload = jwt.decode(token, secret); // 每次請求只判斷Token是否過時,不從新去更新Token過時時間(更新不更新Token的過時時間主要看實際的應用場景) if(Date.now() > payload.exp) { ctx.response.body = { errorMsg: 'Token已過時,請從新登陸' }; } else { ctx.response.body = { data: { username: payload.iss, }, msg: '獲取用戶名成功', errorMsg: '' }; } } app.use(cors()); app.use(route.post('/login', login)); app.use(route.get('/getUsername', getUserName)); app.listen(3200, () => { console.log('啓動成功'); }); 複製代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>JWT-demo</title> <style> .login-wrap { height: 100px; width: 200px; border: 1px solid #ccc; padding: 20px; margin-bottom: 20px; } </style> </head> <body> <div class="login-wrap"> <input type="text" placeholder="用戶名" class="userName"> <br> <input type="password" placeholder="密碼" class="password"> <br> <br> <button class="btn">登錄</button> </div> <button class="btn1">獲取用戶名</button> <p class="username"></p> </body> <script> var btn = document.querySelector('.btn'); btn.onclick = function () { var userName = document.querySelector('.userName').value; var password = document.querySelector('.password').value; fetch('http://localhost:3200/login', { method: 'POST', body: `userName=${userName}&password=${password}`, headers:{ 'Content-Type': 'application/x-www-form-urlencoded' }, mode: 'cors' // no-cors, cors, *same-origin }) .then(function (response) { return response.json(); }) .then(function (res) { // 獲取到Token,將Token放在localStorage document.cookie = `token=${res.data}`; localStorage.setItem('token', res.data); localStorage.setItem('token_exp', new Date().getTime()); alert(res.msg); }) .catch(err => { message.error(`本地測試錯誤${err.message}`); console.error('本地測試錯誤', err); }); } var btn1 = document.querySelector('.btn1'); btn1.onclick = function () { var username = document.querySelector('.username'); const token = localStorage.getItem('token'); fetch('http://localhost:3200/getUsername', { headers:{ 'Authorization': 'Bearer ' + token }, mode: 'cors' // no-cors, cors, *same-origin }) .then(function (response) { return response.json(); }) .then(function (res) { console.log('返回用戶信息結果', res); if(res.errorMsg !== '') { alert(res.errorMsg); username.innerHTML = ''; } else { username.innerHTML = `姓名:${res.data.username}`; } }) .catch(err => { console.error(err); }); } </script> </html> 複製代碼
源碼地址 以上只是一個特別簡單的例子, 對於Token過時只作了簡單的處理,不少邊界條件沒有作處理,好比異常的處理;
通常建議: 將登錄信息等重要信息存放爲session, 其餘信息若是須要保留,能夠放在cookie中
Session是一種HTTP儲存機制, 爲無狀態的HTTP提供持久機制; Token就是令牌, 好比你受權(登陸)一個程序時,它就是個依據,判斷你是否已經受權該軟件;
Session和Token並不矛盾,做爲身份認證Token安全性比Session好,由於每個請求都有簽名還能防止監聽以及重放攻擊,而Session就必須依賴鏈路層來保障通信安全了。如上所說,若是你須要實現有狀態的回話,仍然能夠增長Session來在服務端保存一些狀態。
cookie,session,Token沒有絕對的好與壞之分,只要仍是要結合實際的業務場景和需求來決定採用哪一種方式來管理回話,固然也能夠三種都用。