最近給自己列了一個list,Ummm...列來列去大概是下面這個樣子:
- React SSR服務端渲染
- jwt使用者認證
- Vue全家桶
- 微信小程式開發
- ... 等等
好吧,誰讓自己菜呢,沒什麼好抱怨的,一個一個來吧。正好最近看了一些token做身份認證的文章,發現其中大部分都是說token登入怎麼怎麼好,反正沒有幾個認認真真的實現的。。。正好,秉著我是小白我怕誰的原則,繼續分享一下express + jwt的填坑經歷。為什麼題目起名是最輕實踐呢?因為確實看完這個你可以大概理解token登入的好處以及如何簡單的實現一個前後端通過token進行認證的小系統。這個demo是在我第一篇文章那個腳手架上跑起來的,感興趣的還可以回顧一下----->express-react-scaffold。具體實現就是下面這個樣子:
- 不用token驗證的頁面正常瀏覽
- 需要驗證的頁面進行token驗證
- 沒有token資訊或token資訊過期,提示使用者重新登入,跳轉到登入頁面
- 登入成功之後每次請求攜帶token資訊
這篇文章包括
- 為什麼要用token做身份驗證(另一種模式是session)
- 前端http請求攔截器的設定
- 後端express + jsonwebtoken實現基於token的使用者身份驗證
token是個啥子東西
身份認證的兩種方式
在前後端分離的系統中,身份認證是十分重要的,目前常用的兩種身份認證方式如下:
- 基於cookie
基於cookie的服務端認證,就是我們所熟知session,在服務端生成使用者相關的 session 資料,而發給客戶端 sesssion_id 存放到 cookie 中,這樣用客戶端請求時帶上 session_id 就可以驗證伺服器端是否存在 session 資料,以此完成使用者認證。 - 基於Token令牌
基於 token 的使用者認證是一種服務端無狀態的認證方式,服務端不用存放 token 資料。使用者驗證後,服務端生成一個 token(hash 或 encrypt)發給客戶端,客戶端可以放到 cookie 或 localStorage(sessionStorage) 中,每次請求時在 Header 中帶上 token ,服務端收到 token 通過驗證後即可確認使用者身份。
token認證的好處
- 體積小(一串字串),因而傳輸速度快
- 傳輸方式多樣,可以通過HTTP 頭部(推薦)、 URL、POST 引數等方式傳輸嚴謹的結構化。它自身(在 payload 中)就包含了所有與使用者相關的驗證訊息,如使用者可訪問路由、訪問有效期等資訊,伺服器無需再去連線資料庫驗證資訊的有效性,並且 payload 支援為應用定製化支援跨域驗證,多應用於單點登入 充分依賴無狀態 API ,契合 RESTful 設計原則(無狀態的 HTTP)
- 使用者登入之後,伺服器會返回一串 token 並儲存在本地也就是客戶端,在這之後的對伺服器的訪問都要帶上這串 token,來獲得訪問伺服器相關路由、服務及資源的許可權。 易於實現 CDN,將靜態資源分散式管理
- 在傳統的 session 驗證中,服務端必須儲存 session ID,用於與使用者傳過來的 cookie 驗證。而一開始 sessionID 只會儲存在一臺伺服器上,所以只能由一臺 server 應答,就算其他伺服器有空閒也無法應答,無法充分利用到分散式伺服器的優點。 JWT 依賴的是在客戶端本地儲存驗證資訊,不需要利用伺服器儲存的資訊來驗證,所以任意一臺伺服器都可以應答,伺服器的資源也被較好地利用。
- 對原生的移動端應用支援較好 原生的移動應用對 cookie 與 session 的支援不夠好,而對 token 的方式支援較好。
JWT的組成
JWT的本質實際上就是一個字串,它有三部分組成頭部+載荷+簽名。
// Header
{
"alg": "HS256",//所使用的簽名演算法
"typ": "JWT"
}
// Payload
{
//該JWT的簽發者
"iss": "luffy",
// 這個JWT是什麼時候簽發的
"iat":1441593502,
//什麼時候過期,這是一個時間戳
"exp": 1441594722,
// 接收JWT的一方
"aud":"www.youdao.com",
// JWT所面向的使用者
"sub":"any@126.com",
// 上面是JWT標準定義的一些欄位,除此之外還可以私人定義一些欄位
"form_user": "fsdfds"
}
// Signature 簽名
將上面兩個物件進行base64編碼之後用.進行連線,然後通過HS256演算法進行加密就形成了簽名,一般需要加上我們提供的一個密匙,例如secretKey:'name_luffy'
const base64url = require('base64url')
const base64header = base64url(JSON.stringify(header));
const base64payload = base64url(JSON.stringify(payload));
const secretKey = 'name_luffy';
const signature = HS256(`${base64header}.${base64payload}`,secretKey);
// JWT
// 最後就形成了我們所需要的JWT:
const JWT = base64header + "." + base64payload + "." + signature;
// 它長下面這個樣子:
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
複製程式碼
JWT的工作原理
我從官網JWT.io拿下來的圖來展示,就是下面這個過程,說的很詳細,此外還有一些細節的東西,比如什麼形式儲存,放在頭部哪裡,客戶端要儲存在哪裡等,官網都有比較詳細的介紹,大家可以去看看。
前後端如何用這個東西做身份認證
思路
接下來要詳細的說如何使用jwt來進行前後端的身份驗證了,具體思路如下:
- 使用者登入註冊的邏輯不需要身份驗證,因為沒有使用者的身份資訊和登入狀態;
- 使用者登入之後後端生成token並返給前端,前端拿到token之後將token快取在本地,可以使localStorage也可以是cookie,以便接下來使用。。
- 其他內容涉及到前後端互動的都需要前端把認證的token資訊放在請求頭部傳給後端
- 後端收到請求先校驗token,如果token合法(也就是token正確且沒過期),則執行next(),否則直接返回401以及對應的message。
token登入的具體實現細節
- 後端:express-jwt + jsonwebtoken 首先,安裝兩個包
yarn add express-jwt jsonwebtoken
複製程式碼
之後就是在登入環節生成token並且把token返回給前端
// /routes/user.js
if (user !== null) {
// 使用者登入成功過後生成token返給前端
let token = jwt.sign(tokenObj, secretKey, {
expiresIn : 60 * 60 * 24 // 授權時效24小時
});
res.json({
success: true,
message: 'success',
token: token
});
}
複製程式碼
其次,設定攔截token的中介軟體,包括token的驗證以及錯誤資訊的返回:
// jwt.js,token中介軟體
const expressJwt = require("express-jwt");
const { secretKey } = require('../constant/constant');
// express-jwt中介軟體幫我們自動做了token的驗證以及錯誤處理,所以一般情況下我們按照格式書寫就沒問題,其中unless放的就是你想要不檢驗token的api。
const jwtAuth = expressJwt({secret: secretKey}).unless({path: ["/api/user/login", "/api/user/register"]});
module.exports = jwtAuth;
複製程式碼
// constant.js
// 設定了密碼鹽值以及token的secretKey
const crypto = require('crypto');
module.exports = {
MD5_SUFFIX: 'luffyZhou我是一個固定長度的鹽值',
md5: (pwd) => {
let md5 = crypto.createHash('md5');
return md5.update(pwd).digest('hex');
},
secretKey: 'luffy_1993711_26_jwttoken'
};
複製程式碼
最後在路由中介軟體前面放上jwt中介軟體
// routes/index.js
// 所有請求過來都會進行身份驗證
router.use(jwtAuth);
// 路由中介軟體
router.use((req, res, next) => {
// 任何路由資訊都會執行這裡面的語句
console.log('this is a api request!');
// 把它交給下一個中介軟體,注意中介軟體的註冊順序是按序執行
next();
});
複製程式碼
後端邏輯部分全部完成,下面是前端的實現部分。
- 前端: axios攔截器 + localStorage儲存token 前端主要做的就是兩件事:
第一、把登陸成功之後返回的token存在客戶端,可以使用localStorage也可以使用cookie,我看官方推薦使用localStorage,我這邊也就用localStorage吧。 第二、每次請求把token放到header頭部Authorization欄位。
// axios攔截器
// 攔截請求,給所有的請求都帶上token
axios.interceptors.request.use(request => {
const luffy_jwt_token = window.localStorage.getItem('luffy_jwt_token');
if (luffy_jwt_token) {
// 此處有坑,下方記錄
request.headers['Authorization'] =`Bearer ${luffy_jwt_token}`;
}
return request;
});
// 攔截響應,遇到token不合法則報錯
axios.interceptors.response.use(
response => {
if (response.data.token) {
console.log('token:', response.data.token);
window.localStorage.setItem('luffy_jwt_token', response.data.token);
}
return response;
},
error => {
const errRes = error.response;
if (errRes.status === 401) {
window.localStorage.removeItem('luffy_jwt_token');
swal('Auth Error!', `${errRes.data.error.message}, please login!`, 'error')
.then(() => {
history.push('/login');
});
}
return Promise.reject(error.message); // 返回介面返回的錯誤資訊
});
複製程式碼
可以看到,登入成功後,token被存放在localStorage裡並且每一次請求都會將token放在頭部Authorization欄位內。如果我們把token從localStorage清除,再次訪問就會報錯。此處有坑,在此記錄request.headers['Authorization']必須通過此種形式設定Authorization,否則後端即使收到欄位也會出現問題,返回401,request.headers.Authorization或request.headers.authorization可以設定成功,瀏覽器檢視也沒有任何問題,但是在後端會報401並且後端一律只能拿到小寫的,也就是res.headers.authorization,後端用大寫獲取會報undefined.
總結
非常簡單的一個小栗子,也沒什麼技術含量的文章,就當寫著玩練習文筆了。程式碼沒有另外放在哪?就在express-react-scaffold上增加的登入註冊和token認證。可以通過/login來訪問登陸部分邏輯以及token驗證功能。 O(∩_∩)O哈哈~