安全令牌JWT

行走的柯南發表於2018-07-11

JWT

前段時間處理一個抽獎H5,測試過程中想到如果有使用者抓到抽獎介面,比如

https:xxx/lottery/userinfo
複製程式碼

如果直接訪問抽獎介面,可以直接進行抽獎動作。這裡就涉及到處理驗證使用者身份的問題

之後的解決方式是 判斷介面的cookie中是否包含 userInfo 等引數資訊

不過還可以通過另外一種方式來處理-- JWT

什麼是JWT (JSON WEB TOKEN)

JWT是通訊雙方之間以 JSON物件的形式安全傳遞資訊的方法。

其實可以理解為使用非對稱演算法來進行前後端校驗。

JWT 由三部分組成

頭部

head

  • typ 宣告型別

  • alg 宣告加密的演算法

然後按照此規則將頭部資訊進行base64編碼,構成JWT第一部分

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,則去判斷去“寬限時間”,如果在期寬限之間之內,則予以通過。

不過我個人沒想明白,這種處理方式是不是有問題,既然已經被放入黑名單了,那為什麼又來一個“寬限時間”。為什麼不直接設定一個長一點的有效時間。

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

補充文章

相關文章