Node.js | 青训营笔记

115 阅读15分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 3 天

fs文件系统模块

fs.readFile()

fs.readFile(path, [options], callback)

  • 参数一:必填,文件路径(字符串)
  • 参数二:选填,编码格式
  • 参数三:必填,读取完成后的回调函数
const fs = require('fs')
​
fs.readFile('./files/11.txt', 'utf8', function(err, dataStr) {
  // 如果读取成功,则 err 的值为 null
  // 如果读取失败,则 err 的值为 错误对象,dataStr 的值为 undefined
  if (err) {
    return console.log('读取文件失败!' + err)
  }
  console.log('读取文件成功!' + dataStr)
})

fs.writeFile()

fs.writeFile(path, data, [options], callback)

  • 参数一:必填,文件路径(字符串)
  • 参数二:必填,要写入的内容
  • 参数三:选填,编码格式(默认utf-8)
  • 参数四:必填,读取完成后的回调函数

注意:

※ fs.writeFile() 只能创建文件,不能创建路径

※ fs.writeFile() 重复写入,会覆盖原来的文件

const fs = require('fs')
​
fs.writeFile('./files/3.txt', 'ok123', function(err) {
  // 如果文件写入成功,则 err 的值等于 null
  // 如果文件写入失败,则 err 的值等于一个 错误对象
  if (err) {
    return console.log('文件写入失败!' + err)
  }
  console.log('文件写入成功!')
})

路径拼接错误

文件路径如果是./../这种相对路径,运行时会执行 当前执行node命令所在的目录+path

例:

image-20220929110812931

解决方法:使用__dirname (双下划线)(使用+符号拼接 此方法可能会存在问题)

//__dirname 表示当前文件所处的目录
fs.readFile(__dirname + '/files/1.txt', 'utf8', function(err, dataStr) {})
​
如下会报错(路径多一个./):
fs.readFile(__dirname + './files/1.txt', 'utf8', function(err, dataStr) {})

path路径模块

path.join()

path.join([...paths]) 可以把多个路径片段拼接为完整路径

  • ...paths:路径片段序列
  • 返回值:拼接好的字符串
