Nodejs及expresss框架初探

Nodejs及express框架初探

Nodejs简单介绍

Javascript同样是一门C-like的语言,但是它更加容易上手,因此又跟Python十分类似。Nodejs是一个基于Chrome V8引擎的JavaScript运行环境,大大提高了Javascript的运行效率,从而使Javascript的业务范围从原先的前端向后端衍生,是Javascript成为了与PHP,Python等服务端脚本语言平起平坐的语言。接下来要探讨的Express框架也可以简单理解成和Python中Flask、Django相类似的框架。与Python做类比,可以更好地理解Javascript以及它的一些框架。

需要注意的是:Javascript是编程语言,Nodejs是Javascript的运行环境,Express是与运行在Nodejs下的框架,下文对Nodejs和Javascript不会做明显的区分,有时候也会用Nodejs代指Javascript。

Nodejs的一些特点

回调函数的使用

1
2
3
4
app.get('/greet/:name', (req, res) => {
const name = req.params.name;
res.send(`Hello, ${name}!`);
});

创建了一个打招呼的路由,可以看到app.get()的第二个参数是一个匿名函数。将一个函数作为参数传给另一个函数,这就是一个典型的回调函数的用法,在异步操作结束后会调用这个匿名函数,实现了对对应请求的处理

异步是一种编程模型,它允许在执行某个操作的同时继续执行后续的代码,而不必等待该操作的完成

1
2
3
4
5
6
7
fs.readFile('file.txt', 'utf8', function(err, data) {
if (err) {
console.error('读取文件出错:', err);
return;
}
console.log('文件内容:', data);
});

另一个读取文件的例子

异步的概念主要出现在需要等待的操作,比如网络请求、文件读写、数据库查询等。在传统的同步编程中,这些操作会阻塞程序的执行,直到操作完成才会继续执行下一步。而异步编程通过将这些操作委托给其他系统(比如操作系统、网络模块等)来进行处理,允许程序在等待操作完成的同时执行其他任务

模块导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// controllers.js
function LoginController(req, res) {
// 实现登录控制器的逻辑
}

function CheckInternalController(req, res) {
// 实现内部检查控制器的逻辑
}

function CheckController(req, res) {
// 实现检查控制器的逻辑
}

module.exports = {
LoginController,
CheckInternalController,
CheckController
}

Nodejs通过module.exports实现模块的导出

1
2
3
4
5
// main.js
// 导入整个文件
const controller = require("./controller")
// 导入部分接口
const { LoginController, CheckController } = require('./controllers');

在另一个文件中实现模块的导入,可以联想一下Python的import语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# controllers.py
class LoginController:
def __init__(self):
# 初始化操作,如果有需要的话
pass

def process_request(self, user_input):
# 实现登录控制器的逻辑
pass

class CheckInternalController:
def __init__(self):
# 初始化操作,如果有需要的话
pass

def process_request(self, user_input):
# 实现内部检查控制器的逻辑
pass

class CheckController:
def __init__(self):
# 初始化操作,如果有需要的话
pass

def process_request(self, user_input):
# 实现检查控制器的逻辑
pass

# main.py
import controller
from controller import LoginController

翻译成Python就是这样,是不是很像?所以Nodejs同样也挺简单的

npm

Nodejs特有的包管理器,类似于Python的pip

1
2
3
4
5
6
7
npm install ...
# 国内运行的慢的话可以使用cnpm
cnpm install
# 代理
npm config set proxy http://127.0.0.1:7890
# 换源(淘宝)
npm config set registry https://registry.npmmirror.com

项目文件夹下的node_modules和package.json

  • node_modules:存放安装的第三方包,类似Python的虚拟环境,直接把包安装在了项目文件夹下,而不是全局包
  • package.json: 像Python项目的requirements.txt?声明了项目所依赖的包

原型链(难点)

Javascript非常重要的一个点,在CTF比赛中,Nodejs相关的题目往往伴随着原型链污染。

分清__proto__prototype

原型链相关的四个概念两个准则

四个概念
  • JavaScript的对象有两种:普通对象函数对象,所有的对象都有__proto__属性,但是只有函数对象prototype属性
  • 属性__proto__也是一个对象,它有两个属性:__proto__constructor(构造函数)
  • 原型对象prototype有一个默认的constructor属性,用于记录实例由哪个类创建
  • ObjectFunction都是Javascript内置的函数对象,使用较多的ArrayRegExpDateBooleanNumberString也都是内置的函数对象
两个准则(js之父在设计原型链时所设立的准则)
  • Person.prototype.constructor == Person 准则1:原型对象的construcor的构造函数指向本身
  • person.__proto__ == Person.prototype 准则2:实例的__proto__和原型对象指向同一个地方

链子的终点

结合上图可以看出__proto__prototypeconstructor都有终点

  • __proto__的终点是原型对象Object.prototype,而Object.prototype.__proto__ == null
  • prototype的终点是Function.prototype
  • constructor的终点是Function(),体现了上面所说的第四个概念

e.g.

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age){
this.name = name
this.age = age
}

