前段時間處理一個抽獎H5,測試過程中想到如果有使用者抓到抽獎介面,比如
https:xxx/lottery/userinfo
複製程式碼
如果直接訪問抽獎介面,可以直接進行抽獎動作。這裡就涉及到處理驗證使用者身份的問題
之後的解決方式是 判斷介面的cookie中是否包含 userInfo 等引數資訊
不過還可以通過另外一種方式來處理-- JWT
什麼是JWT (JSON WEB TOKEN)
JWT是通訊雙方之間以 JSON物件的形式安全傳遞資訊的方法。
其實可以理解為使用非對稱演算法來進行前後端校驗。
JWT 由三部分組成
頭部
-
typ 宣告型別
-
alg 宣告加密的演算法
然後按照此規則將頭部資訊進行base64編碼,構成JWT第一部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
複製程式碼
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
-
sign 用於生成 token
-
verify 用於檢驗token
koa-jwt
koa-jwt 用於驗證介面中是否包含token資訊
搭建了一個簡易的server 來看下效果
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 攔截
- 新增 token
工作機制
圖片來自於文章 《前後端分離之JWT使用者認證》
-
登入拿到JWT
-
前端發起請求,Header中掛載JWT
專案實戰
由於我搭建的這個專案中有這種需要鑑權的介面比較少,所以並沒有使用koa-jwt來處理。只是用了 jsonwebtoken
server端
構建兩個介面 login , lottery
- 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資訊儲存起來
我這裡拿到token之後將其寫入了localStorage
window.localStorage.setItem('token', token)
複製程式碼
- 進入抽獎頁面進行抽獎,每次請求的時候掛載Authorization
axios.get(`/${APP_NAME}/win`, {
headers: {
Authorization: token
}
})
複製程式碼
如果傳遞錯誤的token 在server端JWT驗證的時候就會報錯
總而言之,如果你的介面需要考慮鑑權問題,可以參考下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
但是這種處理方案有缺陷,如果使用者兩次請求的間隔時間超過了過期時間(比如20分鐘),則介面過來的時候 首先會被判斷為過期狀態,請求終止(之後的程式碼不被執行,不會被下發新的token了)。使用者會被強制退出到登入介面。
- 每次請求過來的時候,不去判斷有效期 (當然此請求本身攜帶的token必須在有效期內,我的意思是不像第一種,判斷距離過期還有多久) 直接下發新的token
問題3 處理登出
可以說token比較重要的問題就是登出token。
比如我上面第二個jwt的專案,當使用者點選退出登入的時候,僅僅在客戶端做了token的刪除。
但是實際上這個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,則去判斷去“寬限時間”,如果在期寬限之間之內,則予以通過。
不過我個人沒想明白,這種處理方式是不是有問題,既然已經被放入黑名單了,那為什麼又來一個“寬限時間”。為什麼不直接設定一個長一點的有效時間。
以上是對各位的一些回答。歡迎留言討論。