const path = require('path')
// 注意:  只有../ 会抵消前面的路径
const pathStr = path.join('/a', '/b/c', '../', './d', 'e')
console.log(pathStr)  // \a\b\d\e
​
实际应用:
fs.readFile(path.join(__dirname, './files/1.txt'), 'utf8', function(err, dataStr) {}

path.basename()

path.basename(path, [ext]) 获取路径最后一部问,通常用来获取路径中的文件名

  • path:必填,路径字符串
  • ext:选填:文件扩展名
  • 返回值:路径中最后一部分
const path = require('path')
​
const fpath = '/a/b/c/index.html'
//  完整输出
const fullName = path.basename(fpath)
console.log(fullName)   //index.html
//  不输出后缀
const nameWithoutExt = path.basename(fpath, '.html')
console.log(nameWithoutExt)     //index

path.exename()

path.exename(path) 获取路径中的文件扩展名

  • path:必填,路径字符串
  • 返回值:扩展名字符串
const path = require('path')
​
const fpath = '/a/b/c/index.html'
const fext = path.extname(fpath)
console.log(fext)   // 输出.html

http模块

基本使用

// 1. 导入 http 模块
const http = require('http')
​
// 2. 创建 web 服务器实例
const server = http.createServer()
​
// 3. 为服务器实例绑定 request 事件,监听客户端的请求
server.on('request', (req, res) => {
  // req
  const url = req.url   // req.url 是客户端请求的 URL 地址
  const method = req.method // req.method 是客户端请求的 method 类型
  const str = `Your request url is ${url}, and request method is ${method}`
  console.log(str)
    
  // res
  // 调用 res.setHeader() 方法,设置 Content-Type 响应头,解决中文乱码的问题
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end(str)  // 调用 res.end() 方法,向客户端响应一些内容,并结束这次请求的处理过程
})
​
// 4. 启动服务器
server.listen(8080, () => {  
  console.log('server running at http://127.0.0.1:8080')
})

模块化

使用require引用其他模块,会自动执行模块内代码

exports & module.exports

  • 使用require()得到的永远是module.exports 指向的对象
  • 指向同一个对象,最终向外共享的结果以module.exports 指向的对象为准
  • 注意:为了防止混乱,建议不要同时使用

CommonJS

规定了模块的特性和各模块间如何相互依赖

  • 每个模块内部,module变量代表当前模块。
  • module变量是一个对象,它的exports属性(即 module.exports)是对外的接口。
  • 加载某个模块,其实是加载该模块的module.exports属性。require()方法用于加载模块。

自定义模块加载机制

使用 require() 加载自定义模块时,必须指定以 ./../开头的路径标识符。如果没有指定则 node 会把它当作内置模块或第三方模块进行加载。

在使用 require() 导入自定义模块时,如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下的文件:

  • ① 按照确切的文件名进行加载
  • ② 补全 .js 扩展名进行加载
  • ③ 补全 .json 扩展名进行加载
  • ④ 补全 .node 扩展名进行加载
  • ⑤ 加载失败,终端报错

第三方模块加载机制

如果传递给 require() 的模块标识符不是一个内置模块,也没有以 ./../ 开头,则 Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块。

如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。

例如,假设在 'C:\Users\itheima\project\foo.js' 文件里调用了 require('tools'),则 Node.js 会按以下顺序查找:

  • ① C:\Users\itheima\project\node_modules\tools
  • ② C:\Users\itheima\node_modules\tools
  • ③ C:\Users\node_modules\tools
  • ④ C:\node_modules\tools
  • ⑤ 加载失败,终端报错

目录作为模块时加载机制

当把目录作为模块标识符,传递给 require() 进行加载的时候,有三种加载方式:

  • ① 在被加载的目录下查找 package.json 文件,并寻找 main 属性,作为 require() 加载的入口
  • ② 加载目录下的 index.js 文件
  • ③ 加载失败,报告模块的缺失:Error: Cannot find module 'xxx'

npm包管理

devDependencies节点

开发依赖包,只在开发阶段用到,项目上线之后不会用到

//安装指定包,并记录到 devDependencies节点中
//简写
npm i 包名 -D //包名和-D顺序可以颠倒
//非简写
npm install 包名 --save-dev

dependencies节点

核心依赖包,开发和上线之后都会用到

安装方式:直接安装,不用写-D


切换 npm 的下包镜像源

//查看当前的下包镜像源
npm config get registry
​
//将下包的镜像源切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org///检查镜像源是否下载成功
npm config get registry

nrm快速切换镜像源

//通过npm包管理器,将nrm安装为全局可用的工具
npm i nrm -g//查看所有可用的镜像源
nrm ls//将下包的镜像源切换为taobao镜像
nrm use taobao

express

基本使用

// 1.导入 express
const express = require('express')
// 2.创建 web 服务器
const app = express()
​
// 3.启动服务器
app.listen(80, () => {
    console.log('http://127.0.0.1')
})

GET & POST

监听GET请求

app.get() 方法,可以监听客户端的 GET 请求

app.get('请求url', function(req, res) {/*处理函数*/})
//参数1:客户端请求的URL地址
//参数2︰请求对应的处理函数
//      req:请求对象(包含了与请求相关的属性与方法)
//      res:响应对象(包含了与响应相关的属性与方法)
监听POST请求

app.post() 方法,可以监听客户端的 POST 请求

app.post('请求url', function(req, res) {/*处理函数*/})
//参数1:客户端请求的URL地址
//参数2︰请求对应的处理函数
//      req:请求对象(包含了与请求相关的属性与方法)
//      res:响应对象(包含了与响应相关的属性与方法)
res.send()方法

把处理好的数据发送给客户端

app.get('/', (req, res) => {
  res.send('hello world.')
})
app.post('/', (req, res) => {
  res.send('Post Request.')
})
res.query对象

获取请求路径中以查询字符串形式发送的参数(res.query默认是空对象)

app.get('/', (req, res) => {
  //  请求http://127.0.0.1/?a=1&b=2
  res.send(req.query) //响应{"a":"1","b":"2"}
})
req.params对象

获取请求路径中以 : 形式匹配的动态参数(req.params默认是空对象)

app.get('/user/:id/:name', (req, res) => {
  //  请求http://127.0.0.1/user/666/aaa
  res.send(req.params) //响应{"id":"666","name":"aaa"}
})

express.static()

静态资源托管,创建一个静态资源服务器,外部可以直接访问

app.use(express.static('public'))
/*  
    可以访问public中的文件
    http://localhost/images/bg.jpg
    http://localhost/css/style.css
    http://localhost/js/login.js
*/
注意:Express 在指定的静态目录中查找文件,并对外提供资源的访问路径。
因此,存放静态文件的目录名不会出现在 URL 中。

访问多个时,多次使用即可,访问静态资源文件时,会按照目录添加顺序查找

app.use(express.static('public1'))
app.use(express.static('public2'))

挂载路径前缀

app.use('abc',express.static('public'))
/*  
    现在可以通过带有 /public 前缀地址来访问 public 目录中的文件:
    http://localhost/abc/images/kitten.jpg
    http://localhost/abc/css/style.css
    http://localhost/abc/js/app.js
*/

路由

模块化路由

为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上例如app.get() ,app.post() ,而是推荐将路由抽离为单独的模块。

将路由抽离为单独模块的步骤如下:

  • ①创建路由模块对应的 .js 文件
  • ②调用 express.Router() 函数创建路由对象
  • ③向路由对象上挂载具体的路由
  • ④使用 module.exports 向外共享路由对象
  • ⑤使用 app.use() 函数注册路由模块
// 1. 导入 express
const express = require('express')
// 2. 创建路由对象
const router = express.Router()
​
// 3. 挂载具体的路由
router.get('/user/list', (req, res) => {
  res.send('Get user list.')
})
router.post('/user/add', (req, res) => {
  res.send('Add new user.')
})
// 4. 向外导出路由对象
module.exports = router
使用模块
const express = require('express')
const app = express()
​
// 1. 导入路由模块
const router = require('./my_router')
// 2. 注册路由模块
app.use('/abc', router) /* /abc是挂载路由前缀 */
// 注意: app.use() 函数的作用,就是来注册全局中间件
​
app.listen(80, () => {
  console.log('http://127.0.0.1')
})

中间件

对请求进行预处理

Express 的中间件,本质上是一个 function 处理函数,Express 中间件的格式如下:

image-20220930113916218

注意:中间件函数的形参列表中,必须包含 next 参数。而路由处理函数中只包含 req 和 res。

多个中间件之间,共享同一份 req res。基于这样的特性,我们可以在上游的中间件中,统一为 req 或 res 对象添加自定义的属性或方法,供下游的中间件或路由进行使用。

全局生效

使用 app.use(/*中间件*/) #客户端发起的任何请求,到达服务器后都会触发中间件

方法一:
const mw = (req, res, next) => {
  console.log('调用了全局生效的中间件')   //先定义再注册
  next()
}
app.use(mw)
========
方法二:
app.use(function(req, res, next) => {		//直接在注册的时候声明
  console.log('调用了全局生效的中间件')
  next()
})

局部生效

不使用app.use()

// 1. 定义中间件函数
const mw1 = (req, res, next) => {
  console.log('调用了局部生效的中间件')
  next()
}

// 2. 创建路由
app.get('/', mw1, (req, res) => {		//只在当前生效
  res.send('Home page.')
})
app.get('/user', (req, res) => {
  res.send('User page.')
})


//多个
app.get('/', [mw1, mw2], (req, res) => {})
或
app.get('/', mw1, mw2, (req, res) => {})

中间件分类

应用级别中间件

通过app.use()或app.get()或app.post(),绑定到app实例上的中间件。

路由级别中间件

绑定到express.Router()实例上的中间件,用法和应用级别中间件没有区别。

错误级别的中间件

错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。

格式:错误级别中间件的 function 处理函数中,必须有 4 个形参,形参顺序从前到后,分别是 (err, req, res, next)。

image-20221012095027199

注意:错误级别的中间件, 必须注册在所有路由之后

Express内置的中间件

① express.static 快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)

