前言
無狀態的HTTP協議
好久好久以前, Web基本都是文檔的瀏覽而已。既然是瀏覽, 做爲服務器, 不須要記錄在某一段時間裏都瀏覽了什麼文檔, 每次請求都是一個新的HTTP協議,就是請求加響應。不用記錄誰剛剛發了HTTP請求, 每次請求都是全新的。css
如何管理會話
隨着交互式Web應用的興起, 像在線購物網站,須要登陸的網站等,立刻面臨一個問題,就是要管理回話,記住那些人登陸過系統,哪些人往本身的購物車中放商品,也就是說我必須把每一個人區分開。html
本文主要講解cookie,session, token 這三種是如何管理會話的;git
cookie
cookie 是一個很是具體的東西,指的就是瀏覽器裏面能永久存儲的一種數據。跟服務器沒啥關係,僅僅是瀏覽器實現的一種數據存儲功能。github
cookie由服務器生成,發送給瀏覽器,瀏覽器把cookie以KV形式存儲到某個目錄下的文本文件中,下一次請求同一網站時會把該cookie發送給服務器。因爲cookie是存在客戶端上的,因此瀏覽器加入了一些限制確保cookie不會被惡意使用,同時不會佔據太多磁盤空間。因此每一個域的cookie數量是有限制的。web
如何設置
客戶端設置
document.cookie = "name=xiaoming; age=12 "
-
客戶端能夠設置cookie的一下選項: expires, domain, path, secure(只有在https協議的網頁中, 客戶端設置secure類型cookie才能生效), 但沒法設置httpOnly選項
設置cookie => cookie被自動添加到request header中 => 服務端接收到cookieajax
服務端設置
無論你是請求一個資源文件(如html/js/css/圖片), 仍是發送一個ajax請求, 服務端都會返回response.而response header中有一項叫set-cookie
, 是服務端專門用來設置cookie的;算法
-
一個set-cookie只能設置一個cookie, 當你想設置多個, 須要添加一樣多的 set-cookie
-
服務端能夠設置cookie的全部選項: expires, domain, path, secure, HttpOnly
Cookie,SessionStorage,LocalStorage
HTML5提供了兩種本地存儲的方式 sessionStorage 和 localStorage;數據庫
session
什麼是session
session從字面上講,就是會話。這個就相似你和一我的交談,你怎麼知道當時和你交談的是張三而不是李四呢?對方確定有某種特徵(長相等)代表他是張三;session也是相似的道理,服務器要知道當前請求發給本身的是誰。爲了作這種區分,服務器就是要給每一個客戶端分配不一樣的"身份標識",而後客戶端每次向服務器發請求的時候,都帶上這個」身份標識「,服務器就知道這個請求來自與誰了。至於客戶端怎麼保存這個」身份標識「,能夠有不少方式,對於瀏覽器客戶端,你們都採用cookie的方式。npm
過程(服務端session + 客戶端 sessionId)
![](http://static.javashuo.com/static/loading.gif)
-
1.用戶向服務器發送用戶名和密碼 -
2.服務器驗證經過後,在當前對話(session)裏面保存相關數據,好比用戶角色, 登錄時間等; -
3.服務器向用戶返回一個 session_id
, 寫入用戶的cookie
-
4.用戶隨後的每一次請求, 都會經過 cookie
, 將session_id
傳回服務器 -
5.服務端收到 session_id
, 找到前期保存的數據, 由此得知用戶的身份
存在的問題
擴展性很差
單機固然沒問題, 若是是服務器集羣, 或者是跨域的服務導向架構, 這就要求session數據共享,每臺服務器都可以讀取session。json
舉例來講, A網站和B網站是同一家公司的關聯服務。如今要求,用戶只要在其中一個網站登陸,再訪問另外一個網站就會自動登陸,請問怎麼實現?這個問題就是如何實現單點登陸的問題
-
Nginx ip_hash 策略,服務端使用 Nginx 代理,每一個請求按訪問 IP 的 hash 分配,這樣來自同一 IP 固定訪問一個後臺服務器,避免了在服務器 A 建立 Session,第二次分發到服務器 B 的現象。 -
Session複製:任何一個服務器上的 Session 發生改變(增刪改),該節點會把這個 Session 的全部內容序列化,而後廣播給全部其它節點。 -
共享Session:將Session Id 集中存儲到一個地方,全部的機器都來訪問這個地方的數據。這種方案的優勢是架構清晰,缺點是工程量比較大。另外,持久層萬一掛了,就會單點失敗;
另外一種方案是服務器索性不保存session數據了,全部數據就保存在客戶端,每次請求都發回服務器。這種方案就是接下來要介紹的基於Token的驗證;
Token
過程
![](http://static.javashuo.com/static/loading.gif)
-
用戶經過用戶名和密碼發送請求 -
程序驗證 -
程序返回一個簽名的token給客戶端 -
客戶端儲存token, 而且每次用每次發送請求 -
服務端驗證Token並返回數據
這個方式的技術其實很早就已經有不少實現了,並且還有現成的標準可用,這個標準就是JWT;
JWT(JSON Web Token)
數據結構
實際的JWT大概就像下面這樣:
JSON Web Tokens由dot(.)分隔的三個部分組成,它們是:
-
Header(頭部) -
Payload(負載) -
Signature(簽名)
所以,JWT一般以下展現:
xxxxx.yyyyy.zzzz
Header(頭部)
Header 是一個 JSON 對象
{
"alg": "HS256", // 表示簽名的算法,默認是 HMAC SHA256(寫成 HS256)
"typ": "JWT" // 表示Token的類型,JWT 令牌統一寫爲JWT
}
Payload(負載)
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(簽名)
Signature 是對前兩部分的簽名,防止數據被篡改。
首先,須要指定一個密鑰(secret)。這個密鑰只有服務器才知道,不能泄露給用戶。而後,使用Header裏面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出簽名後,把 Header、Payload、Signature 三個部分拼成一個字符串,每一個部分之間用"點"(.)分隔,就能夠返回給用戶。
JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature
如何保證安全?
-
發送JWT要使用HTTPS;不使用HTTPS發送的時候,JWT裏不要寫入祕密數據 -
JWT的payload中要設置expire時間
使用方式
客戶端收到服務器返回的 JWT,能夠儲存在 Cookie 裏面,也能夠儲存在 localStorage。此後,客戶端每次與服務端通訊,都要帶上這個JWT。你能夠把它放在Cookie裏面自動發送,可是這樣不能跨域,因此更好的作法是放在HTTP請求的頭信息 Authorization 字段裏面。
Authorization: Bearer <token>
另外一種作法是, 跨域的時候, JWT就放在POST請求的數據體裏。
JWT 的做用
JWT最開始的初衷是爲了實現受權和身份認證做用的,能夠實現無狀態,分佈式的Web應用受權。大體實現的流程以下
-
客戶端須要攜帶用戶名/密碼等可證實身份的的內容去受權服務器獲取JWT信息; -
每次服務都攜帶該Token內容與Web服務器進行交互,由業務服務器來驗證Token是不是受權發放的有效Token,來驗證當前業務是否請求合法。
這裏須要注意:不是每次請求都要申請一次Token,這是須要注意,若是不是對於安全性要求的狀況,不建議每次都申請,由於會增長業務耗時;好比只在登錄時申請,而後使用JWT的過時時間或其餘手段來保證JWT的有效性;
Acesss Token,Refresh Token
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同樣一直保持在內存中以應對大量的請求。
![](http://static.javashuo.com/static/loading.gif)
一個簡單的JWT使用示例
準備
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>
運行代碼
![](http://static.javashuo.com/static/loading.gif)
源碼地址[1]以上只是一個特別簡單的例子, 對於Token過時只作了簡單的處理,不少邊界條件沒有作處理,好比異常的處理;
區別
Cookie和Session的區別
-
存儲位置不一樣:cookie數據存放在客戶的瀏覽器上,session數據放在服務器上 -
隱私策略不一樣:cookie不是很安全, 別人能夠分析存放在本地的cookie並進行cookie欺騙,考慮到安全應當使用session -
session會在必定時間內保存在服務器上。當訪問增多,就會比較佔用你服務器的性能,考慮到減輕服務器性能方面,應當使用cookie -
存儲大小不一樣:單個cookie保存的數據不能超過4k, 不少瀏覽器都限制一個站點最多保存20個cookie
通常建議:將登錄信息等重要信息存放爲session, 其餘信息若是須要保留,能夠放在cookie中
Token和Session的區別
Session是一種HTTP儲存機制, 爲無狀態的HTTP提供持久機制; Token就是令牌, 好比你受權(登陸)一個程序時,它就是個依據,判斷你是否已經受權該軟件;
Session和Token並不矛盾,做爲身份認證Token安全性比Session好,由於每個請求都有簽名還能防止監聽以及重放攻擊,而Session就必須依賴鏈路層來保障通信安全了。如上所說,若是你須要實現有狀態的回話,仍然能夠增長Session來在服務端保存一些狀態。
總結
cookie,session,Token沒有絕對的好與壞之分,只要仍是要結合實際的業務場景和需求來決定採用哪一種方式來管理回話,固然也能夠三種都用。
參考
-
jwt [2] -
完全理解cookie,session,token [3] -
JSON Web Token 入門教程 [4] -
Cookie、Session、Token那點事兒(原創) [5] -
3種web會話管理的方式 [6] -
你真的瞭解 Cookie 和 Session 嗎 [7] -
不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO [8] -
access_token、refresh_token的原理與實現 [9]
參考資料
源碼地址: https://github.com/funnycoderstar/demos/tree/master/JWT
[2]jwt: https://jwt.io/
[3]完全理解cookie,session,token: https://www.cnblogs.com/moyand/p/9047978.html
[4]JSON Web Token 入門教程: http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
[5]Cookie、Session、Token那點事兒(原創): https://www.jianshu.com/p/bd1be47a16c1
[6]3種web會話管理的方式: https://www.cnblogs.com/lyzg/p/6067766.html
[7]你真的瞭解 Cookie 和 Session 嗎: https://juejin.im/post/6844903842773991431
[8]不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO: https://zhuanlan.zhihu.com/p/38942172
[9]access_token、refresh_token的原理與實現: https://github.com/hehongwei44/my-blog/issues/298
本文分享自微信公衆號 - 牧碼的星星(gh_0d71d9e8b1c3)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。