node.js使用express框架制作简单的学生管理系统
简介
在前面的学习中,我们了解过express框架,也知道了怎么使用node.js对接mysql,接下来我们就要像C语言的结课作业一样做一个简单的学生管理系统(由于没有设计前端框架,所以只是制作一个简单的前后端不分离的服务器)来练练手了。
编写服务器
下载引用的库

这里面用的express和mysql2直接在下面输入
npm install express
npm install mysql2
就可以下载了。
最终在package.json里面会显示,我们在代码中加载的依赖。
连接数据库
const connection = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'kali',
database: 'students'
});
在连接时,需要传入的参数有数据库的位置host(例如localhost就是本地,如果数据库没有部署在本地则需要写明数据库的ip地址),而user和password则是创建在数据库中的账户和密码,使用时可以在里面创建新的用户账户,同时也可以修改用户的权限。
接下来,就是打开数据库的服务器,使用service mysql start开启,没有报错就是打开了。

在打开后可以使用navicat连接由于我们使用的mysql的版本如果较高,加密方式会有不同,所以有些低级版本的navicat并不能连接上mysql,所以我们要准备一个相对版本较高的navicat,这个工具是数据库的可视化工具,相当于客户端主动连接数据库服务器,通常如果它能连接上数据库,用代码连接也没有问题。
在连接之前要先确保打开防火墙,例如mysql运行在3306端口,这就要求我们必须放行3306端口的流量。

这个操作要求我们必须在管理员权限下操作,我们输入sudo su进入管理员权限,接下来如果没有ufw可以通过apt工具下载,在allow指定端口以后,可以通过status来查看端口的开放情况。

在连接时,我选择的是mysql的连接,虽然我的数据库是MariaDB,但也能成功连接,因为它是mysql的一个分支。
登录界面
app.get('/', (req, res) => {
const log = fs.readFileSync('./html/login.html', 'utf-8');
res.set({
'Content-Type' : 'text/html'
})
res.status(200).send(log);
});
这就是一个最简单的页面传输,我们提前写好一个html文件,每次客户访问页面让服务器读取对应的文件。
在我们每次在客户端输入密码,向后端请求的时候,都会发来一个post请求,在服务端只需要判断返回的状态码是多少,就可以判断要不要跳转。前端的脚本是这样的。
。
。
。
<!--这是一个按钮,绑定到了validateLogin函数,每次点击都会运行这个函数-->
<button class="btn" onclick="validateLogin()">立即登录</button>
。
。
。
<script>
function validateLogin() {
const userId = document.getElementById('userId').value;
const password = document.getElementById('password').value;
const errorMessage = document.getElementById('errorMessage');
// 清空错误信息
errorMessage.style.display = 'none';
// 添加加载状态
const btn = document.querySelector('.btn');
btn.innerHTML = '验证中...';
btn.disabled = true;
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ user: userId, password: password })
})
.then(response => {
if (response.ok) {
window.location.href = '/welcome';
} else {
errorMessage.style.display = 'flex';
btn.innerHTML = '立即登录';
btn.disabled = false;
}
})
.catch(error => {
console.error('Error:', error);
errorMessage.textContent = '连接异常,请检查网络';
errorMessage.style.display = 'flex';
btn.innerHTML = '立即登录';
btn.disabled = false;
});
}
// 添加输入框回车提交
document.querySelectorAll('input').forEach(input => {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') validateLogin();
});
});
</script>
if (response.ok) {window.location.href = '/welcome';}这个语句就是用来切换界面的,如果我们输入的语句正确的话,会自动切换到URl为welcome的界面。这就是基本的逻辑。
在服务端会进行一个用户账号密码的校验,根据返回的结果判断状态码应该返回什么。
app.post ('/login', (req, res) => {
const user = req.body.user;
const password = req.body.password;
let sql = `SELECT * FROM Users WHERE UserID = '${user}' AND Password = '${password}'`;
connection.query(sql, (err, result, fields) => {
if (err) {
console.log(err);
res.status(500).send('Error');
} else {
if (result.length > 0) {
res.status(200).send('welcome');
} else {
res.status(401).send('fail');
}
}
});
});
问题思考
在编写sql语句时,我们应该查询用户输入的账户然后获得密码后在前端校验密码,还是应该在传入账号密码以后查询数据库只返回前端对应的校验码?
站在安全的角度讲,如果密码直接用json文件传入前端,那么有很大的风险,例如使用Burp抓包,可以直接看到密码是什么。服务端永远是最权威的校验方。常见的安全策略就是数据库也不存储密码,而是存储密码的哈希值,这样就是数据库管理员也看不到密码,在服务端只需要校验前端送来的哈希加密后的密码和数据库中存储的加密的密码就可以了。
菜单设计
<a href="/insert" class="action-btn">插入数据</a>
<a href="/delete" class="action-btn">删除数据</a>
<a href="/update" class="action-btn">修改数据</a>
<a href="/select" class="action-btn">信息查询</a>
<a href="/search_grade" class="action-btn">成绩查询</a>
<a href="/scourse" class="action-btn special">课程管理</a>
在前端界面写上这些就可以了,然后再在服务端分别配置这些路由。