② express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)

③ express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)

image-20221012095205955

第三方的中间件

非 Express 官方内置的,而是由第三方开发出来的中间件。

例如:在 express@4.16.0 之前的版本中,经常使用 body-parser 这个第三方中间件,来解析请求体数据。使用步骤如下:

①运行 npm install body-parser 安装中间件

②使用 require 导入中间件

③调用 app.use() 注册并使用中间件


自定义中间件

实现步骤:

①定义中间件

②监听 req 的 data 事件

③监听 req 的 end 事件

④使用 querystring 模块解析请求体数据

⑤将解析出来的数据对象挂载为 req.body

⑥将自定义中间件封装为模块

// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 导入 Node.js 内置的 querystring 模块
const qs = require('querystring')
​
// 这是解析表单数据的中间件
app.use((req, res, next) => {
  // 定义中间件具体的业务逻辑
  // 1. 定义一个 str 字符串,专门用来存储客户端发送过来的请求体数据
  let str = ''
  // 2. 监听 req 的 data 事件
  req.on('data', (chunk) => {
    str += chunk
  })
  // 3. 监听 req 的 end 事件
  req.on('end', () => {
    // 在 str 中存放的是完整的请求体数据
    // console.log(str)
    // TODO: 把字符串格式的请求体数据,解析成对象格式
    const body = qs.parse(str)
    req.body = body
    next()
  })
})
​
app.post('/user', (req, res) => {
  res.send(req.body)
})
​
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})