p1 = new Person("Diana", 18)
Person.prototype.motherland = "China"
console.log(p1)
// Person { name: 'Diana', age: 18 }
console.log(p1.motherland)
// China

需要注意的是,通过prototype创建出来的属性是挂载在原型链上面的,用console.log打印不会直接打印该属性

Javascript通过这种方式实现了类的继承,使用原型链能减少内存的消耗

关于原型链污染,我会在安全专题中继续深入探讨(画个饼)

Express框架初探

上文介绍了一些Nodejs下的特点以及一些语法,本质上和别的编程语言没有太大的差别,接下来就简要探索一下Express框架

简单的Web服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const express = require("express")

app = express()

// 定义一个基本路由,返回 "Hello, World!"
app.get('/', (req, res) => {
res.send('Hello, World!');
});

// 定义一个接收参数的路由,返回个性化的欢迎消息
app.get('/greet/:name', (req, res) => {
const name = req.params.name;
res.send(`Hello, ${name}!`);
});

// 监听端口 3000,启动服务器
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

一个很简单的Web服务demo

首先引入express包,赋值给app对象,一般使用两种常见的HTTP请求方式,GETPOST

(req,res)

通常使用req和res作为回调函数的参数,两者都有许多属性,可以获取并修改HTTP包,下面介绍一些常用的属性

req(HTTP请求对象)

  • req.url: 获取请求的 URL。
  • req.method: 获取请求的 HTTP 方法(GET、POST、PUT、DELETE 等)。
  • req.params: 获取路由参数,通常用于获取动态路由中的参数。
  • req.query: 获取GET请求字符串参数。
  • req.headers: 获取 HTTP 头部信息。
  • req.body: 获取请求体中的数据,通常用于 POST 请求中的表单数据或 JSON 数据。
  • req.cookies: 获取客户端发送的 Cookie。
  • req.ip: 获取客户端的 IP 地址。
  • req.protocol: 获取请求的协议(http 或 https)

可以看到这些属性都是http包里的参数,更多参数参考官方文档

GET请求

使用req.query即可获取

POST请求

使用req.body获取,但是默认情况下,express不会解析POST请求的请求体(会显示undefined),所以需要依赖中间件来解析请求体,默认情况下使用body-parser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const express = require("express");
const bodyParser = require("body-parser");
const app = express();

app.use(bodyParser.urlencoded({extend: false}));

app.post("/", (req, res) => {
const username = req.body.username;
const password = req.body.password;

console.log("Username: ", username);
console.log("Password: ", password);

res.send("Post success!");
});

const PORT = 3001;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

可以成功解析

res(HTTP响应对象)

  • res.send(): 发送响应给客户端,通常用于发送文本、HTML、JSON 等数据。

  • res.json(): 发送 JSON 格式的响应给客户端。

  • res.status(): 设置响应的状态码。

  • res.setHeader(): 设置响应头。

  • res.cookie(): 设置响应的 Cookie。

  • res.redirect(): 发送重定向响应给客户端。

  • res.sendFile(): 发送文件给客户端。

  • res.render(): 渲染模板并发送给客户端。

基本都是HTTP响应包的参数

也可以组合使用

1
res.status(200).json(data);
重定向
1
2
3
4
// 重定向
app.get("/", (req, res) => {
res.redirect("https://space.bilibili.com/672328094")
});
下载文件

发送文件需要指定根目录,否则会报错

TypeError: path must be absolute or specify root to res.sendFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');
const path = require('path');
const app = express();

app.get('/file', (req, res) => {
// 获取要发送的文件的绝对路径 __dirname返回文件夹绝对路径 resolve会把路径拼接
const filePath = path.resolve(__dirname, 'package.json');

// 发送文件给客户端
res.download(filePath);
});

const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
上传文件
1
2
3
4
5
6
7
8
9
10
11
12
const { v4: uuidv4 } = require('uuid');

app.post('/upload', (req, res) => {
const file = req.files.file;
const uniqueFileName = uuidv4();
const destinationPath = path.join(__dirname, 'uploads', file.name);
// 将文件写入 uploads 目录
fs.writeFileSync(destinationPath, file.data);
// 添加到全局变量中
global.fileDictionary[uniqueFileName] = file.name;
res.send(uniqueFileName);
});

Ejs模板

可以用Flask的Jinja2模板引擎类比,不过Ejs使用<% %><%= %>作为模板语句

1
2
3
4
5
6
7
8
9
10
11
const ejs = require('ejs');

// 设置模板的位置
app.set('views', path.join(__dirname, 'views'));
// 设置模板引擎
app.set('view engine', 'ejs');

app.get('/', (req, res) => {
// 返回./views/index.ejs
res.send('index');
});

其他重要的中间件

fs模块

