Express + JWT使用者認證最輕實踐

luffyZh發表於2018-05-28

最近給自己列了一個list,Ummm...列來列去大概是下面這個樣子:

  • React SSR服務端渲染
  • jwt使用者認證
  • Vue全家桶
  • 微信小程式開發
  • ... 等等

好吧,誰讓自己菜呢,沒什麼好抱怨的,一個一個來吧。正好最近看了一些token做身份認證的文章,發現其中大部分都是說token登入怎麼怎麼好,反正沒有幾個認認真真的實現的。。。正好,秉著我是小白我怕誰的原則,繼續分享一下express + jwt的填坑經歷。為什麼題目起名是最輕實踐呢?因為確實看完這個你可以大概理解token登入的好處以及如何簡單的實現一個前後端通過token進行認證的小系統。這個demo是在我第一篇文章那個腳手架上跑起來的,感興趣的還可以回顧一下----->express-react-scaffold。具體實現就是下面這個樣子:

  • 不用token驗證的頁面正常瀏覽
  • 需要驗證的頁面進行token驗證
  • 沒有token資訊或token資訊過期,提示使用者重新登入,跳轉到登入頁面
  • 登入成功之後每次請求攜帶token資訊
    Express + JWT使用者認證最輕實踐

這篇文章包括

  • 為什麼要用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拿下來的圖來展示,就是下面這個過程,說的很詳細,此外還有一些細節的東西,比如什麼形式儲存,放在頭部哪裡,客戶端要儲存在哪裡等,官網都有比較詳細的介紹,大家可以去看看。

Express + JWT使用者認證最輕實踐

前後端如何用這個東西做身份認證

思路

接下來要詳細的說如何使用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.

Express + JWT使用者認證最輕實踐
Express + JWT使用者認證最輕實踐
可以看到,登入成功後,token被存放在localStorage裡並且每一次請求都會將token放在頭部Authorization欄位內。如果我們把token從localStorage清除,再次訪問就會報錯。

Express + JWT使用者認證最輕實踐

Express + JWT使用者認證最輕實踐

總結

非常簡單的一個小栗子,也沒什麼技術含量的文章,就當寫著玩練習文筆了。程式碼沒有另外放在哪?就在express-react-scaffold上增加的登入註冊和token認證。可以通過/login來訪問登陸部分邏輯以及token驗證功能。 O(∩_∩)O哈哈~

相關文章