跨域

CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源

浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可以解除浏览器端的跨域访问限制。

image-20221012111753897image-20221012111800066

cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题。

使用步骤分为如下 3 步:

①运行 npm install cors 安装中间件

②使用 const cors = require('cors') 导入中间件

③在路由之前调用 app.use(cors()) 配置中间件

// 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
const cors = require("cors")
app.use(cors())

CORS 响应头部

Access-Control-Allow-Origin

响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:

Access-Control-Allow-Origin: <origin> | *
// origin 参数的值指定了允许访问该资源的外域 URL

例:只允许来自 itcast.cn 的请求

res.setHeader('Access-Control-Allow-Origin','http://itcast.cn')

允许来自任何域的请求

res.setHeader('Access-Control-Allow-Origin','*')

Access-Control-Allow-Headers

默认情况下,CORS 支持客户端向服务器发送如下的 9 个请求头:

Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)

如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!

image-20221012114734765

Access-Control-Allow-Methods

默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。

如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods来指明实际请求所允许使用的 HTTP 方法。

image-20221012114808059


简单请求

同时满足以下两大条件的请求,就属于简单请求:

① 请求方式:GET、POST、HEAD 三者之一

② HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

预检请求

只要符合以下任何一个条件的请求,都需要进行预检请求:

① 请求方式为 GET、POST、HEAD 之外的请求 Method 类型

② 请求头中包含自定义头部字段

③ 向服务器发送了 application/json 格式的数据

在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。

区别:

简单请求的特点:客户端与服务器之间只会发生一次请求。

预检请求的特点:客户端与服务器之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求。


JSONP接口

概念:浏览器端通过

特点

①JSONP 不属于真正的 Ajax 请求,因为它没有使用 XMLHttpRequest 这个对象。

②JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求。

如果项目中已经配置了 CORS 跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。否则 JSONP 接口会被处理成开启了 CORS 的接口。示例代码如下:

image-20221012115057390

实现JSONP接口的步骤

  1. 获取客户端发送过来的回调函数的名字
  2. 得到要通过 JSONP 形式发送给客户端的数据
  3. 根据前两步得到的数据,拼接出一个函数调用的字符串
  4. 把上一步拼接得到的字符串,响应给客户端的

image-20221012115148859

在网页中使用 jQuery 发起 JSONP 请求

调用 $.ajax() 函数,提供 JSONP 的配置选项,从而发起 JSONP 请求

image-20221012115218501


代理

webpack开发配置API代理解决跨域问题-devServer

下文参考链接:segmentfault.com/a/119000001…

一个完整的webpack配置代理代码

设置代理的前提条件: 1、需要使用本地开发插件:webpack-dev-server。 2、webpack-dev-server使用的是http-proxy-middleware来实现跨域代理的。 3、webpack版本: 3.0、4.0亲测有效

一个webpack配置信息:

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://www.baidu.com/',
        pathRewrite: {'^/api' : ''},
        changeOrigin: true,     // target是域名的话,需要这个参数,
        secure: false,          // 设置支持https协议的代理
      },
      '/api2': {
          .....
      }
    }
  }
};
  1. 配置中主要的参数说明

