安全令牌JWT

JWT

前段時間處理一個抽獎H5,測試過程當中想到若是有用戶抓到抽獎接口,好比前端

https:xxx/lottery/userinfo
複製代碼

若是直接訪問抽獎接口,能夠直接進行抽獎動做。這裏就涉及處處理驗證用戶身份的問題ios

以後的解決方式是 判斷接口的cookie中是否包含 userInfo 等參數信息git

不過還能夠經過另一種方式來處理-- JWTgithub

什麼是JWT (JSON WEB TOKEN)

JWT是通訊雙方之間以 JSON對象的形式安全傳遞信息的方法。web

其實能夠理解爲使用非對稱算法來進行先後端校驗。redis

JWT 由三部分組成算法

頭部

head

  • typ 聲明類型數據庫

  • alg 聲明加密的算法npm

而後按照此規則將頭部信息進行base64編碼,構成JWT第一部分json

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
複製代碼

payload

payload 就是存放有效信息的地方

payload

payload 中有一些參數字段是建議使用的 (僅列出幾個)

參數 含義
iat jwt的簽發時間
exp jwt的過時時間,這個過時時間必需要大於簽發時間
nbf 定義在什麼時間以前,該jwt都是不可用的

好比來定義一個payload

{
  "exp": Math.floor(Date.now() / 1000) + (60 * 60),
  "name": "John Doe"
}
複製代碼

payload 會進行base64編碼,構成JWT第二部分

簽證

簽證

能夠看到,簽證部分是由三個部分組成的

參數 含義
base64UrlEncode base64加密後的Header
base64UrlEncode base64加密後的payload
your-256-bit-secret 自定義的加密secret

secret 至關於私鑰,不可泄漏,若是客戶端能夠拿到secret,就能夠自我簽發JWT了

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload)
var signature = HMACSHA256(encodedString, 'secret')
複製代碼

signature 是JWT的第三部分

將以上三部分拼接起來,就是最後的JWT

第三方庫

jsonwebtoken

若是本身在生成jwt,有點複雜。目前已經有不少開發的第三方庫來支持JWT。好比 jsonwebtoken

jsonwebtoken

  • sign 用於生成 token

  • verify 用於檢驗token

koa-jwt

koa-jwt 用於驗證接口中是否包含token信息

搭建了一個簡易的server 來看下效果

項目Git地址

app.use(
  jwtKoa({secret: SECRET})
  .unless({
    path: [/\/login/] // 不須要經過jwt驗證的請求路徑
  })
)
router.get('/login', async (ctx) => {
  let token = jwt.sign({
    name: 'dva'
  }, SECRET)
  console.log(token, 'token')
  ctx.body = {
    token
  }
})

router.get('/try', async (ctx) => {
  let token = ctx.header.authorization
  let result = jwt.verify(token, SECRET)
  ctx.body = {
    result
  }
})

複製代碼
  • /login 拿到token
// {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZHZhIiwiaWF0IjoxNTMxMjgwMDg2fQ.Rh_vAKeytjAL2TbOk-MmXQWFesszjRU3Bzldrx5x17s"}%
複製代碼
  • 若是不添加token 會被koa-jwt 攔截

wrong

  • 添加 token

token

工做機制

工做機制

圖片來自於文章 《先後端分離之JWT用戶認證》

  • 登陸拿到JWT

  • 前端發起請求,Header中掛載JWT

項目實戰

因爲我搭建的這個項目中有這種須要鑑權的接口比較少,因此並無使用koa-jwt來處理。只是用了 jsonwebtoken

項目GIT地址

server端

構建兩個接口 login , lottery

server

  • login 用來生成JWT 返回給前端
token = jwt.sign({
        name: 'who',
        exp: Math.floor(Date.now() / 1000) + (60 * 60), // 設置 token 過時時間
      }, SECRET)
複製代碼
  • lottery 用來驗證JWT,驗證經過則進行抽獎動做
let token = this.headers.authorization
      // 解碼
      let decoded = jwt.verify(token, SECRET)
      // console.log(decoded, 'decoded')
      let {name} = decoded

      if (name != 'who') {
        code = 403
        return
      }
複製代碼

前端

  • 首頁點擊登陸後 經返回的token信息存儲起來

login

我這裏拿到token以後將其寫入了localStorage

window.localStorage.setItem('token', token)
複製代碼
  • 進入抽獎頁面進行抽獎,每次請求的時候掛載Authorization

Authorization

axios.get(`/${APP_NAME}/win`, {
      headers: {
        Authorization: token
      }
    })
複製代碼

若是傳遞錯誤的token 在server端JWT驗證的時候就會報錯

error

error

項目在線地址

總而言之,若是你的接口須要考慮鑑權問題,能夠參考下JWT來處理。

Other

問題回覆

這篇文章發佈以後,不少同窗提出了一些問題,這裏一一回復。感謝各位的評論。

問題1: 關於base64處理 Header 和 payload

其實JWT在處理Header 和 payload 的時候,只是很簡單的進行了Base64編碼。 若是拿到某一個token的話,是很容易就將其解碼出來的。

const base64url = require('base64url')
let header = {
  'typ': 'JWT',
  'alg': 'HS256'
}

let resultH = base64url(JSON.stringify(header))
console.log(resultH, 'resultH')

