Express + JWT用戶認證最輕實踐
最近給自己列了一個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); // 返回接口返回的錯誤信息
});
復制代碼
此處有坑,在此記錄request.headers['Authorization']必須通過此種形式設置Authorization,否則后端即使收到字段也會出現問題,返回401,request.headers.Authorization或request.headers.authorization可以設置成功,瀏覽器查看也沒有任何問題,但是在后端會報401並且后端一律只能拿到小寫的,也就是res.headers.authorization,后端用大寫獲取會報undefined.
可以看到,登錄成功后,token被存放在localStorage里並且每一次請求都會將token放在頭部Authorization字段內。如果我們把token從localStorage清除,再次訪問就會報錯。
總結
非常簡單的一個小栗子,也沒什么技術含量的文章,就當寫着玩練習文筆了。代碼沒有另外放在哪?就在express-react-scaffold上增加的登錄注冊和token認證。可以通過/login來訪問登陸部分邏輯以及token驗證功能。 O(∩_∩)O哈哈~