2.1 '/api'

捕获API的标志,如果API中有这个字符串,那么就开始匹配代理, 比如API请求/api/users, 会被代理到请求 www.baidu.com/api/users

2.2 target

代理的API地址,就是需要跨域的API地址。 地址可以是域名,如:http://www.baidu.com 也可以是IP地址:http://127.0.0.1:3000 如果是域名需要额外添加一个参数changeOrigin: true,否则会代理失败。

2.3 pathRewrite

路径重写,也就是说会修改最终请求的API路径。 比如访问的API路径:/api/users, 设置pathRewrite: {'^/api' : ''},后, 最终代理访问的路径:http://www.baidu.com/users, 这个参数的目的是给代理命名后,在访问时把命名删除掉。

2.4 changeOrigin

这个参数可以让target参数是域名。

2.5 secure

secure: false,不检查安全问题。 设置后,可以接受运行在 HTTPS 上,可以使用无效证书的后端服务器


mysql模块

连接数据库

// 1. 导入 mysql 模块
const mysql = require('mysql')
// 2. 建立与 MySQL 数据库的连接关系
const db = mysql.createPool({
  host: '127.0.0.1', // 数据库的 IP 地址
  user: 'root', // 登录数据库的账号
  password: 'root', // 登录数据库的密码
  database: 'data', // 指定要操作哪个数据库
})

const user = { username: 'aaa', password: 'pcc4321' }
// 定义待执行的 SQL 语句
const sqlStr = 'insert into users set ?'
// 执行 SQL 语句
db.query(sqlStr, user, (err, results) => {
  if (err) return console.log(err.message)
  if (results.affectedRows === 1) {
    console.log('插入数据成功')
  }
}) 

const sqlStr = 'delete from users where id=?'
db.query(sqlStr, 5, (err, results) => {
  if (err) return console.log(err.message)
  // 注意:执行 delete 语句之后,结果也是一个对象,也会包含 affectedRows 属性
  if (results.affectedRows === 1) {
    console.log('删除数据成功')
  }
})

const user = { id: 1, username: 'aaaa', password: '0000' }
// 定义 SQL 语句
const sqlStr = 'update users set user_name = ?,pass_word = ? where id=?'
// 执行 SQL 语句
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
  if (err) return console.log(err.message)
  if (results.affectedRows === 1) {
    console.log('更新数据成功')
  }
}) 

const sqlStr = 'select * from users'
db.query(sqlStr, (err, results) => {
  // 查询数据失败
  if (err) return console.log(err.message)
  // 查询数据成功
  // 注意:如果执行的是 select 查询语句,则执行的结果是数组
  console.log(results)
}) 

身份认证

服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。

前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。

服务端渲染推荐使用 Session 认证机制

前后端分离推荐使用 JWT 认证机制

Session认证机制

cookie

Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。

不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动当前域名下所有未过期的 Cookie 一同发送到服务器。

Cookie的几大特性:

①自动发送

②域名独立

③过期时限

4KB 限制

工作原理

image-20221012150316099

1.安装 express-session 中间件

npm install express-session

2.配置express-session中间件

const session = require('express-session')  //导入session中间件
app.use(
  session({
    secret: 'aaa',  //secret属性值可以为任意字符串
    resave: false,  //固定写法
    saveUninitialized: true //固定写法
})
)

3.向session中存数据

app.post('/api/login', (req, res) => {
  // 判断用户提交的登录信息是否正确
  if (req.body.username !== 'admin' || req.body.password !== '000000') {
    return res.send({ status: 1, msg: '登录失败' })
  }
​
  // 将登录成功后的用户信息,保存到 Session 中
  // 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
  req.session.user = req.body // 用户的信息
  req.session.islogin = true // 用户的登录状态
​
  res.send({ status: 0, msg: '登录成功' })
})

4.从session中取数据

// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
    
  // 从 Session 中获取用户的名称,响应给客户端
  if (!req.session.islogin) {
    return res.send({ status: 1, msg: 'fail' })
  }
  res.send({
    status: 0,
    msg: 'success',
    username: req.session.user.username,
  })
    
})

5.清空session

// 退出登录的接口
app.post('/api/logout', (req, res) => {
    
  // 清空 Session 信息
  req.session.destroy()
    
  res.send({
    status: 0,
    msg: '退出登录成功',
  })
})

