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