node.js極速入門課程:進(jìn)入學(xué)習(xí)
原文地址:https://ailjx.blog.csdn.net/article/details/127909213
作者:海底燒烤店ai
在前面的幾節(jié)中我們已經(jīng)創(chuàng)建并優(yōu)化好了簡易用戶管理系統(tǒng)的項(xiàng)目結(jié)構(gòu),也對 Cookie-Session登錄驗(yàn)證
的工作原理做了講解,接下來我們將繼續(xù)補(bǔ)充這個系統(tǒng)的功能,這一節(jié)我們將實(shí)戰(zhàn)運(yùn)用Cookie-Session
來實(shí)現(xiàn)這個系統(tǒng)的登錄驗(yàn)證功能?!鞠嚓P(guān)教程推薦:nodejs視頻教程】
什么?你還不了解session
、cookie
!快去看看上篇文章吧:詳解 Cookie-Session登錄驗(yàn)證 的工作原理
1️⃣ 定義頁面路由
在vies
目錄下新建login.ejs
:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>登錄頁面</h1> <div>用戶名:<input type="text" id="username"></div> <div>密碼:<input type="password" id="password"></div> <div><button id="login">登錄</button></div> <script> const uname = document.getElementById("username"); const pwd = document.getElementById("password"); const login = document.getElementById("login"); login.onclick = () => { fetch('/api/login', { method: 'POST', body: JSON.stringify({ username: uname.value, password: pwd.value }), headers: { "Content-Type": "application/json" } }).then(res => res.json()).then(res => { // console.log(res); if (res.ok) { location.href = "/" } else { alert("用戶名密碼不匹配!") } }) } </script> </body> </html>
注意:頁面中請求的接口是
POST /api/login
請求
在routes
目錄下新建login.js
,該文件定義login
頁面的頁面路由:
var express = require("express"); var router = express.Router(); /* GET login page. */ router.get("/", function (req, res, next) { res.render("login"); }); module.exports = router;
在app.js
中掛載頁面路由:
// 引入 var loginRouter = require("./routes/login"); // 掛載 app.use("/login", loginRouter);
啟動項(xiàng)目,訪問http://localhost:3000/login
正常顯示:
2️⃣ 定義API接口
在services/UserService.js
中定義接口的模型(M層):
const UserService = { // ....... // 登錄查詢 login: (username, password) => { // 向數(shù)據(jù)庫查詢該用戶 return UserModel.findOne({ username, password }); }, };
在controllers/UserController.js
中定義接口的控制層(C層):
const UserController = { // ...... // 登錄驗(yàn)證 login: async (req, res, next) => { try { const { username, password } = req.body; const data = await UserService.login(username, password); // console.log(data); if (data) { res.send({ ok: 1, msg: "登錄成功!", data }); } else { res.send({ ok: 0, msg: "用戶不存在,登錄失敗!" }); } } catch (error) { console.log(error); } }, };
在routes/users.js
中定義Api
路由:
// 登錄校驗(yàn) router.post("/login", UserController.login);
至此登錄頁面就搭建好了:
3️⃣ 配置session
在上一節(jié)Cookie-Session
登錄驗(yàn)證工作原理的介紹中我們知道:
這個過程顯然是比較復(fù)雜的,在express
中有一個express-session
模塊可以大大降低我們的工作量,讓我們站在巨人的肩膀上開發(fā)!
下載express-session
:
npm i express-session
在app.js
中進(jìn)行配置:
// 引入express-session var session = require("express-session"); // 配置session:需要放在在路由配置的前面 app.use( session({ name: "AilixUserSystem", // cookie名字 secret: "iahsiuhaishia666sasas", // 密鑰:服務(wù)器生成的session的簽名 cookie: { maxAge: 1000 * 60 * 60, // 過期時間:一個小時過期 secure: false, // 為true時表示只有https協(xié)議才能訪問cookie }, resave: true, // 重新設(shè)置session后會重新計(jì)算過期時間 rolling: true, // 為true時表示:在超時前刷新時cookie會重新計(jì)時;為false表示:在超時前無論刷新多少次,都是按照第一次刷新開始計(jì)時 saveUninitialized: true, // 為true時表示一開始訪問網(wǎng)站就生成cookie,不過生成的這個cookie是無效的,相當(dāng)于是沒有激活的信用卡 }) );
配置好后,就會發(fā)現(xiàn)瀏覽器中有一個名為AilixUserSystem
的cookie
:
這是因?yàn)?code>express-session會自動解析cookie
和向前端設(shè)置cookie
,相當(dāng)于是圖一中的3、6(前半部分:通過SessionId
查詢到Session
) ,我們不再需要手動對cookie
進(jìn)行操作。
4️⃣ 權(quán)限驗(yàn)證
在登錄成功時設(shè)置session
:
// controllers/UserController.js // .... // 登錄校驗(yàn) login: async (req, res, next) => { try { const { username, password } = req.body; const data = await UserService.login(username, password); // console.log(data); if (data) { // 設(shè)置session:向session對象內(nèi)添加一個user字段表示當(dāng)前登錄用戶 req.session.user = data; // 默認(rèn)存在內(nèi)存中,服務(wù)器一重啟就沒了 res.send({ ok: 1, msg: "登錄成功!", data }); } else { res.send({ ok: 0, msg: "用戶不存在,登錄失敗!" }); } } catch (error) { console.log(error); } },
我們向req.session
中添加了一個user
字段,來保存用戶登錄的信息,這一步相當(dāng)于是 圖一中的1(SessionId會由express-session
模塊自動生成)、2。
req.session
是一個session
對象,需要注意的是這個對象雖然存在于req
中,但其實(shí)不同的人訪問系統(tǒng)時他們的req.session
是不同的,因?yàn)?req.session
是根據(jù)我們設(shè)置的cookie
(由express-session
模塊自動生成的AilixUserSystem
)生成的,每一個人訪問系統(tǒng)所生成的cookie
是獨(dú)一無二的,所以他們的req.session
也是獨(dú)一無二的。
在收到請求時校驗(yàn)session
,在app.js
添加以下代碼:
// 設(shè)置中間件:session過期校驗(yàn) app.use((req, res, next) => { // 排除login相關(guān)的路由和接口 // 這個項(xiàng)目中有兩個,一個是/login的頁面路由,一個是/api/login的post api路由,這兩個路由不能被攔截 if (req.url.includes("login")) { next(); return; } if (req.session.user) { // session對象內(nèi)存在user,代表已登錄,則放行 // 重新設(shè)置一下session,從而使session的過期時間重新計(jì)算(在session配置中配置了: resave: true) // 假如設(shè)置的過期時間為1小時,則當(dāng)我12點(diǎn)調(diào)用接口時,session會在1點(diǎn)過期,當(dāng)我12點(diǎn)半再次調(diào)用接口時,session會變成在1點(diǎn)半才會過期 // 如果不重新計(jì)算session的過期時間,session則會固定的1小時過期一次,無論這期間你是否進(jìn)行調(diào)用接口等操作 // 重新計(jì)算session的過期時間的目的就是為了防止用戶正在操作時session過期導(dǎo)致操作中斷 req.session.myData = Date.now(); // 放行 next(); } else { // session對象內(nèi)不存在user,代表未登錄 // 如果當(dāng)前路由是頁面路由,,則重定向到登錄頁 // 如果當(dāng)前理由是api接口路由,則返回錯誤碼(因?yàn)獒槍jax請求的前后端分離的應(yīng)用請求,后端的重定向不會起作用,需要返回錯誤碼通知前端,讓前端自己進(jìn)行重定向) req.url.includes("api") ? res.status(401).send({ msg: "登錄過期!", code: 401 }) : res.redirect("/login"); } });
注意:這段代碼需要在路由配置的前面。
這段代碼中我們通過req.session.myData = Date.now();
來修改session
對象,從而觸發(fā)session
過期時間的更新(session
上myData
這個屬性以及它的值 Date.now()
只是我們修改session
對象的工具,其本身是沒有任何意義的),你也可以使用其它方法,只要能將req.session
修改即可。
因?yàn)槲覀冞@個項(xiàng)目是后端渲染模板的項(xiàng)目,并不是前后端分離的項(xiàng)目,所以在配置中間件進(jìn)行session
過期校驗(yàn)攔截路由時需要區(qū)分Api路由
和頁面路由
。
后端在攔截API路由后,向前端返回錯誤和狀態(tài)碼:
這個時候需要讓前端自己對返回結(jié)果進(jìn)行判斷從而進(jìn)行下一步的操作(如回到登錄頁或顯示彈窗提示),該系統(tǒng)中前端是使用JavaScript
內(nèi)置的fetch
來進(jìn)行請求發(fā)送的,通過它來對每一個請求結(jié)果進(jìn)行判斷比較麻煩,大家可以自行改用axios
,在axios
的響應(yīng)攔截器中對返回結(jié)果做統(tǒng)一的判斷。
5️⃣ 退出登錄
向首頁(index.ejs
)添加一個退出登錄的按鈕:
<button id="exit">退出登錄</button>
為按鈕添加點(diǎn)擊事件:
const exit = document.getElementById('exit') // 退出登錄 exit.onclick = () => { fetch("/api/logout").then(res => res.json()).then(res => { if (res.ok) { location.href = "/login" } }) }
這里調(diào)用了GET /api/logout
接口,現(xiàn)在定義一下這個接口,在controllers/UserController.js
中定義接口的控制層(C層):
const UserController = { // ...... // 退出登錄 logout: async (req, res, next) => { // destroy方法用來清除cookie,當(dāng)清除成功后會執(zhí)行接收的參數(shù)(一個后調(diào)函數(shù)) req.session.destroy(() => { res.send({ ok: 1, msg: "退出登錄成功!" }); }); }, };
在routes/users.js
中定義Api
路由:
// 退出登錄 router.get("/logout", UserController.logout);
6️⃣ 鏈接數(shù)據(jù)庫
前面我們通過 req.session.user = data;
設(shè)置的session默認(rèn)是存放到內(nèi)存中的,當(dāng)后端服務(wù)重啟時這些session
就會被清空,為了解決這一問題我們可以將session
存放到數(shù)據(jù)庫中。
安裝connect-mongo
:
npm i connect-mongo
connect-mongo是MongoDB會話存儲,用于用
Typescript編寫的連接
和Express
。
修改app.js
:
// 引入connect-mongo var MongoStore = require("connect-mongo"); // 配置session app.use( session({ name: "AilixUserSystem", // cookie名字 secret: "iahsiuhaishia666sasas", // 密鑰:服務(wù)器生成的session的簽名 cookie: { maxAge: 1000 * 60 * 60, // 過期時間:一個小時過期 secure: false, // 為true時表示只有https協(xié)議才能訪問cookie }, resave: true, // 重新設(shè)置session后會重新計(jì)算過期時間 rolling: true, // 為true時表示:在超時前刷新時cookie會重新計(jì)時;為false表示:在超時前無論刷新多少次,都是按照第一次刷新開始計(jì)時 saveUninitialized: true, // 為true時表示一開始訪問網(wǎng)站就生成cookie,不過生成的這個cookie是無效的,相當(dāng)于是沒有激活的信用卡 store: MongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/usersystem_session", // 表示新建一個usersystem_session數(shù)據(jù)庫用來存放session ttl: 1000 * 60 * 60, // 過期時間 }), // 存放數(shù)據(jù)庫的配置 }) );
至此,我們就實(shí)現(xiàn)了運(yùn)用Cookie&Session
進(jìn)行登錄驗(yàn)證/權(quán)限攔截的功能!