服务端字符编解码&乱码处理
// 在web服务端开发中,字符的解码几乎我们每天都要打交道,编解码一旦处理不当,就会出现令人头疼的乱码问题
// 不少从事node服务端开发的同学,由于对字符串编码相关知识了解不足,遇到问题时候,经常会一筹莫展,花费大量的时间在排查和解决问题
// 文本先对字符编解码的基础进行简单的介绍,然后举例说明如何在node中进行编解码,最后是服务端的代码案例
关于字符编解码
// 在网络通信的过程当中,传输的都是二进制的比特位,不管发送的内容是文本还是图片,采用的语言是中文还是英文
// 举例,客户端向服务端发送你好 客户端 -- 你好 -- 服务端
// 这中间包括了两个关键步骤,分别对应的是编码和解码
// 1. 客户端: 将"你好"这个字符串,编码成计算机网络需要的二进制比特位
// 2. 客户端: 将收到的二进制比特位,解码为 你好 这个字符串
// 总结一下
// 1. 编码: 将需要传送的数据转成对应的二进制比特位
// 2. 解码: 将二进制比特位转为原始的数据
// 上面有些重要的技术细节没有提到包含
// 1. 客户端嗯么知道 你好 这个字符串对应的比特位是多少
// 2. 服务端收到二进制比特位后, 怎么知道对应的字符串是多少
关于字符集和字符编码
// 上面提到的字符和二进制的转换问题,既然两者都可以互相转换,也就是说明存在明确的转换规则,可以实现字符和二进制的相互转换
// 这里提到的转换规则,其实就是我们进场听到的字符集和字符编码
// 字符集:是一系列字符(文本,标点符号)的集合,字符集有很多,常见的有 ASCII Unicode GBK等,不同的字符集的主要区别在于包含字符个数的不同
// 字符集告诉我们支持哪些字符,单具体字符怎么编码,是有 字符编码 决定的,比如 Unicode字符集,支持的字符编码有 UTF8 UTF16 UTF32
// 概括一下
// 字符集: 字符的集合,不同的字符的集合包含的字符数不同
// 字符编码: 字符集中字符的实际编码方法
// 一个字符集可能有多重字符编码方式
// 可以吧字符编码看成一个映射表,客户端,服务端都是根据这个映射表,来实现字符和二进制的编解码转换
// 举个例子, 你 这个字符在UTF8编码当中,占据三个字节 '0xe4 0xbd 0xa0' 而在GBK编码当中,占据两个字节 '0xc4 0xe3'
字符编码例子
// 上面已经提到了字符编解码所需要的基础知识,下面我们看下简答的例子,借助 icon-lite 这个库来帮我们实现编解码的操作
// 可以看到,在字符编码的时候,我们采用了gbk,在解码时候,如果同样采用gbk,可以得到原始的字符,而解码的时候使用utf8就会出现乱码
const iconv = require("iconv-lite");
const originText = "你";
const encodedBuff = iconv.encode(originText, "gbk");
console.log(encodedBuff); // <Buffer c4 e3>
const decodedText = iconv.decode(encodedBuff, "gbk");
console.log(decodedText); // 你
const wrongText = iconv.decode(encodedBuff, "utf8");
console.log(wrongText); // ��
实际例子:服务端编解码
// 通常我们需要处理编解码的常见还有文件的读写,网络请求的处理,这里根据网络请求的例子,介绍如何在服务端进行编解码
// 假设我们运行了如下的http服务,监听来自客户端的请求,客户端传输数据时采用了gbk的编码,而服务器默认采用utf8的解码
// 如果此时服务端采用默认的utf8,对请求进行解码,就会出现乱码,因为需要特殊的处理
const http = require("http");
const iconv = require("iconv-lite");
const server = http.createServer((req, res) => {
let chunks = [];
req.on("data", (data) => {
chunks.push(data);
});
req.on("end", () => {
chunks = Buffer.concat(chunks);
const body = iconv.decode(chunks, "gbk");
console.log(body); // 如果是 gbk 就可以获取 你 , 若是 utf8 获取到的就是 浣�
res.end("HELLO FROM SERVER");
});
});
server.listen(3000);
const charset = "utf8";
const reqBuff = iconv.encode("你", charset);
const options = {
hostname: "127.0.0.1",
port: "3000",
method: "POST",
headers: {
"Content-Type": "text/plain",
"Content-Encoding": "identity",
Charset: charset, // 设置请求字符集编码
},
};
const client = http.request(options, (res, err) => {
res.pipe(process.stdout); // HELLO FROM SERVER
});
client.end(reqBuff);
相关链接
Nodejs学习笔记
https://github.com/chyingp/nodejs-learning-guide
iconv-lite
https://github.com/ashtuchkin/iconv-lite
MD5 入门介绍及 crypto 模块的应用
// MD5(Message-Digest Algorithm)是计算机安全领域广泛使用散列函数(又称哈希算法,摘要算法),主要用来确保信息的完整和一致性,常见的应用场景有密码保护,下载文件校验等
// 本文先对MD5的特征与应用进行简要概述,接着重点介绍MD5在密码保护场景下的应用,最后通过例子对MD5碰撞进行简单介绍
const crypto = require("crypto");
const md5 = crypto.createHash("md5");
特点
1. 运行速度快: 对jQuery.js求md5值,57254个字符,耗时1.907ms
2. 输出长度固定: 输入长度不固定,输出长度固定(128位)
3. 运行不可逆: 已知运算结果的情况下,无法通过逆运算得到原始字符串
4. 高度离散: 输入的微小变化,可导致运算结果差异很大
5. 若碰撞性: 不可输入的散列值可能相同
应用场景
1. 文件完整性校验: 比如从网上下载一个软件,一般网站都会将软件的md5值附在网页上,用户下载完软件后,可对下载的本地软件进行md5运算,然后跟网站上的md5值进行对比,确保下载的软件是完整的或者是正确的
2. 密码保护: 将md5后的密码保存到数据库,而不是保存明文密码,避免拖库等事件发生后,明文密码外泄
3. 防篡改: 比如数字证书的防篡改,就用到了摘要算法(当然要结合数字签名等手段)
密码保护
// 前面提到,将明文密码保存到数据库是很不安全的,最不济也要进行md5后进行保存,比如用户密码是123456 md5运行后 得到 输出:e10adc3949ba59abbe56e057f20f883e
// 这样至少有两个好处
// 1. 防止内部攻击: 网站主任也不知道用户的明文密码,避免网站主人获取到明文密码
// 2. 防止外部攻击: 如果网站被黑客入侵,黑客也只能拿到md5后的密码,而不是用户的明文密码
// 示例代码如下
function cryptpwd(password) {
return md5.update(password).digest("hex");
}
const password = "123456";
const cryptPassword = cryptpwd(password);
console.log(cryptPassword); // e10adc3949ba59abbe56e057f20f883e
单纯对密码进行 md5 不安全
// 前面提到,通过对用户密码进行 md5 运算来提高安全性,但实际上,这样的安全性很差,为什么呢?
// 稍微修改下上面的例子,可能就明白了,相同的明文密码,md5的值也是相同的
function cryptpwd(password) {
return md5.update(password).digest("hex");
}
const password = "123456";
const cryptPassword = cryptpwd(password);
console.log(cryptPassword); // e10adc3949ba59abbe56e057f20f883e
console.log(cryptPassword); // e10adc3949ba59abbe56e057f20f883e
// 也就是说,当攻击者知道算法是 md5,且数据库里面存储的密码是 'e10adc3949ba59abbe56e057f20f883e' 的时候,理论上就可以猜到用户的明文密码是 123456
// 事实上,彩虹表就是这样进行包里的破解的,事先将常见明文密码的md5值运算好存起来,然后跟网站数据库里面存储的密码进行匹配,就能够快读找到用户的明文密码
// 那么有什么办法可以进一步提升安全性呢,就是密码加盐
密码加盐
// 加盐这个词看上去很玄乎,其实原理很简单,就是在密码的特定位置插入特定的字符后,在对修改后的字符串进行md5运算
// 例子如下,同样的密码,当 盐 值不一样的时候,md5值就有很大的差异,通过密码加盐,可以防止最初级的暴力破解,如果攻击者时间不知道 盐值, 暴力破解的难度就会非常大
function cryptpwd(password, salt) {
const saltPassword = password + ":" + salt;
// 密码 加盐
console.log("原始密码: %s", password);
console.log("加盐后的密码: %s", saltPassword);
// 加盐密码的md5值
const result = md5.update(saltPassword).digest("hex");
console.log("加盐密码的md5值: %s", result);
}
// cryptpwd("123456", "abc");
// 原始密码: 123456
// 加盐后的密码: 123456:abc
// 加盐密码的md5值: 51011af1892f59e74baf61f3d4389092
cryptpwd("123456", "bcd");
// 原始密码: 123456
// 加盐后的密码: 123456:bcd
// 加盐密码的md5值: 55a95bcb6bfbaef6906dbbd264ab4531
密码加盐: 随机盐数
// 通过密码加盐,密码的安全性已经提高了不少,但是其实上面的例子存在不少问题
// 1. 短盐值: 需要穷举的可能性较小,容易暴力破解,一般采用长盐值来解决
// 2. 盐值固定: 类似的,攻击者只需要把常用密码+盐值的hash值表算出来,就能推算出来密码
// 短盐值自不必说,可以避免,对于为什么不使用固定盐值,我们需要多解释一下,很多时候,我们的盐值是硬编码到我们代码里面的(比如配置文件),一旦坏人通过使用某种手段获知盐值,那么只需要针对这串固定的盐值进行暴力的穷举就可以了
// 比如我们上面的代码,当你知道盐值是abc的时候,立刻就能猜到, 51011af1892f59e74baf61f3d4389092 对应的明文密码是 123456
// 那么需要我们怎么优化呢;就是随机盐值
// 实例代码如下,可以看到,密码同样是123456,由于采用了随机盐值,前后运算得出的结果是不相同的,这样带来的好处就是多个用户,同样的密码,攻击者需要进行多次运算才可以完全破解,同样是纯数字的三位短盐值,随机盐值破解的运算量是固定盐值的1000倍
function getRandomSlat(params) {
return Math.random().toString().slice(2, 5);
}
function cryptpwd(password, salt) {
// 密码加盐
const saltPassword = password + ":" + salt;
console.log("原始密码 %s", password);
console.log("加盐后的密码: %s", saltPassword);
// 加盐密码的md5值
const result = md5.update(saltPassword).digest("hex");
console.log("加盐密码的md5值: %s", result);
}
const password = "123456";
cryptpwd(password, getRandomSlat());
// 原始密码 123456
// 加盐后的密码: 123456:871
// 加盐密码的md5值: 5d2a956275532e26128ef8b6cf2a6db6
cryptpwd(password, getRandomSlat());
// 原始密码 123456
// 加盐后的密码: 123456:385
// 加盐密码的md5值: 1189580fe0d143f65a6d3e82903281c3
MD5 碰撞
// 简单来说,就是两段不同的字符串,经过MD5运算后,得到相同的结果
// 网上有不少例子,这里就不赘述,直接上例子,参考(这里)[http://www.mscs.dal.ca/~selinger/md5collision/]
function getHashResult(hexString) {
// 转成16进制,如 0x4d 0xc9
hexString = hexString.replace(/(\w{2,2})/g, "0x$1 ").trim();
console.log("hexString--->", hexString);
// hexString---> 0xd1 0x31 0xdd 0x02 0xc5 0xe6 0xee 0xc4 0x69 0x3d 0x9a 0x06 0x98 0xaf 0xf9 0x5c 0x2f 0xca 0xb5 0x87 0x12 0x46 0x7e 0xab 0x40 0x04 0x58 0x3e 0xb8 0xfb 0x7f 0x89.....
// 转成16进制的数组 如 [0x4d, 0xc9, ...]
const arr = hexString.split(" ");
console.log("arr--->", arr);
// arr---> [
// '0xd1', '0x31', '0xdd', '0x02', '0xc5', '0xe6', '0xee',
// '0xc4', '0x69', '0x3d', '0x9a', '0x06', '0x98', '0xaf', ......
// 转成对应的buffer 如:<Buffer 4d c9 ...>
const buff = Buffer.from(arr);
console.log("buff--->", buff);
// buff---> <Buffer d1 31 dd 02 c5 e6 ee c4 69 3d 9a 06 98 af f9 5c 2f ca b5 07 12 46 7e ab 40 04 58 3e b8 fb 7f 89 55 ad 34 06 09 f4 b3 02 83 e4 88 83 25 f1 41 5a 08 51 ... 78 more bytes>
const hash = crypto.createHash("md5");
// 计算md5值
const result = hash.update(buff).digest("hex");
return result;
}
const str1 =
"d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70";
const str2 =
"d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70";
const result1 = getHashResult(str1);
const result2 = getHashResult(str2);
if (result1 == result2) {
console.log("获的了相同的md5值--->", result1); // 获的了相同的md5值 79054025255fb1a26e4bc422aef54eb4
} else {
console.log("获取了不同的md值");
}
相关链接
MD5碰撞的一些例子
http://www.jianshu.com/p/c9089fd5b1ba
MD5 Collision Demo
http://www.mscs.dal.ca/~selinger/md5collision/
Free Password Hash Cracker
https://crackstation.net/
Express 常用中间件 body-parser 实现解析
// body-parser 是非常常用的一个 express 中间件,作用是对http请求体进行解析,使用非常简单,以下两行代码已经覆盖了大部分的是使用场景
// app.use(bodyParser.json());
// app.use(bodyParser.urlencoded({ extended: false }));
// 本文从简单的例子出发,探究 bodyParser 的内部实现,至于 bodyParser如何使用,可以自行查阅 https://github.com/expressjs/body-parser/
入门基础
// 在正式的讲解之前,我们可以看下POST请求的报文
POST /test HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: text/plain; charset=utf8
Content-Encoding: gzip
LGQ
// 其中需要我们注意的有 content-type content-Encoding 已经报文主体
// content-type: 请求报文的主体类型,编码,常见的类型有 text/plain application/json application/x-www-form-urlencoded,常见的编码有 utf-8 gbk等
// content-Encoding: 声明报文主体的压缩格式,常见的取值有 gzip deflare identity
// 报文主体: 这里是个普通的文本字符串, 'LGQ'
使用例子
// 不使用 jsonParser 中间件
function demo1(params) {
const express = require("express");
const app = express();
app.post("/login", (req, res) => {
console.log(req.body);
res.end();
});
app.listen(3000);
}
// 直接访问 http://127.0.0.1:3000/login 没有jsonParser中间件
// 参数为json {"method":"global.login"} 接收到的 req.body 为 undefined
// demo1();
// 使用 jsonParser 中间件
function demo2(params) {
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
// create application/json parser
const jsonParser = bodyParser.json();
app.post("/login", jsonParser, (req, res) => {
console.log(req.body);
res.end();
});
app.listen(3000);
}
// 访问 http://127.0.0.1:3000/login 有jsonParser中间件
// 参数为json {"method":"global.login"} 接收到的 req.body 为 { method: 'global.login' }
// demo2();
// 不使用 urlencodedParser 中间件
function demo3(params) {
const express = require("express");
const app = express();
app.post("/login", (req, res) => {
console.log(req.body);
res.end();
});
app.listen(3000);
}
// 访问 http://127.0.0.1:3000/login 没有urlencodedParser中间件
// x-www-form-urlencoded参数为 name LGQ; value: Dahuatech 接收到的 req.body 为 undefined
// demo3();
// 使用 urlencodedParser 中间件
function demo4(params) {
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const urlencodedParser = bodyParser.urlencoded({ extended: false });
app.post("/login", urlencodedParser, (req, res) => {
console.log(req.body);
res.end();
});
app.listen(3000);
}
// 访问 http://127.0.0.1:3000/login 没有urlencodedParser中间件
// x-www-form-urlencoded参数为name LGQ; value: Dahuatech 接收到的 req.body 为 [Object: null prototype] { name: 'LGQ', value: 'Dahuatech' }
demo4();
body-parser 主要做了什么
// body-parser实现的要点如下
// 1. 处理不同类型的请求体,比如 text json urlencoded等,对应的报文主体的格式不同
// 2. 处理不同的编码,比如 utf-8 gbk 等
// 3. 处理不同的压缩类型,比如 gzip deflare 等
// 4. 其他边界,异常的处理
一, 处理不同类型的请求
解析 text/plain
// 客户端请求的代码如下,采用默的编码,不对请求体进行压缩,请求的类型为 text/plain
const http = require("http");
// 服务端代码
const parsePostBody = function (req, done) {
let arr = [];
let chunks;
req.on("data", (buff) => {
arr.push(buff);
});
req.on("end", () => {
chunks = Buffer.concat(arr);
done(chunks);
});
};
const server = http.createServer((req, res) => {
parsePostBody(req, (chunks) => {
const body = chunks.toString();
res.end("your nick is: " + body);
});
});
server.listen(3000);
// 客户端代码
const options = {
hostname: "127.0.0.1",
port: "3000",
path: "/test",
method: "POST",
headers: {
"Content-Type": "text/plain",
"Content-Encoding": "identity",
},
};
const client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end("LGQ"); // your nick is: LGQ
解析 application/json
// 服务端代码如下, 相比较 text/plain 只是多了个 JSON.parse() 的过程
const http = require("http");
// 服务端代码
const parsePostBody = function (req, done) {
let arr = [];
let chunks;
req.on("data", (buff) => {
arr.push(buff);
});
req.on("end", () => {
chunks = Buffer.concat(arr);
done(chunks);
});
};
const server = http.createServer((req, res) => {
parsePostBody(req, (chunks) => {
const body = JSON.parse(chunks.toString());
res.end("your nick is: " + body.nick); // your nick is: LGQ
});
});
server.listen(3000);
// 客户端代码
const options = {
hostname: "127.0.0.1",
port: "3000",
path: "/test",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Encoding": "identity",
},
};
const client = http.request(options, (res) => {
res.pipe(process.stdout);
});
const jsonBody = {
nick: "LGQ",
};
client.end(JSON.stringify(jsonBody)); // your nick is: LGQ
解析 application/x-www-form-urlencoded
const http = require("http");
const queryString = require("querystring");
// 服务端代码如下 同样跟 text/plain的解析差不多,就多了个 queryString.parse() 的调用
const parseBody = function (req, done) {
const length = req.headers["content-length"] - 0;
let arr = [];
let chunks;
req.on("data", (buff) => {
arr.push(buff);
});
req.on("end", () => {
chunks = Buffer.concat(arr);
done(chunks);
});
};
const server = http.createServer((req, res) => {
parseBody(req, (chunks) => {
const body = queryString.parse(chunks.toString()); // 关键代码
console.log(body);
res.end("your nick is: " + body.nick);
});
});
server.listen(3000);
// 客户端代码如下
const options = {
port: "3000",
path: "/test",
method: "POST",
headers: {
"Content-Type": "form/x-www-form-urlencoded",
"Content-Encoding": "identity",
},
};
const postBody = { nick: "LGQ" };
const client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end(queryString.stringify(postBody)); // your nick is: LGQ
二, 处理不同的编码
// 很多时候,来自客户的请求,采用的不一定是默认的utf8编码,这个时候,就需要对请求体进行编码处理
// 客户端请求如下,有两个要点
// 1. 编码声明: 在 Content-Type 最后加上 ;charset=bgk
// 2. 请求体编码: 这里借助 iconv-lite, 对请求体进行编码, iconv-encode('程序员',encoding)
const http = require("http");
const contentType = require("content-type");
const iconv = require("iconv-lite");
// 服务端代码
const parseBody = function (req, done) {
const obj = contentType.parse(req.headers["content-type"]);
const charset = obj.parameters.charset; // 编码判断 这里获取到的值是 bgk
let arr = [];
let chunks;
req.on("data", (buff) => {
arr.push(buff);
});
req.on("end", () => {
chunks = Buffer.concat(arr);
const body = iconv.decode(chunks, charset);
done(body);
});
};
const server = http.createServer((req, res) => {
parseBody(req, (body) => {
res.end("your nick is: " + body);
});
});
server.listen(3000);
// 客户端代码
const encoding = "gbk";
const options = {
hostname: "127.0.0.1",
port: "3000",
path: "/test",
method: "POST",
headers: {
"Content-Type": "text/plain; charset=" + encoding,
"Content-Encoding": "identity",
},
};
// 备注: nodejs本身不支持gbk编码, 所以请求发送前, 需要先进性编码;
const buff = iconv.encode("程序员", encoding);
const client = http.request(options, (res) => {
res.pipe(process.stdout);
});
client.end(buff, encoding); // your nick is: 程序员
三, 处理不同的压缩类型
// 这里举个gzip压缩的例子,客户端代码如下,要点如下
// 1. 压缩类型声明: content-encoding 赋值为 gzip
// 2. 请求体压缩: 通过zlib模块对请求体进行gzip压缩
const http = require("http");
const zlib = require("zlib");
// 服务端代码
const parsePostBody = function (req, done) {
const contentEncoding = req.headers["content-encoding"];
let stream = req;
// 关键代码如下
if (contentEncoding === "gzip") {
stream = zlib.createGunzip();
req.pipe(stream);
}
let arr = [];
let chunks;
stream.on("data", (buff) => {
arr.push(buff);
});
stream.on("end", () => {
chunks = Buffer.concat(arr);
done(chunks);
});
};
const server = http.createServer((req, res) => {
parsePostBody(req, (chunks) => {
const body = chunks.toString();
res.end("your nick is: " + body);
});
});
server.listen(3000);
// 客户端代码;
const options = {
hostname: "127.0.0.1",
port: "3000",
path: "/test",
method: "POST",
headers: {
"Content-Type": "text/plain",
"Content-Encoding": "gzip",
},
};
const client = http.request(options, (res) => {
res.pipe(process.stdout);
});
const buff = zlib.gzipSync("LGQ");
client.end(buff); // your nick is: LGQ
写在后面
bodyParser的核心实现并不复杂,翻看源码有发现,更多的代码是处理异常跟边界,
另外,对post请求,还有一个常见的 Content-Type 是 multipart/from-data 这个的处理相对复杂一些,bodyParser不打算对其进行支持
相关链接
https://github.com/expressjs/body-parser/
https://github.com/ashtuchkin/iconv-lite
基于 express+muter 的文件上传
// 图片上传是web开发中常见到的功能,常见的开源组件有 multer formidable等,借助这两个开源组件,可以快速搞定图片上传.
// 本文主要讲解了以下内容,后续张杰会对技术深入挖掘
// 常见例子: 借助express bulter实现单个图片,多个图片上传
// 常见API: 获取上传的图片的信息
// 进阶使用: 自定义保存的图片路径,名称
const multer = require("multer");
环境初始化
非常简单,一行命令。
npm install express multer multer --save
每个示例下面,都有下面两个文件
➜ upload-custom-filename git:(master) ✗ tree -L 1
.
├── app.js # 服务端代码,用来处理文件上传请求
├── form.html # 前端页面,用来上传文件
基础例子 单图上传
const fs = require("fs");
const express = require("express");
const multer = require("multer");
const app = express();
const upload = multer({ dest: "upload/" });
// 单图上传
app.post("/upload", upload.single("logo"), (req, res, next) => {
res.send({ ret_code: "0" });
});
app.get("/form", (req, res, next) => {
const form = fs.readFileSync("./form.html", { encoding: "utf8" });
res.send(form);
});
app.listen(3000);
// html文件
/* <form action="/upload" method="post" enctype="multipart/form-data"> */
// <h2>单图上传</h2>
// <input type="file" name="logo">
// <input type="submit" value="提交">
// </form>
// 访问 http://127.0.0.1:3000/form ,选择图片,点击“提交”,done。然后,你就会看到 upload 目录下多了个图片。
基础例子 多图上传
// 将前面的 upload.single('logo') 改成 upload.array('logo', 2) 就行。表示:同时支持2张图片上传,并且 name 属性为 logo。
const fs = require("fs");
const express = require("express");
const multer = require("multer");
const app = express();
const upload = multer({ dest: "upload/" });
// 单图上传
app.post("/upload", upload.array("logo", 2), (req, res, next) => {
res.send({ ret_code: "0" });
});
app.get("/form", (req, res, next) => {
const form = fs.readFileSync("./form.html", { encoding: "utf8" });
res.send(form);
});
app.listen(3000);
// html文件
/* <form action="/upload" method="post" enctype="multipart/form-data"> */
// <h2>单图上传</h2>
// <input type="file" name="logo">
// <input type="file" name="logo">
// <input type="submit" value="提交">
// </form>
获取上传的图片的信息
var fs = require("fs");
var express = require("express");
var multer = require("multer");
var app = express();
var upload = multer({ dest: "upload/" });
// 单图上传
app.post("/upload", upload.single("logo"), function (req, res, next) {
var file = req.file;
console.log("文件类型:%s", file.mimetype);
console.log("原始文件名:%s", file.originalname);
console.log("文件大小:%s", file.size);
console.log("文件保存路径:%s", file.path);
// 文件类型:image/png
// 原始文件名:logo.png
// 文件大小:18068
// 文件保存路径:upload\b19d667d0afe2035a7696942c743a0d8
res.send({ ret_code: "0" });
});
app.get("/form", function (req, res, next) {
var form = fs.readFileSync("./form.html", { encoding: "utf8" });
res.send(form);
});
// html文件
/* <form action="/upload" method="post" enctype="multipart/form-data"> */
// <h2>单图上传</h2>
// <input type="file" name="logo">
// <input type="submit" value="提交">
// </form>
app.listen(3000);
自定义文件上传路径,名称
自定义本地保存的路径
// 比如我们想要将文件上传到 my-upload 目录下面,修改下 dest 配置项就可以了
const upload = multer({ dest: "upload/" });
// 在上面的配置上,所有资源都是保存在同一个目录下,有时候我们需要针对不同文件进行个性化设置,可以参考下一小节的内容
自定义本地保存的文件名
// multer提供了 storage 这个参数来对资源的保存路径,文件名进行个性化设置
// 使用注意事项:
// destination:设置资源的保存路径,注意,如果没有这个配置项,默认会保存在 /tmp/uploads 下,此外路径需要自己创建
// filename: 设置资源保存在本地的文件名
const fs = require("fs");
const express = require("express");
const multer = require("multer");
const app = express();
const createFolder = function (folder) {
try {
fs.accessSync(folder);
} catch (error) {
fs.mkdirSync(folder);
}
};
const uploadFolder = "./upload/";
createFolder(uploadFolder);
// 通过filename属性定制
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadFolder); // 保存的路径,备注:需要自己创建
},
filename: (req, file, cb) => {
cb(null, file.fieldname + "-" + Date.now());
},
});
// 通过 storage 选项来对 上传行为 进行定制化
const upload = multer({ storage: storage });
app.post("/upload", upload.single("logo"), (req, res) => {
var file = req.file;
res.send({ ret_code: "0" });
});
app.get("/form", function (req, res, next) {
var form = fs.readFileSync("./form.html", { encoding: "utf8" });
res.send(form);
});
// html文件
/* <form action="/upload" method="post" enctype="multipart/form-data"> */
// <h2>单图上传</h2>
// <input type="file" name="logo">
// <input type="submit" value="提交">
// </form>
app.listen(3000);
相关链接
multer官方文档:https://github.com/expressjs/multer
将图片转成 datauri
// 1. 读取图片二进制数据 --> 2. 转成base64字符串 -> 3. 转成datauri
// 关于base64的介绍,可以参考阮一峰的文章 http://www.ruanyifeng.com/blog/2008/06/base64.html
// datauri的格式为: >data:[<mediatype>][;base64],<data>
// 可以看到png图片,大概如下,其中xxx就是前面的base64字符串,接下来,我们看下在nodejs里面该如何实现
// >data: image/png;base64, xxx
const fs = require("fs");
const filepath = "./1.png";
const bData = fs.readFileSync(filepath);
console.log(bData); // <Buffer 89 50 4e 47 0d 0a 1a 0a ... 18018 more bytes>
// 然后将二进制数据转换成base64字符串
const base64Str = bData.toString("base64");
console.log(base64Str); // iVBORw0KGgoAAAANSUhEUgAABmQAAAMm..
// 最后转成datauri的格式
const datauri = "data:image/png;base64," + base64Str;
console.log(datauri); // data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABmQAAAMm...
express+session 实现简易身份认证
// 文本基于 express express-session 实现简单的登录和登出功能,完整的代码示例
环境初始化
// 首先,初始化项目
// express -e
// 然后,安装依赖。
// npm install
// 接着,安装session相关的包。
// npm install --save express-session session-file-store
session 相关配置
const fs = require("fs");
const express = require("express");
const app = express();
const session = require("express-session");
const FileStore = require("session-file-store")(session);
const identityKey = "skey";
app.use(
session({
name: identityKey,
secret: "LGQ", // 用于对session id相关的cookie进行签名
store: new FileStore(), // 本地存储session(文本文件,也可以是其他的store,比如redis等)
saveUninitialized: false, //是否自动保存未初始化的会话,建议是false
resave: false, //是否每次否重新保存会话,建议是false
cookie: {
maxAge: 10 * 1000, //有效期,单位是毫秒
},
})
);
实现登录/登出接口
创建测试账户数据
// 首先在本地创建个文件,来保存可用于登录的账户信息,避免创建链接服务器的繁琐
// user.js
// module.exports = {
// items: [{ name: "LGQ", password: "123456" }],
// };
登录,登出接口实现
// 实现登录和登出接口,其中
// 1. 登录: 如果用户存在,则通过 req.regenerate 创建session,保存到本地,并听过 Set-cookie 将session id 保存到用户侧
// 2. 登出: 销毁session,并清除cookie
const fs = require("fs");
const express = require("express");
const app = express();
const session = require("express-session");
const FileStore = require("session-file-store")(session);
const identityKey = "skey";
const bodyParser = require("body-parser");
const jsonParser = bodyParser.json();
app.use(
session({
name: identityKey,
secret: "LGQ", // 用于对session id相关的cookie进行签名
store: new FileStore(), // 本地存储session(文本文件,也可以是其他的store,比如redis等)
saveUninitialized: false, //是否自动保存未初始化的会话,建议是false
resave: false, //是否每次否重新保存会话,建议是false
cookie: {
maxAge: 10 * 1000, //有效期,单位是毫秒
},
})
);
const users = require("./user").items;
const findUser = (name, password) => {
return users.find((item) => {
return item.name === name && item.password === password;
});
};
// 登录接口
app.post("/login", jsonParser, (req, res, next) => {
const user = findUser(req.body.name, req.body.password);
if (user) {
req.session.regenerate((err) => {
if (err) {
return res.json({ ret_code: 2, ret_msg: "登录失败" });
}
req.session.loginUser = user.name;
res.json({ ret_code: 0, ret_msg: "登录成功" });
});
} else {
res.json({ ret_code: 1, ret_msg: "账号或密码错误" });
}
});
app.post("/logout", (req, res, next) => {
// 备注:这里用 session-file-store 在 destory 方法里,并没有销毁cookie
// 所以客户端的 cookie还是存在的,导致的问题->登出后,服务端检测到cookie,
// 然后去查找对应的session文件,报错
// session-file-store 的问题,本身的bug
req.session.destroy((err) => {
console.log(err);
if (err) {
res.json({ ret_code: 2, ret_msg: "退出登录失败" });
}
res.clearCookie(identityKey);
res.redirect("/");
});
});
app.get("/", (req, res, next) => {
const sess = req.session;
const loginUser = sess.loginUser;
const isLogined = !!loginUser;
res.render("./index.html", {
isLogined: isLogined,
name: loginUser || "",
});
});
app.listen(3000, () => {
console.log("服务已开启");
});
}
登录状态判断
// 用户访问 http://127.0.0.1:3000 时候,判断用户是否是登录,如果是,则调到用户详情界面,如果没有登录,则跳到登录界面
app.get("/", (req, res, next) => {
const sess = req.session;
const loginUser = sess.loginUser;
const isLogined = !!loginUser;
res.render("/login", {
isLogined: isLogined,
name: loginUser || "",
});
});
express+morgan:从入门使用到源码剖析
// morgan是express的默认的日志中间件,也可以脱离 express,作为node.js的日志组件来单独使用,本文主要包括了
// 1. Morgan使用入门案例
// 2. 如何将日志保存到本地文件
// 3. 核心API使用说明及例子
// 4. 进阶使用:1.日志分割,2.将日志写入数据库
// 5. 源码剖析,Morgan的日志格式以及预编译
const morgan = require("morgan");
const { compile } = require("morgan");
入门例子
// 首先初始化项目 npm install express morgan
const express = require("express");
const app = express();
const morgan = require("morgan");
app.use(morgan("short"));
app.use((req, res, next) => {
res.send("OK");
});
app.listen(3000);
// 在浏览器内访问 http://127.0.0.1:3000 打印了如下日志
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 2.810 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.330 ms
将日志打印到本地文件
// morgan支持stream配置项,可以通过它来实现将日志落地的效果,代码如下
const express = require("express");
const app = express();
const morgan = require("morgan");
const fs = require("fs");
const path = require("path");
const accessLogStream = fs.createWriteStream(
path.join(__dirname, "access.log"),
{ flags: "a" } // 'a': 打开文件进行追加。 如果文件不存在,则创建该文件。
);
app.use(morgan("short", { stream: accessLogStream }));
app.use((req, res, next) => {
res.send("OK");
});
app.listen(3000);
// 在浏览器多次访问 http://127.0.0.1:3000
// 生成了access.log文件 文件内容
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 2.936 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.344 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.458 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.195 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.324 ms
使用讲解
核心 API
// morgan的API非常少,使用频率最高的就是 morgan(),作用是返回一个express日志中间件
// morgan(format,options)
// 参数说明如下:
// format: 可选,morgan与定义了几种日志格式,每种格式都有对应的名称,比如 combined short等,默认是 default,不同格式差别可以参考https://github.com/expressjs/morgan/#predefined-formats
// options: 可选,配置项,包含 stream(常用), skip, immediate
// stream: 日志的输出流配置,默认是 process.stdout
// skip: 是否跳过日志记录,使用方法可以参考 https://github.com/expressjs/morgan/#skip
// immediate: 布尔值,默认是false,当为true的时候,一收到请求就记录日志,如果为false,则在请求返回后在记录日志
自定义日志类型
// 首先需要搞清楚 morgan中的两个概念,format 跟 token 非常简单
// format: 日志格式,本质是代表日志格式的字符串,比如 :method :url :status :res[content-length] :response-time ms
// token: format的重要组成部分,比如上面 :method :url 即所谓的token
// 搞清楚 morgan token 的区别后,就可以看下 morgan中,关于自定义日志的关键API
// morgan.format(name, format); // 自定义日志格式
// morgan.token(name, fn); // 自定义token
自定义 format
// 非常简单,首先通过 morgan.format() 定义名为 joke 的日志格式, 然后通过 morgan("joke") 调用即可
const express = require("express");
const app = express();
const morgan = require("morgan");
morgan.format("joke", "[joke] :method :url :status");
app.use(morgan("joke"));
app.use((req, res) => {
res.send("OK");
});
app.listen(3000);
// [joke] GET / 304
// [joke] GET /favicon.ico 304
// [joke] GET /login 200
// [joke] GET /favicon.ico 304
自定义 token
// 代码如下,通过 morgan.token() 自定义token,然后将自定义的token加入自定义的 format中即可
const express = require("express");
const app = express();
const morgan = require("morgan");
// 自定义token from
morgan.token("from", (req, res) => {
return req.query.from || "-";
});
// 自定义 format 其中包含了自定义的token
morgan.format("joke", "[joke] :method :url :status :from");
// 使用自定义的format
app.use(morgan("joke"));
app.use((req, res) => {
res.send("OK");
});
app.listen(3000);
// 在浏览器内运行 http://127.0.0.1:3000/hello?from=app 和 http://127.0.0.1:3000/hello?from=pc
// [joke] GET /hello?from=app 304 app
// [joke] GET /favicon.ico 304 -
// [joke] GET /hello?from=pc 304 pc
// [joke] GET /favicon.ico 304 -
高级使用
日志切割
// 一个线上应用,如果所有的日志都落地到同一个本地文件,时间久了,文件就会变得非常大,不仅影响性能,也不便于查看,这时候,就需要日志分割了
// 借助 file-stream-rotator插件,可以轻松完成日志分割的工作,除了 file-stream-rotator 相关配置代码,其余跟之前的例子差不多,
const fileStreamRotator = require("file-stream-rotator");
const express = require("express");
const fs = require("fs");
const morgan = require("morgan");
const path = require("path");
const app = express();
const logDirectory = path.join(__dirname, "log");
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);
const accessLogStream = fileStreamRotator.getStream({
date_format: "YYYYMMDD",
filename: path.join(logDirectory, "access-%DATE%.log"),
frequency: "daily",
verbose: false,
});
app.use(morgan("combined", { stream: accessLogStream }));
app.get("/", (req, res) => {
res.send("HELLO WORLD");
});
app.listen(3000);
// 每天会生成一个 assess-20230322 当天的log文件, 将日志写入log文件
日志写入数据库
// 有的时候,我们会有这样的需求,将访问的日志写入数据库里,这种需求常见于需要实时查询统计的日志系统
// 在 morgan 里面,该如何实现呢,从文档上,并没有看到合适的扩展接口,于是查阅了下 morgan 的源码,发现实现起来非常简单
// 回顾下之前日志写入本地文件的例子,最关键的梁行代码如下,通过 stream 指定日志的输出流
// const accessLogStream = fs.createWriteStream(__dirname,'access.log',{flags:'a'})
// app.use(morgan("short", { stream: accessLogStream }));
// 在 morgan 内部,大致实现是这样的
// opts为配置文件
function demo1(params) {
const stream = opts.stream || process.stdout;
const logString = createLogSring(); // 伪代码,根据 format token 定义,生成日志的字符
// 日志
stream.write(logString);
}
// 于是,可以用比较巧妙的方法来实现目的,声明一个带 write 方法的对象,并作为 stream 配置传入
function demo(params) {
const express = require("express");
const morgan = require("morgan");
const app = express();
const dbStream = {
write: (line) => {
saveToDataBase(line); // 伪代码,保存到数据库
// ::ffff:127.0.0.1 - GET / HTTP/1.1 200 2 - 1.980 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.461 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.203 ms
// ::ffff:127.0.0.1 - GET / HTTP/1.1 304 - - 0.321 ms
},
};
app.use(morgan("short", { stream: dbStream }));
app.use((req, res) => {
res.send("ok");
});
app.listen(3000);
}
深入剖析
// morgan的代码非常简洁,从设计上来说,morgan的生命周期包括
// token定义 -> 日志格式定义 -> 日志格式预编译 -> 请求到达/返回 -> 写日志
// 其中,token定义,日志格式定义前面已经讲到了,这里只讲 日志格式预编译 的细节
// 跟模板引擎预编译是一样的,日志格式预编译,也是为了提升性能,源码如下,最关键的代码是 compile(fmt)
function getFormatFunction(name) {
const fmt = morgan[name] || name || morgan.default;
return typeof fmt !== "function" ? compile(fmt) : fmt;
}
// compile() 方法的实现就不细说,着重看下 compile(fmt) 返回的内容
const format = morgan["tiny"];
const fn = morgan.compile(format);
console.log(fn.toString());
// 运行上面的程序,输出内容如下,其中 tokens 其实就是 morgan
// function anonymous(tokens, req, res
// ) {
// "use strict"
// return "" +
// (tokens["method"](req, res) || "-") + " " +
// (tokens["url"](req, res) || "-") + " " +
// (tokens["status"](req, res) || "-") + " " +
// (tokens["res"](req, res, "content-length") || "-") + " - " +
// (tokens["response-time"](req, res) || "-") + " ms"
// }
// 看下 morgan.token() 的定义就很清晰了
function token(name, fn) {
morgan[name] = fn;
return this;
}
相关链接
// https://github.com/expressjs/morgan
express+cookie-parser:签名机制深入剖析
// cookieParser是 express的中间件,用来实现 cookie 的解析,是官方脚手架内置的中间件之一
// 他的使用很简单,但在使用过程中偶尔会遇到一些问题,一般都是因为 express + cookieParser 的签名,验证机制不了解导致
// 本文深入讲解 express + cookieParser 的签名和验证的实现机制,一级 cookie 签名是如何增强网站的安全性的
const express = require("express");
const cookieParser = require("cookie-parser");
入门例子:cookie 设置与解析
// 先从最简单的例子来看 cookieParser 的使用, 下面采用默认的配置.
// cookie设置: 使用 express 的内置方法 resizeBy.cookie
// cookie解析: 使用 cookieParser 中间件
const app = express();
app.use(cookieParser());
app.use((req, res, next) => {
console.log(req.cookies.nick); // 第二次访问 输出 LGQ
next();
});
app.use((req, res, next) => {
res.cookie("nick", "LGQ");
res.end("ok");
});
app.listen(3000);
// 在当前的场景下, cookieParser 中间件大致实现如下
app.use((req, res, next) => {
req.cookies = cookie.parse(req.headers.cookie);
next();
});
进阶例子: cookie 签名和解析
// 处于安全的考虑,我们通常需要对 cookie 进行签名
// 例子改写如下,有两个注意点
// 1. cookieParser 初始化时候,传入 secret 作为签名的密钥
// 2. 设置cookie的时候,将 signed 设置为 true, 表示对 cookie 进行签名
// 3. 获取cookie的时候,可以通过 req.cookies 也可以通过 req.signedCookies 获取
const app = express();
// 初始化中间件,传入的第一个参数为 singed secret
app.use(cookieParser("secret"));
app.use((req, res, next) => {
console.log("cookies", req.cookies); // undefined
console.log("signedCookies", req.signedCookies.nick); // LGQ
next();
});
app.use((req, res, next) => {
// 传入第三个参数, { signed: true} 表示要对 cookie 进行摘要算法
res.cookie("nick", "LGQ", { signed: true });
res.end("OK");
});
app.listen(3000);
// 签名前的cookie值为 LGQ, 签名后的cookie值为 nick=s%3ALGQ.SjLAmv8lWCKXwDHxJKjzE8%2B0jRydXKtPIsCnLcpQ2XY
// 下面来分析下 cookie的签名,解析是如何实现的
cookie 签名,解析实现剖析
// express完成cookie值的签名,cookieParser实现签名cookie的解析,两者公用同一个秘钥
cookie 签名
// express对cookie的设置(包括签名),都是通过 res.cookie 这个方法来实现的
// 精简的代码如下
res.cookie = (name, value, options) => {
const secret = this.req.secret;
const signed = options.signed;
// 如果 options.signed 为 true,则对 cookie 进行签名
if (signed) {
val = "s:" + sign(val, secret);
}
this.append("Set-Cookie", cookie.serialize(name, String(val), options));
return this;
};
// sign 为签名函数,伪代码如下,其实就是吧cookie的原始值,跟hmac后的值拼接起来
function sign(val, secret) {
return val + ":" + hmac(val, secret); // 签名后的cookie包含了原始值
}
// 这里的 secret 是从哪里来的呢? cookieParser初始化的时候传进来的,如下伪代码标识为
const cookieParser = function (secret) {
return function (req, res, next) {
req.secret = secret;
// ...
next();
};
};
app.use(cookieParser("secret"));
签名 cookie 解析
// 知道了cookie签名的机制后,如何解析签名cookie就清除了,这个阶段,中间件主要做了两件事
// 1. 将签名cookie对应的原始值提取出来
// 2. 验证签名cookie是否合法
// str 签名后的cookie,比如 s:nick=s%3ALGQ.SjLAmv8lWCKXwDHxJKjzE8%2B0jRydXKtPIsCnLcpQ2XY
// secret 秘钥,比如 secret
function signedCookie(str, secret) {
// 检查是否 s: 开头,确保只对签过名的cookie解析
if (str.substr(0, 2) !== "s:") {
return str;
}
// 校验签名的值是否合法,如果合法,则返回true,否则返回false
const val = unsign(str.slice(2), secret);
if (val !== false) {
return val;
}
return false;
}
// 判断,提取cookie原始值比较简单,只是 unsign这个方法比较有迷惑性
// 一般只会对签名进行合法校验,并没所谓的反签名
// unsign 的方法代码如下,首先,从传入的cookie中分别提取出原始值A1,签名值B1,用同样的方法对A1签名,得到A2,根据A2 B1是否相等,判断签名是否合法
exports.unsign = (val, secret) => {
const str = val.slice(0, val.lastIndexOf(".")),
mac = exports.sign(str, secret);
return sha1(mac) === sha1(val) ? str : false;
cookie 签名的作用
// 主要是出于安全的考虑,防止cookie被篡改,增强安全性,
// 举个小例子来看下cookie签名是如何实现篡改的
// 基于前面的例子展开,假设网站通过nick这个cookie来区分当前用户到底是谁,在前面例子中,登录用户的cookie中 nick对应的值如下 (decode后的)
// nick=s%3ALGQ.SjLAmv8lWCKXwDHxJKjzE8%2B0jRydXKtPIsCnLcpQ2XY
// 此时有人试图修改这个cookie值,来达到伪造身份的目的,比如修改为xiaoming
// nick=s%3Axiaoming.SjLAmv8lWCKXwDHxJKjzE8%2B0jRydXKtPIsCnLcpQ2XY
// 当网站受到请求的时候,对签名进行解析,发现签名验证不通过,由此判断,cookie是伪造的
// hmac("xiaoming", "secret") !== "uVofnk6k+9mHQpdPlQeOfjM8B5oa6mppny9d+mG9rD0"
签名能够确保安全么?
// 当然不是
// 上个小节的例子,仅通过nick这个cookie的值来判断登录的是哪个用户,这是一个非常糟糕的设计,虽然在秘钥位置的情况下,很难伪造签名cookie的,但原始值相同的情况下,签名也是一样的,这种情况下其实就很容易伪造了
// 虽然开源组件的算法都是公开的,因此秘钥的安全性就成为了关键,要确保秘钥不泄露
小结
// 本文主要对 express + cookieParser 的签名和解析机制进行了介绍,不少类似的总结文章中,把cookie的签名说成加密,这是个常见的错误,
相关链接
// https://github.com/expressjs/cookie-parser
Nodejs 进阶:log4js入门实例
// 对于线上项目来说,日志是非常重要的一环,log4js是使用比较多的日志组件,经常跟 express一起配合使用,本文从入门实例开始,讲解 log4js 的使用以及和 express 进行整合
const log4js = require('log4js')
const logger = log4js.getLogger()
入门例子
// 输出日志如下,包括打印时间, 日志级别, 日志分类, 日志内容
logger.level = 'debug';
logger.debug('hello world')
// [2023-04-04T15:32:51.273] [DEBUG] default - hello world
日志级别
// logger.level = 'info', 表示想要打印的最低级别的日志信息室INFO,也就是说,调用类似 logger.debug() 等等级低于INFO的接口,日志是不会打印出来的
logger.level = 'info'
logger.debug('level: debug') // 不打印
logger.info('level: iofo') // [2023-04-04T16:00:07.396] [INFO] default - level: iofo
logger.error('level: error') // [2023-04-04T15:59:56.974] [ERROR] default - level: error
日志类别
// 除级别外,还可以对日志进行分类, log4js.getLogger(category) 如下所示
const alogger = log4js.getLogger('category-a')
const blogger = log4js.getLogger('category-b')
alogger.level = 'debug'
blogger.level = 'debug'
alogger.info('hello-1')
blogger.info('hello')
alogger.info('hello-2')
// [2023-04-04T16:27:40.190] [INFO] category-a - hello-1
// [2023-04-04T16:27:40.192] [INFO] category-b - hello
// [2023-04-04T16:27:40.193] [INFO] category-a - hello-2
appenders
// appenders 指定日志输出位置,可以勇士配置多个,用 category 进行区分,比如 log4js.getLogger('info') 应用的就是 type 为 dataFIle的配置
// 可以注意到,type为console的配置没有声明category,因此,所有的日志都会打印到控制台
const log4js = require("log4js");
log4js.configure({
appenders: { cheese: { type: "file", filename: "./logs/log.log" } },
categories: { default: { appenders: ["cheese"], level: "info" } }, // info 及以下的不打印
});
const logger = log4js.getLogger("cheese");
logger.trace("Entering cheese testing");
logger.debug("Got cheese.");
logger.info("Cheese is Comté.");
logger.warn("Cheese is quite smelly.");
logger.error("Cheese is too ripe!");
logger.fatal("Cheese was breeding ground for listeria.");
// [2023-04-06T12:06:22.380] [INFO] cheese - Cheese is Comté.
// [2023-04-06T12:06:22.381] [WARN] cheese - Cheese is quite smelly.
// [2023-04-06T12:06:22.381] [ERROR] cheese - Cheese is too ripe!
// [2023-04-06T12:06:22.381] [FATAL] cheese - Cheese was breeding ground for listeria.
调试日志打印:debug模块
// 在node程序开发时候,经常需要打印调试日志,用的比较多的就是debug模块,比如 express 框架中就用到了,下面简单举几个例子进行说明,
// 备注:node在0.11.3版本也加入了 util.debuglog() 用于打印调试日志,使用方法和 debug 模块大同小异
npm install debug
基础例子
const debug = require('debug')('app')
debug('hello')
// $ DEBUG=app node debug-log.js 输入
// app hello +0ms 运行
例子:命名空间
// 当项目程序变得复杂,我们需要对日志进行分析的时候,debug支持命令空间,如下所示
// DEBUG=app,api: 标识同时打印出命名空间为 app api 的调试日志
// DEBUG=a*: 支持通配符,所有命名空间为a开头的调试日志都打印出来
const debug = require('debug')
const appDebug = debug('app')
const apiDebug = debug('api')
appDebug('hello')
apiDebug('hello')
// $ DEBUG=app node debug-log.js
// app hello +0ms
// $ DEBUG=api node debug-log.js
// api hello +0ms
// $ DEBUG=app,api node debug-log.js
// app hello +0ms
// api hello +0ms
// $ DEBUG=a* node debug-log.js
// app hello +0ms
// api hello +0ms
例子: 命名空间排除
// 有的时候,我们想要打印出所有的调试日志,除了个别命名空间下的,这个时候,可以通过 - 来进行排除,如下所示, -account* 表示排除所有以 account 开头的命名空间的调试日志
const debug = require('debug')
const listDebug = debug('app:list')
const profileDebug = debug('app:profile')
const loginDebug = debug('account:login')
listDebug('hello')
profileDebug('hello')
loginDebug('hello')
// $ DEBUG=* node debug-log.js
// app:list hello +0ms
// app:profile hello +0ms
// account:login hello +0ms
// $ DEBUG=*,-account* node debug-log.js
// app:list hello +0ms
// app:profile hello +0ms
例子: 自定义格式化
// debug也支持格式化输出,例子如下
// const debug = require('debug')
// debug('my name is $s', 'LGQ')
const createDebug = require('debug')
createDebug.formatArgs.h = function (v) {
return v.toUpperCase()
}
const debug = createDebug('foo')
debug('my name is %s', 'LGQ')
// $ DEBUG=foo node debug-log.js
// foo my name is LGQ +0ms
Nodejs进阶:crypto模块之理论篇
// 互联网时代,网络的数据量每天都在以惊人的速度增长,同时,各类网络安全问题层出不穷,在信息安全重要性日益凸显的今天,作为一个开发着,需要加强对安全的认识,并通过技术手段增强服务的安全性
// crypto 模块是nodejs 的核心模块之一,他提供了安全相关的功能,如摘要算法,加密,电子签名等,很多初学者对着API列表,不知道如何入手,因此他背后涉及了大量安全领域的知识
// 本文重点讲解API背后的理论知识,主要包括
// 1. 摘要 hash ,基于摘要的消息验证码 HMAC
// 2. 对称加密,非对称加密,电子签名
// 3. 分组加密模式
摘要(hash)
// 摘要:将长度不固定的消息作为输入,通过运行hash函数生成,生成固定长度的输出,这段输出就叫做摘要,通常用来验证信息的完整,未被篡改
// 摘要运算是不可逆的,也就是说,输入固定的情况下,产生固定的输出,但是知道输出的情况下,无法反推出输入
// 伪代码如下
// 常见的摘要算法与对应的输出位数如下
// MD5: 128位
// SHA-1: 160位
// SHA256: 256位
// SHA512: 512位
const crypto = require('crypto')
const md5 = crypto.createHash('md5')
const message = 'hello'
const digest = md5.update(message, 'utf-8').digest('hex')
console.log(digest); // 5d41402abc4b2a76b9719d911017c592 这里输出的是16进制的
// 备注:在各类文章或者文献当中,摘要,hash,散列这几个词会经常出现,导致不少初学者看了一脸懵逼,其实大部分时候指的都是一回事,记住上面对摘要的定义就好
MAC HMAC
// MAC(Message Authentication Code) 消息认证码, 用以保证数据的完整性,运算结果取决于消息本身和秘钥
// MAX可以有很多种不同的实现方式,比如HMAC
// HMAC(Hash-based Message Authentication Code) 可以粗略的理解为带秘钥的hash函数
const crypto = require('crypto')
// 参数一: 摘要函数
// 参数二: 秘钥
let hmac = crypto.createHmac('md5', '123456')
let ret = hmac.update('hello').digest('hex')
console.log(ret); // 9c699d7af73a49247a239cb0dd2f8139
对称加密, 非对称加密
// 加密/解密: 给定明文,通过一定的算法,产生加密后的密文,返过来就是解密
// 秘钥: 为了进一步增强加解密算法的安全性,在加解密的过程中引入了秘钥,秘钥可以视为加解密算法的关键,在已知密文的情况下,如果不知道解密所用的秘钥,则无法将密文解开
// 根据加密解密所用秘钥是否相同,可以将加密算法分为 对称加密 非对称加密
对称加密
// 加密解密用到的秘钥都是相同的
// 常见的对称加密算法有 DES 3DES AES Blowfish RC5 IDEA
// 加解密伪代码
// >encryptedText = encrypt(plainText, key); // 加密
// >plainText = decrypt(encryptedText, key); // 解密
非对称加密
// 又称公开秘钥加密,加解密用到的秘钥是不同的,
// 加密秘钥是公开的,成为公钥,解密秘钥保密,成为秘钥
// 常见的非对称加密算法 RSA DSA Elgamal
// >encryptedText = encrypt(plainText, publicKey); // 加密
// >plainText = decrypt(encryptedText, priviteKey); // 解密
对比与应用
// 除了秘钥的差异还有运算速度的差异,通常来说
// 1. 对称加密速度要快于非对称加密
// 2. 非对称加密通常加密短文本,对称加密通常用于加密长文本
// 两者可以结合起来使用,比如HTTPS协议,可以在握手阶段,通过RSA来交换生成对称秘钥,在之后的通讯阶段,可以使用对称加密算法对数据进行加密,秘钥则是握手阶段生成的
// 备注:对称秘钥交换不一定通过RSA,还可以通过类似DH来完成
数字签名
// 从签名可以猜到,数字签名的用途,主要作为为:
// 1. 确认信息来源于特定的主题
// 2. 确认信息完整,没有被篡改
// 为了达到上述目的,需要有两个过程
// 1. 发送方: 生成签名
// 2. 接收方: 验证签名
发送方生成签名
// 1. 计算原始信息的摘要
// 2. 通过私钥对摘要进行签名,得到电子签名
// 3. 将原始信息,电子签名,发送给接收方
// >digest = hash(message); // 计算摘要
// >digitalSignature = sign(digest, priviteKey); // 计算数字签名
接收方验证签名
// 1. 通过公钥解开电子签名,得到摘要D1(如果解不开,信息来源主题校验失败)
// 2. 计算原始信息的摘要D2
// 3. 对比 D1 D2 如果D1等于D2 说明原始信息完整 未被篡改
// >digest1 = verify(digitalSignature, publicKey); // 获取摘要
// >digest2 = hash(message); // 计算原始信息的摘要
// >digest1 === digest2 // 验证是否相等
对比分对称加密
// 由于RSA算法的特殊性,加密/解密, 签名/验证 看上去特别像,很多同学容易混淆,
// 1. 加密/解密 公钥加密 私钥解密
// 2. 签名/验证 私钥签名,公钥验证
分组加密模式,填充,初始化向量
// 常见的对称加密算法,有AES DES 都采用分组加密模式,这其中,有三个关键的概念需要掌握,模式 填充 初始化向量
// 搞清楚这三点,才会知道 crypto 模块对称加密 API 的参数代表什么含义,出了错知道如何去排查
分组加密模式
// 所谓的分组加密,就是将明文拆分完成固定长度的块,然后对拆分的块按照特定的模式进行加密,
// 常见的分组加密模式有 ECB CBC CFB OFB CTR
// 以最简单的BCB为例,现将信息拆分成等分的模块,然后利用秘钥进行加密
const crypto = require('crypto');
const algorithm = 'aes-128-ecb';
const key = 'mysecretkey123'; // 密钥必须为16字节(128位)
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, key, Buffer.alloc(0));
let encrypted = cipher.update(text, 'utf-8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
function decrypt(encrypted) {
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.alloc(0));
let decrypted = decipher.update(encrypted, 'hex', 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}
// 使用示例
const plaintext = 'Hello World';
const ciphertext = encrypt(plaintext);
console.log(`加密后的文本:${ciphertext}`);
const decryptedtext = decrypt(ciphertext);
console.log(`解密后的文本:${decryptedtext}`);
初始化向量:IV
// 为了增强算法的安全性,部分分组加密模式(CFB OFB CTR)中引入了初始化变量,使得加密的结果随机化,也就是说,对于同一段明文,IV不同,加密的结果不同
// 以CBC为例,每一个数据块,都与前一个加密块进行亦或运算后,在进行加密,对于第一个数据块,则是与IV进行亦或,
// IV的大小跟数据块的大小有关(128位),跟秘钥的长度无关
填充:padding
// 分组加密模式需要对长度固定的块进行加密,分组拆分后,最后一个数据块长度可能小于128位,此时需要进行条虫来满足长度的需求
// 假设分组长度为k字节,最后一个分组长度为 k-last 可以看到
// 1. 不管明文的长度是多少,加密之前都会对明文进行填充,(不然解密函数无法区分最后一个分组是否被填充,因为存在最后一个分组长度的刚好为k的情况)
// 2. 如果最后一个分组长等于 k-last === k ,难么填充内容为一个完成的分组 kkk...kkk(k个字节)
// 3. 如果最后一个分组长度小于 k-last < k 那么填充内容为 k-last mod k
概括来说
// 1. 分组加密: 现将明文切分为固定长度的块(128位),在进行加密
// 2. 分组加密的几种模式: ECB CBC CFB OFB CTR
// 3. 填充(padding):部分加密模式,当最后一个块的长度小于128位时,需要通过特定的方式进行填充。(ECB、CBC需要填充,CFB、OFB、CTR不需要填充)
// 4. 初始化向量(IV):部分加密模式(CFB、OFB、CTR)会将 明文块 与 前一个密文块进行亦或操作。对于第一个明文块,不存在前一个密文块,因此需要提供初始化向量IV(把IV当做第一个明文块 之前的 密文块)。此外,IV也可以让加密结果随机化。
Nodejs进阶:5分钟入门非对称加密用法
机密解密方法
// 加密函数
crypto.publicEncrypt(key, buffer)
// 解密函数
crypto.privateDecrypt(privateKey, buffer)
入门例子
const crypto = require('crypto')
// 加密方法
exports.encrypt = (data, key) => {
// 注意,第二个参数是Buffer类型
return crypto.publicEncrypt(key, Buffer.from(data))
}
exports.decrypt = (encrypted, key) => {
// 注意, encrypted是Buffer类型
return crypto.privateDecrypt(key, encrypted);
}
const utils = require('./utils')
const keys = require('./keys')
const plainText = '你好,我是程序员LGQ'
const crypted = utils.encrypt(plainText, keys.pubKey) // 加密
console.log('crypted', crypted); // crypted <Buffer be 03 79 71 3f 94 f9 01 68 de 7d bf b1 43 78 dc 08 9d da 1d 70 de 96 23 25 24 a0 25 49 63 5b ff df ff fb 64 1d 7e de e2 87 e0 94 5f 0c 64 ff a9 d1 a7 ... 78 more bytes>
const decrypted = utils.decrypt(crypted, keys.privKey); // 解密
console.log(decrypted.toString()); // 你好,我是程序猿小卡
// 附上公钥、私钥 `keys.js`:
// ```javascript
// exports.privKey = `-----BEGIN RSA PRIVATE KEY-----
// MIICXQIBAAKBgQDFWnl8fChyKI/Tgo1ILB+IlGr8ZECKnnO8XRDwttBbf5EmG0qV
// 8gs0aGkh649rb75I+tMu2JSNuVj61CncL/7Ct2kAZ6CZZo1vYgtzhlFnxd4V7Ra+
// aIwLZaXT/h3eE+/cFsL4VAJI5wXh4Mq4Vtu7uEjeogAOgXACaIqiFyrk3wIDAQAB
// AoGBAKdrunYlqfY2fNUVAqAAdnvaVOxqa+psw4g/d3iNzjJhBRTLwDl2TZUXImEZ
// QeEFueqVhoROTa/xVg/r3tshiD/QC71EfmPVBjBQJJIvJUbjtZJ/O+L2WxqzSvqe
// wzYaTm6Te3kZeG/cULNMIL+xU7XsUmslbGPAurYmHA1jNKFpAkEA48aUogSv8VFn
// R2QuYmilz20LkCzffK2aq2+9iSz1ZjCvo+iuFt71Y3+etWomzcZCuJ5sn0w7lcSx
// nqyzCFDspQJBAN3O2VdQF3gua0Q5VHmK9AvsoXLmCfRa1RiKuFOtrtC609RfX4DC
// FxDxH09UVu/8Hmdau8t6OFExcBriIYJQwDMCQQCZLjFDDHfuiFo2js8K62mnJ6SB
// H0xlIrND2+/RUuTuBov4ZUC+rM7GTUtEodDazhyM4C4Yq0HfJNp25Zm5XALpAkBG
// atLpO04YI3R+dkzxQUH1PyyKU6m5X9TjM7cNKcikD4wMkjK5p+S2xjYQc1AeZEYq
// vc187dJPRIi4oC3PN1+tAkBuW51/5vBj+zmd73mVcTt28OmSKOX6kU29F0lvEh8I
// oHiLOo285vG5ZtmXiY58tAiPVQXa7eU8hPQHTHWa9qp6
// -----END RSA PRIVATE KEY-----
// `;
// exports.pubKey = `-----BEGIN PUBLIC KEY-----
// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDFWnl8fChyKI/Tgo1ILB+IlGr8
// ZECKnnO8XRDwttBbf5EmG0qV8gs0aGkh649rb75I+tMu2JSNuVj61CncL/7Ct2kA
// Z6CZZo1vYgtzhlFnxd4V7Ra+aIwLZaXT/h3eE+/cFsL4VAJI5wXh4Mq4Vtu7uEje
// ogAOgXACaIqiFyrk3wIDAQAB
// -----END PUBLIC KEY-----
// `;
// ```