Node中cookie/session/token的使用

項目中是使用Koa的搭建服務(wù)器。
端口配置放在.env文件中,上傳時應(yīng)該忽略該文件,因為每個設(shè)備有自己的端口號。
這里使用dotenv來加載env文件。

登錄憑證

web開發(fā)中,使用最多的就是http協(xié)議,但http是一個無狀態(tài)的協(xié)議。也就是每一次http請求都是一個獨立的請求,并不知道之前的狀態(tài)。比如我們登錄成功之后,再去調(diào)用其他接口獲取信息時會發(fā)送一個新的請求,而這次的請求并不知道用戶是否已經(jīng)登錄過。

所以登錄成功之后服務(wù)器需要返回給客戶端一個登錄憑證,下次請求時拿著登錄憑證證明該用戶已經(jīng)登錄。

常見的登錄憑證:

  1. cookie+session

  2. Token令牌

Cookie

cookie類型為小型文本文件,某些網(wǎng)站為了辨別用戶身份而存儲在用戶本地終端的數(shù)據(jù)。瀏覽器會在特定情況下攜帶上cookie來發(fā)送網(wǎng)絡(luò)請求。

cookie總是抱存在客戶端,按照在客戶端的存儲位置可以分為內(nèi)存coolie和硬盤cookie

  • 內(nèi)存cookie由瀏覽器維護,保存在內(nèi)存中,瀏覽器關(guān)閉時cookie會消失,存在時間是短暫的

  • 硬盤cookie保存在硬盤中,有一個過期時間,手動清除或者到了過期時間才會消失

如何判斷一個cookie是內(nèi)存cookie還是硬盤cookie?

  • 沒有設(shè)置過期時間,默認是內(nèi)存cookie

  • 有設(shè)置過期時間,并且過期時間不為0或者負數(shù)的cookie是硬盤cookie,需要手動或者到期時才會刪除

cookie的生命周期

默認情況下cookie是內(nèi)存cookie,也稱之為會話cookie。可以設(shè)置expires或者max-age來設(shè)置過期時間:

  • expires:設(shè)置的是Date.toUTCString(),設(shè)置格式是:expires=date-in-GMTString-format

  • max-age:設(shè)置過期的時間:max-age=max-age-in-seconds(例如一年為60 * 60 * 24 * 365)

cookie的作用域

cookie的作用域也就是允許cookie發(fā)送給哪些URL

  • Domain:指定哪些主機可以接收cookie

    • 如果不指定,默認為origin,不包括子域名

    • 如果指定Domain,則包含子域名。比如,如果設(shè)置Domain=mozilla.org, 則cookie也包含在子域名中(比如develop.mozilla.org)

  • Path:指定主機下哪些路徑可以接收cookie

    • 比如設(shè)置Path=/docs,則/docs、/docs/web/、/docs/web/http都會匹配到
設(shè)置cookie

cookie可以由客戶端設(shè)置,也可以由服務(wù)端設(shè)置。一般都是由服務(wù)器設(shè)置cookie,客戶端來刪除cookie

谷歌瀏覽器查看cookie的方法:F12進入控制臺 => Application => Storage => Cookies

客戶端設(shè)置cookie

      // document.cookie= 'name=coderwy';
      // 5s之后刪除cookie
      document.cookie = 'age=18;max-age=5'

服務(wù)端設(shè)置cookie

testRouter.get('/test', (ctx, next) => {
  // 設(shè)置cookie
  ctx.cookies.set('name','Lucy',{
    maxAge:5 * 1000 // 單位是毫秒
  })
  ctx.body = "test";
})

服務(wù)端獲取cookie

// domain默認為origin 所以同域名下可以獲取到cookie
testRouter.get('/demo', (ctx, next) => {
  // 獲取cookie
  const value = ctx.cookies.get('name')
  ctx.body = '你的cookie是:' + value
})

瀏覽器在發(fā)送請求時,會自動將cookie添加到請求頭中


image.png

Session

session是基于cookie來實現(xiàn)的,在koa項目中可以借助koa-session來實現(xiàn)session認證

const session = Session({
  key:'sessionid',
  maxAge:10 * 1000,
  signed:false // 暫時設(shè)置為不需要簽名
}, app)
app.use(session)

// 登錄接口
testRouter.get('/test', (ctx, next) => {
  // 假設(shè)用戶通過name和password登錄 登錄成功之后從數(shù)據(jù)庫查詢
  // 得到id和name
  const id = 110
  const name = 'Lucy'
  // 設(shè)置session
  // 因為有app.use(session)操作 所有有session這個屬性
  ctx.session.user = {id, name}
  
  ctx.body = "test";
})
image.png