局限性

Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。

注意:

  • 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
  • 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。

JWT认证机制

JWT(英文全称:JSON Web Token),用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。

工作原理

image-20221012152353067

组成部分

JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。

三者之间使用英文的“.”分隔,格式如下:

Header.Payload.Signature

JWT 字符串的具体示例:

image-20221012152754021

  • Payload 部分是真正的用户信息,它是用户信息经过加密之后生成的字符串。
  • Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性。

1.安装JWT相关的包

npm install jsonwebtoken express-jwt
// jsonwebtoken 用于生成 JWT 字符串
// express-jwt 用于将 JWT 字符串解析还原成 JSON 对象

2.导入JWT相关包

const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

3.定义 secret 密钥

为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,需要专门定义一个用于加密和解密的 secret 密钥:

  • 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 JWT 字符串
  • 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥进行解密
const secretKey = 'xxxxxxxxx'   //密钥就是一个字符串,可以任曦填写

4.登录成功后生成JWT字符串

const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' }) //30s之内有效
//在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的秘钥
// 参数3:配置对象,可以配置当前 token 的有效期// 记住:千万不要把密码加密到 token 字符中

5.将 JWT 字符串还原为 JSON 对象

客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。

此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象:

app.use(expressJWT({ secret: secretKey }).unless({ path: [/^/api//] }))
//  expressJWT({ secret: secretKey } secretKey是自己定义的,用来解析Token
//  .unless({ path: [/^/api//] }) 用来指定哪些接口不需要访问权限

注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上。

6.使用 req.user 获取用户信息

当 express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息了,示例代码如下:

// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
 status: 200,
 message: '获取用户信息成功!',
 data: req.user, // 要发送给客户端的用户信息
})
})

7. 捕获解析 JWT 失败后产生的错误

当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期不合法,会产生一个解析失败的错误,影响项目的正常运行。可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理。

使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误

app.use((err, req, res, next) => {
// 这次错误是由 token 解析失败导致的
if (err.name === 'UnauthorizedError') {
 return res.send({
   status: 401,
   message: '无效的token',
 })
}
res.send({
 status: 500,
 message: '未知的错误',
})
})

常用案例

发请求

客户端
const http = require("http");
//  请求参数
const body = JSON.stringify({
  msg: "hello from client",
});
​
const req = http.request(
  "http://127.0.0.1:3000",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
  },
  (res) => {
    const bufs = [];
    res.on("data", (buf) => {
      bufs.push(buf);
    });
    res.on("end", () => {
      const buf = Buffer.concat(bufs).toString("utf-8");
      const json = JSON.parse(buf);
      console.log("json:", json);
    });
  }
);
​
req.end(body);
​
服务端
const http = require("http");
const server = http.createServer((req, res) => {
  const bufs = [];
  // 监听数据传输
  req.on("data", (buf) => {
    bufs.push(buf);
  });
  // 数据传输结束
  req.on("end", () => {
    const buf = Buffer.concat(bufs).toString("utf-8");
    let msg = "11";
    try {
      const ret = JSON.parse(buf);
      console.log(ret);
      msg = ret.msg;
      const responseJson = {
        msg1: `receive:${msg}`,
        msg2: `receive:${msg}`,
        msg3: `receive:${msg}`,
      };
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify(responseJson));
    } catch (err) {
      res.end("invalid json");
    }
  });
});
​
const port = 3000;
​
server.listen(port, () => {
  console.log("listening on : http://localhost:" + port);
});

静态文件服务

文件结构

--static

--index.html

--static_server.js

const http = require("http");
const fs = require("fs");
const path = require("path");
const url = require("url");
​
// 静态文件位置(__dirname当前文件位置)
const folderPath = path.resolve(__dirname, "./static");
​
const server = http.createServer((req, res) => {
  // expected http:127.0.0.1:3000/index.html
  const info = url.parse(req.url);
​
  // static/index.html
  const filepath = path.resolve(folderPath, "." + info.path);
​
  // stream api..
  const filestream = fs.createReadStream(filepath);
  filestream.pipe(res);
});
​
const port = 3000;
​
server.listen(port, () => {
  console.log("listening on http://localhost:" + port);
});