Cookie
- HTTP協議是
無狀態
的,但在WEB應用中,在多個請求之間共享會話是非常必要的,所以出現了Cookie - cookie是為了
辯別使用者身份
,進行會話跟蹤而儲存在客戶端
上的資料
伺服器設定cookie:客戶端第一次訪問伺服器時,會通過響應頭向客戶端傳送Cookie,屬性之間用分號空格
分隔
客戶端接收並儲存cookie:客戶端再請求伺服器時,會攜帶Cookie至伺服器端,而cookie本身就是一個請求的header
重要屬性
通過修改本地hosts檔案,模擬兩個不同的域名。
# hosts
127.0.0.1 a.echoyya.com
127.0.0.1 b.echoyya.com
屬性 | 說明 |
---|---|
name=value |
鍵值對,可以設定要儲存的 Key/Value |
Domain |
針對某個域名生效 可以跨父域和子域,預設是當前域名 |
expires/max-age |
cookie存活時間 ,expires 絕對時間, max-age 相對時間 單位秒 |
secure | 當 secure 值為 true 時,cookie 在 HTTP 中是無效,只在https下生效 |
Path |
表示 cookie 影響到的路徑,預設是/ 都能被訪問到。若路徑不匹配時,瀏覽器則不傳送這個Cookie |
httpOnly |
表示瀏覽器無法通過程式碼來獲取,防止XSS攻擊,但是可以通過手動修改控制檯方式進行更改。 |
實現原理
npm install koa koa-router
const Koa = require('koa');
const Router = require('koa-router');
const querystring = require('querystring') // 用於解析和格式化網址查詢字串
const app = new Koa();
const router = new Router();
// koa 操作cookie 實現原理
app.use(async (ctx, next) => {
// 擴充套件一個設定cookie的方法
let cookieArr = [];
ctx.req.getCookie = function (key) {
let cookies = ctx.req.headers['cookie']; // name=xx; age=yy => name=xx&age=yy
let cookieObj = querystring.parse(cookies,'; ')
return cookieObj[key] || ''
}
ctx.res.setCookie = function (key, value, options={}) {
let args = []; // 每個cookie 屬性集合
options.domain && args.push(`domain=${options.domain}`);
options.maxAge && args.push(`max-age=${options.maxAge}`);
options.httpOnly && args.push(`httpOnly=${options.httpOnly}`);
options.path && args.push(`path=${options.path}`);
cookieArr.push(`${key}=${value}; ${args.join('; ')}`);
ctx.res.setHeader('Set-Cookie', cookieArr); // 字串陣列
}
await next();
})
router.get('/read', async (ctx, next) => {
// 自己封裝
ctx.body = ctx.req.getCookie('name') || 'empty';
// koa 實現
// ctx.body = ctx.cookies.get('name') || 'empty';
// 原生用法
// ctx.body = ctx.req.headers['cookie'] || 'empty'; // 請求頭
})
router.get('/write', async (ctx, next) => {
// 自己封裝
ctx.res.setCookie('name', 'nn', {domain: '.echoyya.com'}); // 限制可訪問的域名
ctx.res.setCookie('age', '12', {httpOnly: true,path:'/write'}); // 限制可訪問的路徑
// koa 實現
// ctx.cookies.set('name', 'nn', {domain: '.echoyya.com'});
// ctx.cookies.set('age', '12', {httpOnly: true,path:'/write'});
// 原生用法
// ctx.res.setHeader('Set-Cookie','name=yy');
// ctx.res.setHeader('Set-Cookie','age=15'); // 設定一個cookie,再次set cookie 會將上一次的覆蓋
// ctx.res.setHeader('Set-Cookie', ['name=yy; domain=.echoyya.com', 'age=15; path=/; max-age=10; httpOnly=true']); // 設定多個cookie時,可接受一個字串陣列
ctx.body = 'write ok';
})
app.use(router.routes())
app.listen(4000);
cookie簽名實現原理
cookie通常由伺服器產生,存在客戶端,隨著每次請求傳送至伺服器端,而前端儲存資料可以被使用者手動篡改,
因此可以給cookie簽名使其相對安全, 根據cookie的內容產生一個標識,保留原有內容,每次請求檢驗簽名,新增一個配置{ signed: true }
const Koa = require('koa');
const Router = require('koa-router');
const querystring = require('querystring');
const crypto = require('crypto');
const app = new Koa();
const router = new Router();
app.keys = ['ya'];
// base64Url 需要特殊處理 + = /
const sign = value => crypto.createHmac('sha1',app.keys.join('')).update(value).digest('base64').replace(/\+/g,'-').replace(/\=/g,'').replace(/\//g,'_');
app.use(async (ctx, next) => {
let cookieArr = [];
ctx.req.getCookie = function (key, options = {}) {
let cookies = ctx.req.headers['cookie'];
let cookieObj = querystring.parse(cookies,'; ')
if(options.signed){
// 傳遞過來的簽名,和最新計算獲得的結果一直,則說明未被修改
if(cookieObj[key + '.sig'] === sign(`${key}=${cookieObj[key]}`)){
return cookieObj[key];
}else {
return ''
}
}
return cookieObj[key] || ''
}
ctx.res.setCookie = function (key, value, options = {}) {
let args = [];
let keyValue = `${key}=${value}`
options.domain && args.push(`domain=${options.domain}`);
options.maxAge && args.push(`max-age=${options.maxAge}`);
options.httpOnly && args.push(`httpOnly=${options.httpOnly}`);
options.path && args.push(`path=${options.path}`);
options.signed && cookieArr.push(`${key}.sig=${sign(keyValue)}`); // 是否開啟cookie簽名
cookieArr.push(`${keyValue}; ${args.join('; ')}`);
ctx.res.setHeader('Set-Cookie', cookieArr); // 字串陣列
}
await next();
})
// app.keys required for signed cookies
router.get('/visit', async (ctx, next) => {
// Koa 實現
// let count = ctx.cookies.get('visit',{ signed: true }) || 0;
// let visitCount = Number(count) + 1;
// ctx.cookies.set('visit', visitCount, { signed: true });
// ctx.body = `you visit ${visitCount}`
// 自己封裝
let count = ctx.req.getCookie('visit', { signed: true }) || 0;
let visitCount = Number(count) + 1;
ctx.res.setCookie('visit', visitCount, { signed: true });
ctx.body = `ya visit: ${visitCount}`
})
app.use(router.routes())
app.listen(3000);
注意事項
- 可能被客戶端篡改,
使用前驗證簽名的合法性
, 不要儲存敏感資料
,比如使用者密碼,賬戶餘額- 每次
請求都會自動攜帶cookie
,儘量減少cookie的體積
設定正確的domain和path
,減少資料傳輸
Session
是另一種記錄客戶狀態的機制,不同的是Cookie儲存在客戶端
瀏覽器中,而session儲存在伺服器
上
在伺服器儲存使用者對應的資訊,伺服器可以儲存敏感資訊,而session本身是基於cookie的且比cookie安全
同時session 沒有持久化功能,需要配合資料庫或者redis使用
實現原理
npm install uuid
const Koa = require('koa');
const Router = require('koa-router');
const uuid = require('uuid');
const app = new Koa();
const router = new Router();
app.keys = ['ya']
const session = {}; // 用來儲存使用者和資訊的對映關係,對瀏覽器不可見
const cardName = 'connect_sig';
router.get('/cut', async (ctx, next) => {
let id = ctx.cookies.get(cardName, {signed:true});
if(id && session[id]){
session[id].mny -= 20;
ctx.body = `mny:` + session[id].mny;
}else{
let cardId = uuid.v4();
session[cardId] = { mny: 500 };
ctx.cookies.set(cardName, cardId,{httpOnly:true,signed:true}); // cookie中只存一個標識,並沒有真實的資料
ctx.body = `mny 500`;
}
})
app.use(router.routes())
app.listen(3000);
JWT
JSON Web Token(JWT)
是目前最流行的跨域身份驗證解決方案,JWT 預設是不加密的,任何人都可以讀到,所以不要把重要資訊放在這個部分。
解決問題:session不支援分散式架構,無法支援橫向擴充套件,只能通過資料庫來儲存會話資料實現共享。如果持久層失敗會出現認證失敗。
優點:伺服器不儲存任何會話資料,即伺服器變為無狀態,使其更容易擴充套件。
使用方式
- HTTP 請求的頭資訊Authorization欄位裡面
Authorization: Bearer <token>
- 如果是post請求也可以放在請求體中,取決於後端採用哪種認證方式
- 通過url傳輸
http://www.xxx.com/pwa?token=xxxxx
,但是一般不建議這樣使用,因為會存在連線分享導致安全隱患
組成
JWT包含了使用.分隔
的三部分 Header.Payload.Signature
1. Header 頭部
{ "alg": "HS256", "typ": "JWT"}
// algorithm => HMAC SHA256
// type => JWT
2. Payload內容 JWT 規定了7個官方欄位
iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號
3. Signature 簽名
對前兩部分的簽名,防止資料篡改 HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
實際應用
npm install koa-bodyparser jwt-simple
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const jwt = require('jwt-simple');
const app = new Koa();
const router = new Router();
app.use(bodyParser())
// 登陸
router.post('/login', async (ctx, next) => {
let { username, password } = ctx.request.body;
if (username == 'admin' && password == 'admin') {
// let token = jwt.encode(username,'ya'); // jwt-simple 實現
let token = myJwt.encode(username,'ya'); // 自己實現
ctx.body = {
code: 200,
data: {
token,
username
}
}
}
})
// 驗證是否有許可權
router.get('/validate', async (ctx, next) => {
let authorization = ctx.get('authorization');
if(authorization){
let [,token] = authorization.split(' ');
try{
// let r = jwt.decode(token,'ya'); // jwt-simple 實現
let r = myJwt.decode(token,'ya'); // 自己實現
ctx.body = {
code:200,
data:{
username:r
}
}
}catch{
ctx.body = {
code:401,
data:'token已失效'
}
}
}
})
app.use(router.routes())
app.listen(3000);
實現原理
// token組成部分為為三段,1,固定格式表示型別 2,內容 3 簽名
// 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ImFkbWluIg.xJ0xCP2SXSaJSC-Q1PXuByHdJlBUHCNjdGRU4XW0abU'
const myJwt = {
sign(str,secret){
str = require('crypto').createHmac('sha256',secret).update(str).digest('base64');
return this.toBase64Escape(str);
},
toBase64(content){ // 物件轉base64 需要先轉為buffer => base64
let source = typeof content === 'string' ? content : JSON.stringify(content);
return this.toBase64Escape(Buffer.from(source).toString('base64'));
},
toBase64Escape(base64){
return base64.replace(/\+/g,'-').replace(/\//g,'_').replace(/\=/g,'');
},
encode(username, secret){ // 轉為base64並不是為了安全,只是為了可以在網路中傳輸
let header = this.toBase64({typ:'JWT',alg:'HS256'});
let content = this.toBase64(username);
let sign = this.sign([header, content].join('.'),secret)
return header + '.' + content + '.' + sign
},
base64urlUnescape(str){
str += new Array(5 - str.length % 4).join('=');
return str.replace(/\-/g, '+').replace(/_/g, '/');
},
// 相同的內容生成的簽名相同,可以新增一些過期時間等資訊
// 通過內容生成了一個簽名,反之通過校驗簽名。即可得知內容是否發生改變
decode(token,secret){
let [header, content, sign] = token.split('.');
let newSign = this.sign([header, content].join('.'),secret);
if(sign === newSign){ // 此時內容line2中的資料一定是可靠的
return Buffer.from(this.base64urlUnescape(content),'base64').toString();
}else{
throw new Error('token已失效')
}
}
}
前端儲存方式 cookie session localStorage sessionStorage token 區別
- cookie特點可以每次請求的時候自動攜帶,可以實現使用者登入功能. 使用cookie來識別使用者,1.如果單純的使用cookie,不建議存放敏感資訊,如果被劫持到。(cookie是存在客戶端,並不安全,使用者可以自行篡改)2.每個瀏覽器一般對請求頭都有大小限制 cookie 不能大於4k,如果cookie過大,會導致頁面白屏。 每次訪問伺服器都會浪費流量(合理設定cookie);
- session:在伺服器儲存使用者對應的資訊,伺服器可以儲存敏感資訊,而session本身是基於cookie的且比cookie安全;
- localStorage:關掉瀏覽器資料依然存在,除非手動清楚,有大小限制約5M,傳送請求不會攜帶;
- sessionStorage:頁面不關閉就不會銷燬 (用途:如單頁應用訪問時儲存滾動條地址)
- token -> jwt -> jsonwebtoken 不需要伺服器儲存,沒有跨域限制,不建議儲存敏感資訊