let payload = {
  name: 'dva',
  exp: 1531410000
}

let result = base64url(JSON.stringify(payload))
console.log(result, 'result')

// 解碼payload
let isP = 'eyJuYW1lIjoiZHZhIiwiZXhwIjoxNTMxNDEwMDAwfQ'
let getP = base64url.decode(isP)
console.log(getP, 'getP')
複製代碼

因此不建議在payload中存放敏感信息,好比用戶手機號,地址信息等

問題2 JWT怎麼作續簽更新

查閱資料後總結,jwt的續簽更新目前有如下處理方式,基本的原理就是在某一個時間點,server端發放新的token

  • 每次客戶端發起新的請求過來,server自動更新token,返回最新的token信息給客戶端,客戶端拿到token後須要再更新token

好比 在用戶點擊抽獎,發起請求的時候,server端每次更新一個token(我這裏只更新jwt的有效期)

token

拿到新的token以後將其返回。下次發起抽獎動做的時候,掛載這個最新的token

可是這種處理方案有缺陷,若是用戶兩次請求的間隔時間超過了過時時間(好比20分鐘),則接口過來的時候 首先會被判斷爲過時狀態,請求終止(以後的代碼不被執行,不會被下發新的token了)。用戶會被強制退出到登陸界面。

  • 每次請求過來的時候,不去判斷有效期 (固然此請求自己攜帶的token必須在有效期內,個人意思是不像第一種,判斷距離過時還有多久) 直接下發新的token

問題3 處理註銷

能夠說token比較重要的問題就是註銷token。

好比我上面第二個jwt的項目,當用戶點擊退出登陸的時候,僅僅在客戶端作了token的刪除。

remove

可是實際上這個token仍是處於有效期內的。若是用戶保存了token值,在點擊了退出登陸以後,實際還可使用此token值的。能夠理解爲僞註銷。

傳統的方式怎麼處理用戶的註銷行爲呢?-- 刪除數據庫記錄。當用戶註銷登陸信息的時候,server會變動數據庫信息

可是jwt是沒有介入服務器來存儲用戶狀態的。這就比較難處理了。咱們但願token可以在用戶註銷後不能夠被繼續使用了

  • 設置比較短的token 有效期,每次請求過來的時候,從新下發,不斷更新token.

  • 使用服務器存儲token狀態。當用戶點擊註銷,將token置空。

問題4 JWT單點登陸(強制退出用戶登陸,好比修改密碼後,但願能讓其餘客戶端登錄的地方所有強制登出)

能夠理解爲如何讓一個token當即失效(有點像上面的問題3)

  • jwt + 數據庫(好比 redis)+ 白名單 (這種思路是公司同事提出來的,特別感謝~)

每一位用戶在設備A登陸的時候,將UID和token對應關係存放起來

myRedis.set(`${uid}:token`, ${tokenA})
複製代碼

用戶換設備B登陸的時候,將redis中的的token進行更新

myRedis.set(`${uid}:token`, ${tokenB})
複製代碼

每次請求發起的時候,server端去驗證該UID對應的token信息是不是最新的token, 這樣 若是攜帶的不是redis中的token的話,拒絕請求。前端強制退出登陸。

我將這個單點登陸的邏輯加入到了項目中。

登陸嘗試

你能夠在兩臺設備使用同一個用戶名進行登陸,嘗試是否是能夠將第一臺設備的狀態登出。

新增邏輯部分:將用戶{name, token} 對應關係存放在文件中,每次發送抽獎請求的時候,判斷文件最新的token與接口攜帶的token是否一致。不一致則反饋前端須要退出登陸

退出

缺點:須要保留每一位用戶的 {user, token} 對應關係

  • jwt + 數據庫(好比 redis)+ 黑名單

當用戶點擊退出登陸,此token則被放入黑名單(好比存放在redis)。若是有請求此時攜帶了黑名單中的token,則不予處理

缺點:久而久之黑名單數據量增加

問題5 如何防範Replay Attacks (重放攻擊)

重放攻擊就是攻擊者發送一個目的主機已接收過的包,來達到欺騙系統的目的,主要用於身份認證過程

好比用戶的token被獲取,那麼即便用戶登出了系統,其餘人還能夠利用Token模擬正常請求,而服務器端則沒法判斷這種狀況。

仍是黑名單思路,每次token更新以後,或者用戶登出以後,舊的token被放入黑名單。攜帶此token的請求一概不予處理

問題6 使用‘每一次發送請求就去更新token的方式’ 若是客戶端有併發的請求,如何處理

em,這個和上面的問題5有點矛盾,若是使用變化token的狀況處理,那麼確定會有當請求併發狀態下,第一個請求在處理完畢拿到新的token,後面的請求攜帶的token就變成了舊的token,請求會失敗

查閱資料後發現,有些人在將token存到黑名單的時候,會同時添加一個「寬限時間」 。當請求中攜帶了一個黑名單中的過時token,則去判斷去「寬限時間」,若是在期寬限之間以內,則予以經過。

不過我我的沒想明白,這種處理方式是否是有問題,既然已經被放入黑名單了,那爲何又來一個「寬限時間」。爲何不直接設置一個長一點的有效時間。

以上是對各位的一些回答。歡迎留言討論。

補充文章

相關文章
相關標籤/搜索