其中Value是我們設(shè)置的session.user的base64的編碼??梢钥吹絪ession本質(zhì)上還是一個cookie

獲取session:

// 獲取session
testRouter.get('/demo', (ctx, next) => {
  console.log(ctx.session.user); // { id: 110, name: 'lucy' }
  ctx.body = 'demo'
})

由于瀏覽器會自定在請求時帶上cookie,也就是上面的sessionid和其value,服務(wù)器這邊會對其進行解析,最終獲得登錄憑證(id: 110, name: 'lucy')

現(xiàn)在來看下使用簽名的情況:

const session = Session({
  key:'sessionid',
  maxAge:30 * 1000,
  signed:true // 使用加密簽名
}, app)
app.keys = ['aaaaa']
app.use(session)

此時設(shè)置的session內(nèi)容為:


image.png

在我們獲取session時能正常獲取,而當我們修改了sessionid對應(yīng)的value時,就不會獲取到對應(yīng)的信息


image.png

這里是隨便修改的value,但我們知道value是根據(jù)id和name的內(nèi)容轉(zhuǎn)成的base64,如果別人也通過其他的id來生成base64來修改value的話,服務(wù)端也是能獲取到對應(yīng)的登錄憑證的,所以需要進行加密簽名

此時獲取session時:

// 獲取session
testRouter.get('/demo', (ctx, next) => {
  console.log(ctx.session.user); 
  // 判斷session內(nèi)容
  // ...
  ctx.body = 'demo'
})

此時打?。?/p>

undefined

token

cookie和session的方式有很多缺點:

  • cookie會附加在每個http請求中,無形中增加了流量
  • cookie是明文傳輸?shù)?,所以存在安全性問題
  • cookie的大小限制為4kb,對于復(fù)雜的需求是不夠的
  • 對于瀏覽器其他的客戶端(比如iOS,Android)需要手動設(shè)置cookie
  • 對于分布式系統(tǒng)和服務(wù)器集群中如何可以保證其他系統(tǒng)也可以正確的解析session?

所以目前的前后端分離的開發(fā)過程中,經(jīng)常使用token進行身份驗證:

token也就是令牌,在驗證了用戶賬號密碼正確的情況下,給用戶頒發(fā)一個令牌,這個令牌作為用戶訪問其他接口或者資源的憑證。

JWT實現(xiàn)Token機制

JWT實現(xiàn)Token

JWT(Json Web Token)生成的Token由三部分組成:

  • header
    • alg:采用的加密算法。默認是HMAC SHA256(也就是HS256,一種對稱加密算法),采用同一個密鑰進行加密解密
    • typ:JWT。固定值,通常寫成JWT即可
    • 會通過base64Url算法對上面兩部分進行編碼,生成一串字符串(也就是header部分了);
  • payload
    • 攜帶的數(shù)據(jù),比如可以講用戶的id和name放到payload中
    • 默認會攜帶iat(issued at),令牌簽發(fā)時間
    • 也可以設(shè)置過期時間:exp(expiration time)
    • 通過base64Url算法對攜帶的數(shù)據(jù)進行編碼
  • signature:因為通過進行base64Url編碼的結(jié)果很容易被反編碼的。所以除了header和payload之外還需要簽名
    • 設(shè)置一個secretKey,然后將前兩個的結(jié)果和secretKey進行HS256算法的加密:
    • HS256(baseUrl(header) + . + baseUrl(payload), secretKey);
    • 所以secretKey暴露是一件非常危險的事情,因為之后就可以模擬頒發(fā)token,也可以解密token。
image.png
const Koa = require('koa')
const Router = require('koa-router')
const jwt = require('jsonwebtoken')

const app = new Koa()

const testRouter = new Router()

const SECRET_KEY = 'secretkey'

// 登錄接口
testRouter.post('/test', (ctx, next) => {
  const payload = {id:119, name:'lwy'}
  // 生成token
  const token = jwt.sign(payload,SECRET_KEY,{
    expiresIn:10 
  })
  // 返回token
  ctx.body = token
})

app.use(testRouter.routes())
app.use(testRouter.allowedMethods())

app.listen(8080, () => {
  console.log('服務(wù)器啟動成功');
})