根据不同的超链接标签可以跳转到不同的界面。
服务端完整代码
const express = require('express');
const app = express();
const fs = require("fs");
const mysql = require("mysql2");
const port = 8000;
const host = '192.168.1.14';//'127.0.0.1';
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// 连接数据库
const connection = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'kali',
database: 'students'
});
app.get('/', (req, res) => {
const log = fs.readFileSync('./html/login.html', 'utf-8');
res.set({
'Content-Type' : 'text/html'
})
res.status(200).send(log);
});
app.post ('/login', (req, res) => {
const user = req.body.user;
const password = req.body.password;
let sql = `SELECT * FROM Users WHERE UserID = '${user}' AND Password = '${password}'`;
connection.query(sql, (err, result, fields) => {
if (err) {
console.log(err);
res.status(500).send('Error');
} else {
if (result.length > 0) {
res.status(200).send('welcome');
} else {
res.status(401).send('fail');
}
}
});
});
。
。
。
app.use((req,res)=>{
const f = fs.readFileSync('./html/404.html', 'utf-8');
res.set({
'Content-Type' : 'text/html'
});
res.status(200).send(f);
});
app.listen(port,host,() => {
console.log(`Server is running on host ${host} port ${port}`);
});
这个是服务端的代码,因为篇幅问题不在这里展示完整的代码,大家可以直接复制使用,但是在使用的过程中自己要修改监听的端口和host地址(IP),同时,数据库的基本信息也要做修改,但是由于建立的数据库不一样所以只能实现页面浏览跳转的功能。
优化与安全策略
在使用的过程中,我们发现通过输入密码可以从/根目录跳转到/welcome,但是直接在浏览器中输入url也同样可以到达界面。这种漏洞,如果有用户使用过系统,其它的用户就可以根据他提供的url直接登录操作界面。
session-cookie策略
这种策略主要是用来区别用户的,例如,在淘宝中,我登陆了一个界面,我把某商品加入了购物车,服务器怎么区别我是谁,怎么区别是加入你的购物车还是我的。通常某些需要登录的网页中会设置cookie,每次操作的过程都会向服务器传过去cookie值,用来记录用户的身份。
如果cookie被别人窃取了,那么他就可以以我们的身份来做一些事情,相当于拿着我们的身份证干坏事,这就是中间人攻击。
接下来,我会介绍如何使用session-cookie。
下载依赖
npm install express-session cookie-parser
在express中启动依赖
const session = require('express-session');
const cookieParser = require('cookie-parser');
// 启用 Cookie 解析
app.use(cookieParser());
// 配置 Session
app.use(session({
secret: 'your-secret-key', // 用于加密 Session 的密钥(建议使用环境变量存储)
resave: false,
saveUninitialized: false,
cookie: { secure: false } // 如果是 HTTPS,设为 true
}));
修改路由在成功登录时设置session
app.post('/login', (req, res) => {
const user = req.body.user;
const password = req.body.password;
let sql = `SELECT * FROM Users WHERE UserID = '${user}' AND Password = '${password}'`;
connection.query(sql, [user, password], (err, result) => {
if (err) {
console.log(err);
return res.status(500).send('Error');
}
if (result.length > 0) {
//登录成功,设置 Session
req.session.isLoggedIn = true;
req.session.username = user;
res.status(200).send('welcome');
} else {
res.status(401).send('fail');
}
});
});
sql注入漏洞
这种漏洞我们在之前介绍过,通常是由于以下情况引起的。
1.用户输入可以直接操作数据库。
2.数据库sql语句每次执行可以同时执行多个,或者sql语句直接使用字符串拼接。
3.查询的返回值或者报错没有处理,直接呈现给用户,这通常会造成敏感信息的泄露。
接下来,我们使用sqlmap来扫描一下。
C:\Users\35057\Desktop\sqlmapproject-sqlmap-28c838a>python sqlmap.py -u http://192.168.1.14:8000/search?type=StudentID&data=1
___
__H__
___ ___["]_____ ___ ___ {1.9.3.1#dev}
|_ -| . [(] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 22:21:21 /2025-05-03/
[22:21:21] [INFO] testing connection to the target URL
[22:21:21] [INFO] checking if the target is protected by some kind of WAF/IPS
[22:21:21] [INFO] testing if the target URL content is stable
[22:21:22] [INFO] target URL content is stable
[22:21:22] [INFO] testing if GET parameter 'type' is dynamic
[22:21:22] [WARNING] GET parameter 'type' does not appear to be dynamic
[22:21:22] [WARNING] heuristic (basic) test shows that GET parameter 'type' might not be injectable
[22:21:22] [INFO] testing for SQL injection on GET parameter 'type'
[22:21:22] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[22:21:22] [INFO] GET parameter 'type' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable
[22:21:22] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'MySQL'
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] y
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] y
[22:21:28] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[22:21:28] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[22:21:28] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[22:21:28] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[22:21:28] [INFO] testing 'MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'
[22:21:28] [INFO] testing 'MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)'
[22:21:28] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[22:21:28] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[22:21:28] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[22:21:28] [INFO] testing 'MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[22:21:28] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[22:21:28] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[22:21:28] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[22:21:28] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[22:21:28] [INFO] testing 'MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[22:21:28] [INFO] testing 'MySQL >= 4.1 OR error-based - WHERE or HAVING clause (FLOOR)'
[22:21:28] [INFO] testing 'MySQL OR error-based - WHERE or HAVING clause (FLOOR)'
[22:21:28] [INFO] testing 'MySQL >= 5.1 error-based - PROCEDURE ANALYSE (EXTRACTVALUE)'
[22:21:28] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (BIGINT UNSIGNED)'
[22:21:28] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (EXP)'
[22:21:28] [INFO] testing 'MySQL >= 5.6 error-based - Parameter replace (GTID_SUBSET)'
[22:21:28] [INFO] testing 'MySQL >= 5.7.8 error-based - Parameter replace (JSON_KEYS)'
[22:21:28] [INFO] testing 'MySQL >= 5.0 error-based - Parameter replace (FLOOR)'
[22:21:28] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (UPDATEXML)'
[22:21:28] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (EXTRACTVALUE)'
[22:21:28] [INFO] testing 'Generic inline queries'
[22:21:28] [INFO] testing 'MySQL inline queries'
[22:21:28] [INFO] testing 'MySQL >= 5.0.12 stacked queries (comment)'
[22:21:28] [INFO] testing 'MySQL >= 5.0.12 stacked queries'
[22:21:28] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP - comment)'
[22:21:28] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP)'
[22:21:28] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK - comment)'
[22:21:28] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK)'
[22:21:28] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[22:21:38] [INFO] GET parameter 'type' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
[22:21:38] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[22:21:38] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[22:21:38] [INFO] testing 'MySQL UNION query (NULL) - 1 to 20 columns'
[22:21:38] [INFO] testing 'MySQL UNION query (random number) - 1 to 20 columns'
[22:21:39] [INFO] target URL appears to be UNION injectable with 12 columns
[22:21:39] [INFO] GET parameter 'type' is 'MySQL UNION query (random number) - 1 to 20 columns' injectable
GET parameter 'type' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y
sqlmap identified the following injection point(s) with a total of 136 HTTP(s) requests:
---
Parameter: type (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: type=StudentID AND 2689=2689
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: type=StudentID AND (SELECT 3513 FROM (SELECT(SLEEP(5)))rMGe)
Type: UNION query
Title: MySQL UNION query (random number) - 12 columns
Payload: type=StudentID UNION ALL SELECT 8317,8317,8317,8317,CONCAT(0x7176767671,0x566d466a4e685172784a74494e4877554d504d4465764a487941436244734d7176525071567a4773,0x716b626b71),8317,8317,8317,8317,8317,8317,8317#
---
[22:21:43] [INFO] the back-end DBMS is MySQL
web application technology: Express
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
[22:21:43] [WARNING] HTTP error codes detected during run:
500 (Internal Server Error) - 108 times
[22:21:43] [INFO] fetched data logged to text files under 'C:\Users\35057\AppData\Local\sqlmap\output\192.168.1.14'
[*] ending @ 22:21:43 /2025-05-03/
这是扫描后得到的结果,因为该漏洞,黑客可以了解我们的数据库是mysql,使用的服务器框架是express。并且黑客已经可以完全控制数据库。(布尔盲注、时间盲注、联合注入)
解决方案
1.弃用字符串拼接,使用?占位的方式:
connection.query(
'SELECT * FROM table WHERE ?? = ?',
[req.query.type, req.query.data],
(err, result) => {...}
);
2.不使用query方法,使用execute方法,这种方法一次只能执行一个语句。
3.处理错误操作数据库的结果,而不是直接返回。
文件读取
我们发现根据我们的代码,每次用户向我们发送请求时,我们加载新的界面都需要重新读取文件,在基本的操作中,我们又知道,读写文件的操作通常是最慢的,因此,我们的系统很容易因为ddos攻击造成资源耗尽。所以,我们应该想到的策略是把页面数据(html文件)读到全局变量中,每次请求直接读页面就可以。
promise解决异步操作失败
app.get('/search_g', (req, res) => {
const id = req.query.id;
let sql = `SELECT * FROM Grades WHERE StudentID = ${id}`;
connection.query(sql, (err, result, fields) => {
if (err) {
console.log(err);
return res.status(500).send('Error');
}
if (!result || result.length === 0) {
return res.status(404).send('No grades found for the given StudentID');
}
// 创建一个 Promise 数组来处理每个异步查询
const promises = result.map(item => {
return new Promise((resolve, reject) => {
const sql2 = `SELECT * FROM Courses WHERE CourseID = ${item.CourseID}`;
connection.query(sql2, (err, result2, fields2) => {
if (err) {
console.log(err);
reject(err);
} else if (result2.length > 0) {
item.CourseName = result2[0].CourseName; // 添加 CourseName 到 item 对象
item.Coursegrade = result2[0].CourseCredits; // 重命名 Grade 为 Coursegrade
resolve(item);
} else {
console.log('No course found for CourseID:', item.CourseID);
item.CourseName = ''; // 或者你可以设置一个默认值
resolve(item);
}
});
});
});
// 使用 Promise.all 等待所有异步查询完成
Promise.all(promises)
.then(updatedItems => {
res.status(200).json(updatedItems); // 发送更新后的结果
})
.catch(error => {
console.error('Error:', error);
res.status(500).send('Error');
});
});
});
以上是一段promise解决多次查询数据库,异步执行报错的代码。前面提到node.js作为一门不支持多线程的语言,它的异步处理机制非常发达,但是这样并不是好事,例如可能会出现,我们读文件没读完结果已经返回了,或者数据库sql语句没执行完。因此promise就是为了解决这类问题,node的代码在特定的情况下也可以同步执行。
测试界面



更多推荐
所有评论(0)