前端node Session和JWT鑒權登錄示例詳解
服務端渲染及session鑒權
服務端渲染
服務端渲染簡單來說就是前端頁面是由服務器通過字符串拼接動態生成的,客戶端不需要額外通過Ajax請求參數,隻需要做好渲染工作即可。
優點
- 前端耗時少,前端隻需要請求一次接口就能將數據渲染出來,首屏加載速度變快。
- 利於SEO,因為服務器端相應的是完整的html頁面內容,利於爬蟲獲取信息。
缺點
- 占用服務器資源,請求過多會造成訪問壓力。
- 不利於前後端分類,並且前端復雜度高時不利於開發。
服務端身份驗證Session原理
對於服務端渲染,推薦使用Session認證機制,再次之前,先說明一下cookie
比如你可以在baidu.com看到以下cookie:
session的鑒權就是利用瞭cookie,用戶調用登錄接口,完成賬號密碼的校驗之後,將用戶信息或者其他校驗信息生成為cookie字符串,返回給用戶,同時將cookie存儲在服務器內存,用戶請求其他接口時,會在請求頭自動將cookie發送給服務器,服務器會通過與服務器內存中的用戶信息匹配,如果匹配成功,則返回客戶端想要的內容,否則拋出錯誤提示客戶端需要重新登錄。大致流程圖如下:
實踐操作
接下來我們來進行實踐操作,在此之前請預先執行 npm i express express-session
,安裝所需要的模塊。
index.js文件的代碼如下:
// 導入 express 模塊 const express = require('express') // 創建 express 的服務器實例 const app = express() // 01:配置 Session 中間件 const session = require('express-session') app.use( session({ secret: 'heyyyyfx',//此處的secret密鑰可以是任意字符串,是你自己制定的專屬加密方案,此處筆者將以自己的名字為例 resave: false,//無需在意,但是要寫上 saveUninitialized: true,//無需在意,但是要寫上 }) ) // 托管靜態頁面,此處筆者代理瞭一個靜態文件,文件內容下文可見。 app.use(express.static('./pages')) // 解析 POST 提交過來的表單數據 app.use(express.urlencoded({ extended: false })) // 登錄的 API 接口 app.post('/api/login', (req, res) => { // 判斷用戶提交的登錄信息是否正確,此處寫死一個賬號密碼校驗,在實際開發中肯定是需要數據庫匹配。 if (req.body.username !== 'admin' || req.body.password !== '000000') { return res.send({ status: 1, msg: '登錄失敗' }) } // 02:請將登錄成功後的用戶信息,保存到 Session 中 // 註意:隻有成功配置瞭 express-session 這個中間件之後,才能夠通過 req 點出來 session 這個屬性 req.session.user = req.body // 用戶的信息,我們將用戶的信息轉換成cookie字符串返回給用戶。 req.session.islogin = true // 用戶的登錄狀態,也是我們鑒權的參考 res.send({ status: 0, msg: '登錄成功' }) }) // 獲取用戶姓名的接口 app.get('/api/username', (req, res) => { // 03:請從 Session 中獲取用戶的名稱,響應給客戶端 if (!req.session.islogin) {//此處就進行瞭鑒權,看用戶的cookie是否有我們之前發送給他的islogin字段。 return res.send({ status: 1, msg: 'fail' }) } res.send({ status: 0, msg: 'success', username: req.session.user.username, }) }) // 退出登錄的接口 app.post('/api/logout', (req, res) => { // 04:清空 Session 信息 req.session.destroy() res.send({ status: 0, msg: '退出登錄成功', }) }) // 調用 app.listen 方法,指定端口號並啟動web服務器 app.listen(80, function () { console.log('Express server running at http://127.0.0.1:80') })
筆者在此隻附上index.js的內容,其他文件內容可以在文末中拿到源碼。
其他
缺陷
可以看到,Session機制需要cookie的配合才能實現,因此cookie的的缺點或特性也就會影響到Session鑒權,比如,cookie是默認不支持跨域的,當前端跨域請求後端接口時,需要做很多額外的配置,這也就是為什麼Session推薦在服務端使用。
關於跨域
筆者在本文中說到的的跨域問題,指的是客戶端和服務端二者的跨域,如果讀者下載瞭源碼,可以看到筆者是在app.js(index.js)中使用app.use(express.static('./pages'))
進行瞭靜態托管,以此來保證客戶端和服務端都是locallhost:80,是同源的。感興趣的讀者可以嘗試用live Sever來代理Index.html文件,看看效果如何,在此之前記得引入cors
中間件支持跨域。
想說的
其實筆者在此隻是簡單講解瞭Session鑒權的大致原理以及進行瞭簡單的實現,在實際真實開發中,首先我們不建議將用戶信息返回生成cookie字符串再返回給客戶端,因為這是非常隱私的信息,其次要知道cookie是可以直接在客戶端更改的,因此鑒權關鍵字段也是需要斟酌的,現實開發是非常嚴謹的,請讀者在實際使用時秉承嚴謹的態度。
JWT鑒權
適用情況
上文已經說到瞭,session會受到跨域的影響,因此在前後端分離開發以及存在跨域的情況下,我們推薦使用JWT鑒權。
JWT鑒權原理
JWT原理和Session大致相同,不同的點在於,JWT生成的Token字符串需要客戶端手動存儲在localStorage或sessionStorage中。再次請求時,客戶端需要將Token放在請求頭的Authorization字段中。
JWT
jwt是Json Web Token的縮寫,它的結構分為三個部分:header.payload.signature,兩兩之間用【.】分隔。
header
header是一個JSON結構,主要包含token的類型(即JWT),簽名的算法
{ "alg":"HS256", "typ":"JWT" }
payload
payload也是JSON結構,它是存放有效信息的地方,JWT官方提供瞭一些官方字段,你也可以定義自己的私有字段,其中官方字段如下:
- iss:簽發人
- exp:token過期時間
- sub:主題
- aud:受眾
- nbf:生效時間
- iat:簽發時間
- jti:編號
但是註意,payload是默認不加密的,因此建議自己定義的私有字段不要放入用戶私密信息。
signature
它是用戶自己定義的字段,用戶要設計一個獨一無二且保證不會外泄的密鑰,通過下方算法生成簽名,用於未來的身份驗證。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
實踐
首先安裝必要的npm包,執行以下指令:
npm i body-parser cors express express-jwt jsonwebtoken,
在index.js中寫入以下內容:
// 導入 express 模塊 const express = require('express') // 創建 express 的服務器實例 const app = express() // 01:安裝並導入 JWT 相關的兩個包,分別是 jsonwebtoken 和 express-jwt const jwt = require('jsonwebtoken') const expressJWT = require('express-jwt') // 允許跨域資源共享 const cors = require('cors') app.use(cors()) // 解析 post 表單數據的中間件 const bodyParser = require('body-parser') app.use(bodyParser.urlencoded({ extended: false })) // 02:定義 secret 密鑰,建議將密鑰命名為 secretKey const secretKey = 'heyyyyfx' // 04:註冊將 JWT 字符串解析還原成 JSON 對象的中間件 // 註意:隻要配置成功瞭 express-jwt 這個中間件,就可以把解析出來的用戶信息,掛載到 req.user 屬性上 // unless指定哪些接口不需要訪問權限,即白名單。 app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] })) // 登錄接口 app.post('/api/login', function (req, res) { // 將 req.body 請求體中的數據,轉存為 userinfo 常量 const userinfo = req.body // 登錄失敗 if (userinfo.username !== 'admin' || userinfo.password !== '000000') { return res.send({ status: 400, message: '登錄失敗!', }) } // 登錄成功 // 03:在登錄成功之後,調用 jwt.sign() 方法生成 JWT 字符串。並通過 token 屬性發送給客戶端 // 參數1:用戶的信息對象 // 參數2:加密的秘鑰 // 參數3:配置對象,可以配置當前 token 的有效期,本處設置的是30S // 記住:千萬不要把密碼加密到 token 字符中 const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' }) res.send({ status: 200, message: '登錄成功!', token: tokenStr, // 要發送給客戶端的 token 字符串 }) }) // 這是一個有權限的 API 接口 app.get('/admin/getinfo', function (req, res) { // 05:使用 req.user 獲取用戶信息,並使用 data 屬性將用戶信息發送給客戶端 console.log(req.user) res.send({ status: 200, message: '獲取用戶信息成功!', data: req.user, // 要發送給客戶端的用戶信息 }) }) // 06:使用全局錯誤處理中間件,捕獲解析 JWT 失敗後產生的錯誤 app.use((err, req, res, next) => { // 這次錯誤是由 token 解析失敗導致的 if (err.name === 'UnauthorizedError') { return res.send({ status: 401, message: '無效的token', }) } res.send({ status: 500, message: '未知的錯誤', }) }) // 調用 app.listen 方法,指定端口號並啟動web服務器 app.listen(8888, function () { console.log('Express server running at http://127.0.0.1:8888') })
開啟node服務後,用postman進行測試,調用登錄接口後,拿到返回的Token:
隨後調用獲取用戶信息接口,註意,該接口是需要權限的,我們需要在請求頭中加入Authorization字段,值為Token,同時有個註意事項,Token值前需要加入Bearer關鍵字,用空格分隔,這是必要的操作。
如果你操作的過慢,就會看到如下報錯,這是因為我們的有效期隻有30s。
不妨再調用一次登錄接口,同時迅速用新的Token請求用戶信息接口,結果如下,說明成功。
想說的
Token有效期問題
在本文中,我們是自己為Token設置瞭30s的有效期,但如果你用心觀察國內外的網站,貌似沒有出現用著用著就突然返回到登錄界面讓你突然重新登陸的,難道是因為他們的有效期設置的特別長?
其實在真實開發中,Token的有效期往往不會用這種方式設置,大多數有效期是動態的,打個比方,隻有當你在當前頁面半小時之內沒有任何請求之後,才會讓你的Token自動失效,這種是怎樣實現的?其實有很多種實現方案,筆者在此隻舉一種例子,讀者可以先瞭解一下redis數據庫。
redis數據庫及動態Token解決方案
redis的優點在此不做過多說明,感興趣的可以自行查閱,redis數據庫提供瞭一個叫expire的命令,命令用於設置 key 的過期時間,key 過期後將不再可用。單位以秒計。
我們可以以此為基礎,當用戶請求登錄接口時,我們將Token返回給用戶,同時我們將這個Token作為Key存儲到數據庫,Value為這個用戶的個人信息或其他內容,並為這個key設置一個定時刪除命令,當用戶在有效期時,數據庫將用戶請求接口時攜帶的Token進行查詢,看是否存在這個Token的key,當可以被查詢時,說明有效期還在(因為過瞭有效期這個Token就會被刪除,表中就無法查詢到這個Token),同時再次對這個Key執行定時刪除任務,達到覆蓋上一次刪除定時任務,延長有效期的作用,隻有當沒有接口請求後,刪除任務執行,Token才會失效,以此來實現動態Token的目的,至於覆蓋定時刪除任務這個操作,因為是每一個操作相關的接口都要進行,因此不妨將它封裝成全局中間件,避免在每個接口中都寫下重復代碼。
最後
源碼:https://github.com/fengxiao1998/SessionAndJWT
本文所有內容都是基於node的鑒權,相比於純後端Java開發肯定會有很多不足之處,對於前端而言隻是和大傢一起瞭解學習鑒權相關知識,更多關於node Session JWT鑒權登錄的資料請關註WalkonNet其它相關文章!