使用postman進行請求可以看到返回的token為:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTE5LCJuYW1lIjoibHd5IiwiaWF0IjoxNjA2NzI0MTg1LCJleHAiOjE2MDY3MjQxOTV9.D8q8-8zIZ5CamXi-sSZdWnPoN_yQ-A4Y8zzIYJW77yQ

可以看到,其中是以 . 分隔成三部分的,對應(yīng)的就是上面說的token的組成

接下來就可以通過postman來模擬把token傳遞給服務(wù)端。一種方式是把token放到body里,不過很少這樣使用,一般都是將token放到header中。


image.png

可以看到有很多的認證方式,常用的是Bearer(送信人) Token方式

獲取token:

testRouter.get('/demo', (ctx, next) => {
  // 獲取token
  console.log(ctx.headers.authorization);
})

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTE5LCJuYW1lIjoibHd5IiwiaWF0IjoxNjA2NzI0MTg1LCJleHAiOjE2MDY3MjQxOTV9.D8q8-8zIZ5CamXi-sSZdWnPoN_yQ-A4Y8zzIYJW77yQ

接下來可以獲取到token:

testRouter.get('/demo', (ctx, next) => {
  // 獲取token
  console.log(ctx.headers.authorization);
  const auth = ctx.header.authorization
  const token = auth.replace('Bearer ', '')
  try {
    // 驗證token verify驗證失敗之后會拋出異常 
    const result = jwt.verify(token, SECRET_KEY)
    console.log(result);
    ctx.body = result
  } catch (error) {
    console.log('驗證失?。?, error);
    ctx.body = 'token 無效'
  }
})

token驗證成功之后的返回:

{

? "id": 119,

? "name": "lwy",

? "iat": 1606874354,

? "exp": 1606884354

}

非對稱加密

前面用到的HS256加密算法是一種對稱加密(維基百科),一旦密鑰暴露是一種很危險的事情。比如在分布式系統(tǒng)中,每一個子系統(tǒng)都需要獲取到密鑰,那么每個子系統(tǒng)既可以發(fā)布令牌,也可以驗證令牌,但對一些資源服務(wù)器來說,只需要有驗證令牌的功能即可。

這個時候就可以使用非對稱加密(維基百科):

  • 私鑰(private key):用于發(fā)布令牌

  • 公鑰(public key):用于驗證令牌

通常我們使用公鑰加密,用私鑰解密。而在數(shù)字簽名中,我們使用私鑰加密(相當于生成簽名),公鑰解密(相當于驗證簽名)

我們可以使用openssl來生成私鑰,然后根據(jù)私鑰生成對應(yīng)的公鑰。mac中自帶的有openssl工具。

比如生成一個密鑰到指定的目錄中:

  1. cd 指定目錄
  2. openssl:進入到ssl的命令行交互頁面`
  3. genrsa -out private.key 1024: 生成私鑰
    1. genrsa: gen是generate的縮寫,rsa是常用的非對稱加密算法
    2. -out:代表我們要導(dǎo)出
    3. private.key: 導(dǎo)出的文件名(代表我們生成的是私鑰)
    4. 1024:生成的私鑰的長度,也可以搞成其他長度
image.png

接下來還要生成用于驗證簽名的公鑰:

rsa -in private.key -pubout -out public.key

  1. -in private.key:表示將剛剛生成的密鑰作為輸入
  2. -pubout:表示這次生成的是公鑰
  3. -out:表示我們要導(dǎo)出
  4. public.key:導(dǎo)出的文件名
image.png

使用非對稱加密生成token:

const PRIVATE_KEY = fs.readFileSync('./keys/private.key')
const PUBLIC_KEY = fs.readFileSync('./keys/public.key')

// 使用非對稱加密
testRouter.post('/test', (ctx, next) => {
  const payload = {id:110, name:'lwy'}
  // 私鑰用來生成簽名
  const token = jwt.sign(payload, PRIVATE_KEY,{
    expiresIn:10 * 1000,
    algorithm:"RS256" // 指明用到的算法 
  })
  ctx.body = token
})
testRouter.get('/demo', (ctx, next) => {
  const auth = ctx.headers.authorization
  const token = auth.replace('Bearer ', '')
  try {
    const res = jwt.verify(token, PUBLIC_KEY,{
      algorithms:["RS256"]  
    })
    ctx.body = res 
  } catch (error) {
    console.log('token驗證錯誤:', error);
    ctx.body = 'token失效'
  }
})

ps: 項目中相對路徑是相對于process.cwd()的,也就是當前項目啟動的目錄

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容