Nodejs使用fs模块进行文件操作

  1. 文件读取和写入
    • fs.readFile():用于异步读取文件内容。
    • fs.readFileSync():用于同步读取文件内容。
    • fs.writeFile():用于异步写入文件内容。
    • fs.writeFileSync():用于同步写入文件内容。
  2. 文件操作
    • fs.rename():用于重命名文件或移动文件。
    • fs.unlink():用于删除文件。
    • fs.copyFile():用于复制文件。
  3. 目录操作
    • fs.mkdir():用于创建目录。
    • fs.rmdir():用于删除目录。
    • fs.readdir():用于读取目录内容。
  4. 文件信息
    • fs.stat():用于获取文件或目录的状态信息,如大小、创建时间等。
    • fs.existsSync():用于检查文件或目录是否存在。
  5. 流操作
    • fs.createReadStream():用于创建可读流。
    • fs.createWriteStream():用于创建可写流。
  6. 其他
    • fs.access():用于检查文件或目录的权限。
    • fs.chmod():用于修改文件或目录的权限。
    • fs.watch():用于监视文件或目录的变化。

读取文件

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

filePath = "./example.txt"

fs.readFile(filePath, 'utf-8', (err, data) => {
if (err){
console.log("File read error!");
return;
}
console.log("文件内容:", data);
});

同样使用了回调函数

写文件

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

filePath = "./example.txt";
fileContent = "Hello World!";

fs.writeFile(filePath, fileContent, (err) => {
if (err){
console.log("File write error!");
return;
}
});

child_process模块

Nodejs的另一个核心模块,通常用于执行系统命令

exec

1
2
3
4
5
6
7
8
9
10
11
12
13
const childProcess = require('child_process');

childProcess.exec('whoami', (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error.message}`);
return;
}
if (stderr) {
console.error(`执行出错: ${stderr}`);
return;
}
console.log(`执行结果: ${stdout}`);
});

也可以使用Nodejs的语法糖,使用{}child_process对象中提取exec方法

1
2
3
4
5
6
7
8
9
10
11
12
13
{ exec } = require('child_process');

exec('whoami', (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error.message}`);
return;
}
if (stderr) {
console.error(`执行出错: ${stderr}`);
return;
}
console.log(`执行结果: ${stdout}`);
});

spawn

spawn用于创建一个新的nodejs子进程,它比exec更灵活,可以更好地处理大量输出数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{ spawn } = require('child_process');

// 创建了一个whoami进程
const whoami = spawn('whoami');

//监听data事件,获取子进程标准输出流的数据
whoami.stdout.on('data', (data) => {
console.log(`子进程输出: ${data}`);
});

//监听error事件,当子进程错误时触发
whoami.stderr.on('error', (err) => {
console.error(`子进程错误: ${err}`);
});


//监听close,当子进程退出时触发
whoami.on('close', (code) => {
console.log(`子进程退出,退出码: ${code}`);
});

lodash模块

lodash 是一个 JavaScript 实用工具库,提供了许多常用的函数和方法,用于简化开发过程中的数据处理、函数操作、集合迭代、数组操作、对象操作等。lodash 中的每个函数都经过优化,具有高性能和可靠性。

map函数

和Python中一样

1
2
3
4
5
6
const _ = require('lodash');

const numbers = [1, 2, 3, 4, 5];
const squares = _.map(numbers, (num) => num * num);

console.log(squares); // 输出: [1, 4, 9, 16, 25]

groupBy函数

用于分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const _ = require('lodash');

const people = [
{ name: 'Ava', age: 19 },
{ name: 'Diana', age: 18 },
{ name: 'Bella', age: 19 }
];

const groupedByAge = _.groupBy(people, 'age');

console.log(groupedByAge);
// 输出:
// {
// '18': [ { name: 'Diana', age: 18 } ],
// '19': [ { name: 'Ava', age: 19 }, { name: 'Bella', age: 19 } ]
// }

merge函数

与安全最紧密相关的当然是这个merge函数,从这个merge函数衍生出了很多原型链污染的CTF题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const _ = require('lodash');

const object = {
'a': [{ 'b': 2 }, { 'd': 4 }]
};

const source = {
'a': [{ 'c': 3 }, { 'e': 5 }]
};

const result = _.merge(object, source);
console.log(result);
// 输出:
// {
// 'a': [
// { 'b': 2, 'c': 3 },
// { 'd': 4, 'e': 5 }
// ]
// }

递归合并了两个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const _ = require('lodash');

const object = {
'name': 'Ava',
'gender': 'female',
'age': 18,
'country':{
'population': '1400000000'
}
};

const source = {
'name': 'Diana',
'gender': 'female',
'age': 18,
'country': {
'name': 'China',
'continent': 'Asia'
}
};

const result = _.merge(object, source);
console.log(result);
// 输出:
// {
// name: 'Diana',
// gender: 'female',
// age: 18,
// country: { population: '1400000000', name: 'China', continent: 'Asia' }
// }

递归合并了两个json对象,保留source中的属性,舍弃object中的属性,新建原先source中没有的属性

1
2
3
4
5
6
7
8
9
10
// 源码大致就是这个意思 当然这里的source和上面的source相反
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

关于原型链污染,会在安全专题展开讲

作者

D1anash1ba

发布于

2024-02-09

更新于

2024-02-09

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.