nodejs教程含ES6模块化+npm+Express+MySQL+前后端分离+身份认证

目录

 一、Node.js基础

1.使用node.js运行JS文件

 安装nodemon

 2.fs模块

使用fs.readFile()读取文件内容

 使用fs.writeFile()写入文件内容

 案例练习——考试成绩整理

 fs模块路径动态拼接的问题 __dirname

 其它运用

3.path路径模块

路径拼接 path.join() 

获取路径中文件名 path.basename() 

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

4. http 模块

 创建一个基础WEB服务器

 实现简陋路由效果

5.模块化

 模块分类

 模块作用域

 module对象

 模块化规范

 模块的加载机制

 6.包

 从哪里搜索包

 下载包与包管理工具npm

 安装包

 包的语义化版本规范

 包管理配置文件

规范包的结构

 二、Express框架

 1.基本使用

 安装Express

 创建WEB服务器,监听客户请求

2.托管静态资源

 3.路由

 4.中间件

 全局中间件

 局部中间件

 中间件分类

5.CORS跨域

CORS 中间件解决跨域

 CORS 常见响应头解决跨域

 CORS 请求分类

6.编写api接口

三、在项目中操作MySQL

配置 MySQL 模块

MySQL的增删改查

 四、前后端分离和身份验证

 1. Web 开发模式

 服务端渲染的 Web 开发模式

前后端分离的 Web 开发模式

 如何选择

 2. 身份认证

 不同开发模式的身份验证

 Session 认证机制

JWT 认证机制


 一、Node.js基础

Node.js 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境

 Node.js的安装

1.使用node.js运行JS文件

node xxxx.js

 

  •  安装nodemon

 在终端中,运行如下命令,即可将nodemon安装为全局可用的工具

npm install -g nodemon

 

 2.fs模块

  • 使用fs.readFile()读取文件内容

可以判断err对象是否为null,从而判断文件是否读取成功

  •  使用fs.writeFile()写入文件内容

可以判断err对象是否为null,从而判断文件是否写入成功

  •  案例练习——考试成绩整理

 

 

 

  •  fs模块路径动态拼接的问题 __dirname

在使用 fs 模块操作文件时,如果提供的操作路径是以 ./ 或 ../ 开头的相对路径时,容易出现路径动态拼接错误的问题

原因:代码在运行的时候,会以执行 node 命令时所处的目录,动态拼接出被操作文件的完整路径

解决方案:在使用 fs 模块操作文件时,直接提供完整的路径,从而防止路径动态拼接的问题

__dirname 获取当前文件所处的绝对路径

 

  •  其它运用

验证路径是否存在:

  • fs.exists(path, callback)
  • fs.existsSync(path)

获取文件信息:

  • fs.stat(path, callback)
  • fs.stat(path)

删除文件:

  • fs.unlink(path, callback)
  • fs.unlinkSync(path)

列出文件:

  • fs.readdir(path[,options], callback)
  • fs.readdirSync(path[, options])

截断文件:

  • fs.truncate(path, len, callback)
  • fs.truncateSync(path, len)

建立目录:

  • fs.mkdir(path[, mode], callback)
  • fs.mkdirSync(path[, mode])

删除目录:

  • fs.rmdir(path, callback)
  • fs.rmdirSync(path)

重命名文件和目录:

  • fs.rename(oldPath, newPath, callback)
  • fs.renameSync(oldPath, newPath)

监视文件更改:

  • fs.watchFile(filename[, options], listener)

3.path路径模块

 path 模块是 Node.js 官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求

  • 路径拼接 path.join() 

const path = require('path')
const fs = require('fs')

// 注意 ../ 会抵消前面的路径
// ./ 会被忽略
const pathStr = path.join('/a', '/b/c', '../../', './d', 'e')
console.log(pathStr) // \a\d\e

fs.readFile(path.join(__dirname, './files/1.txt'), 'utf8', function (err, dataStr) {
  if (err) {
    return console.log(err.message)
  }
  console.log(dataStr)
})
  • 获取路径中文件名 path.basename() 

  • path: 文件路径
  • ext: 文件扩展名
path.basename(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.extname() 

const path = require('path')

const fpath = '/a/b/c/index.html'

const fext = path.extname(fpath)
console.log(fext) // .html

4. http 模块

http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。

  •  创建一个基础WEB服务器

const http = require('http')

// 创建 web 服务器实例
const server = http.createServer()

// 为服务器实例绑定 request 事件,监听客户端的请求
server.on('request', function (req, res) {
  const url = req.url
  const method = req.method
  const str = `Your request url is ${url}, and request method is ${method}`
  console.log(str)

  // 设置 Content-Type 响应头,解决中文乱码的问题
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  // 向客户端响应内容
  res.end(str)
})

server.listen(8080, function () {
  console.log('server running at http://127.0.0.1:8080')
})
  •  实现简陋路由效果

const http = require('http')
const server = http.createServer()

server.on('request', (req, res) => {
  const url = req.url
  // 设置默认的响应内容为 404 Not found
  let content = '<h1>404 Not found!</h1>'
  // 判断用户请求的是否为 / 或 /index.html 首页
  // 判断用户请求的是否为 /about.html 关于页面
  if (url === '/' || url === '/index.html') {
    content = '<h1>首页</h1>'
  } else if (url === '/about.html') {
    content = '<h1>关于页面</h1>'
  }

  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end(content)
})

server.listen(80, () => {
  console.log('server running at http://127.0.0.1')
})

5.模块化

  • 模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程,模块是可组合、分解和更换的单元。
  • 模块化可提高代码的复用性可维护性实现按需加载
  • 模块化规范是对代码进行模块化拆分和组合时需要遵守的规则,如使用何种语法格式引用模块和向外暴露成员。
  •  模块分类

 

  •  注意:使用require()方法加载其他模块时,会执行被加载模块中的代码。加载自定义模块时,路径要以 ./ 或 ../ 开头,否则会作为内置模块或第三方模块加载。在加载自定义模块期间可以省略.js的后缀名
  •  模块作用域

  • 和函数作用域类似,在自定义模块(.js文件)中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域
  • 防止全局变量污染
  •  module对象

  • 自定义模块中都有一个 module 对象,存储了和当前模块有关的信息
  • 在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用。导入自定义模块时,得到的就是 module.exports 指向的对象(在自定义模块中,默认module.exports是空对象)。
  • 由于module.exports 单词写起来比较复杂,为了简化向外共亨成员的代码,Node 提供了exports对象。默认情况下,exports和module.exporls指向同一个对象。最终共享的结果,还是以module.exports 指向的对象为准。

 通过module.export对象向外共享模块作用域中的成员

易错点 

 

  •  模块化规范

  • Node.js 遵循了CommonJS模块化规范,CommonJS规定了模块的特性和各模块之间如何相互依赖.
  • CommonJS规定:                        

                每个模块内部,module 变量代表当前模块

         module 变量是一个对象,module.exports 是对外的接口

                加载某个模块即加载该模块的 module.exports 属性

  •  模块的加载机制

  1.  模块第一次加载后会被缓存,即多次调用 require() 不会导致模块的代码被执行多次,提高模块加载效率。
  2. 内置模块加载优先级最高。
  3. 导入自定义模块时,若省略文件扩展名,则 Node.js 会按顺序尝试加载文件:

 6.包

  •  Node.js中的第三方模块又叫做,第三方模块和包指的是同一个概念,只是叫法不同
  • 包是由第三方个人或团队开发出来的,免费且开源,不需要付费即可免费下载使用。
  •  从哪里搜索包

https://www.npmjs.com/ 网站上搜索自己所需要的包和该包的使用方法

  •  下载包与包管理工具npm

  1. 我们可以使用包管理工具npm,从https://registry.npmjs.org/服务器上下载自己需要的包
  2. npm会随着Node.js的安装包一起到用户的电脑上,通过npm -v命令查看npm的版本号
  •  安装包

npm install 包的完整名称

 install可以简写成i,如下格式:npm i 包的完整的名称

 安装指定的版本的包,在包名之后通过@符号指定版本,例:npm i moment@2.22.2

 卸载包:npm uninstall 包的完整名称

 注意:初次安装包完成后,在项目文件夹会多出一个叫做node_modules的文件夹和package-lock.json的配置文件

  •  包的语义化版本规范

 

  •  包管理配置文件

 

 多人协作问题

npm提供了一个快捷命令,在执行命令所处的目录中,快速创建package.json文件

npm init -y

 

 

package.json文件下的devDependenciesdependencies节点区别

 上述情况举例,如webpack是记录到devDependencies节点中的

  • 规范包的结构

 

 二、Express框架

Express - 基于 Node.js 平台的 web 应用开发框架 - Express 中文文档 | Express 中文网

基于 Node.js 平台,快速、开放、极简的 Web 开发框架

Express的本质:就是一个npm上的第三方包模块,提供了快速创建Web服务器的方法

 1.基本使用

  •  安装Express

npm i express
  •  创建WEB服务器,监听客户请求

//导入express
const express = require('express')
// 创建 web 服务器实例
const app = express()

// 监听客户端的 GET 和 POST 请求,并向客户端响应具体的内容
//参数1:请求的URL地址,参数2:回调函数,req:请求对象(包含与请求相关的属性与方法)res:响应对象(包含了与响应相关的属性与方法)
//res.send()方法可以把处理好的内容和数据,发送给客户端
app.get('/user', (req, res) => {
  //调用express提供的send()方法,向客户端响应一个JSON对象
  res.send({ name: 'zs', age: 20, gender: '男' })
})
app.post('/user', (req, res) => {
  //调用express提供的send()方法,向客户端响应一个文本字符串
  res.send('请求成功')
})


//req.query 和 req.params 都是获取GET参数,req.body 用于 POST 请求
app.get('/', (req, res) => {
  // req.query默认是一个空对象
  // 通过 req.query 可以获取到客户端发送过来的参数(使用?name=zs&age=20这种字符串的形式)
  // 如:req.query.name  ,  req.query.age
  console.log(req.query)
  res.send(req.query)
})

// 这里的 :id 是一个动态的参数
app.get('/user/:ids/:username', (req, res) => {
  // req.params 是动态匹配到的 URL 参数,默认是一个空对象
  console.log(req.params)
  res.send(req.params)
})


//调用app.listen(端口号,启动成功后的回调函数),启动服务器
app.listen(80, () => {
  console.log('express server running at http://127.0.0.1')
})

2.托管静态资源

  1. 通过 express.static() 方法可创建静态资源服务器,向外开放访问静态资源。
  2. Express 在指定的静态目录中查找文件,并对外提供资源的访问路径,存放静态文件的目录名不会出现在 URL 中
  3. 访问静态资源时,会根据托管顺序查找文件
  4. 可为静态资源访问路径添加前缀
app.use(express.static('public'))
app.use(express.static('files'))
app.use('/b', express.static('bruce'))

/*
可直接访问 public, files 目录下的静态资源,若两个目录含有相同文件名的静态资源,
会根据由上到下来访问,会先去找public下的静态资源,再去找file下的静态资源
http://localhost:3000/images/bg.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/login.js

通过带有 /b 前缀的地址访问 bruce 目录下的文件
http://localhost:8080/bruce/images/logo.png
*/

 3.路由

 

 

  •  模块化路由

 

 1.创建路由模块:

// router.js

// 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

 2.注册和使用路由模块:

//app.js

const express = require('express')
const app = express()

//导入路由模块
const router = require('./router')

// 注册路由模块,(可选择是否添加)添加访问前缀 /api,这里也可以把 router 理解为中间件
app.use('/api', router)

app.listen(80, () => {
  console.log('http://127.0.0.1')
})

 注意:app.use()函数的作用,就是注册全局中间件

 4.中间件

  • 中间件是指流程的中间处理环节(类似后端的过滤器,vue的路由守卫,PHP的中间件)
  • 服务器收到请求后,一般会先调用中间件进行预处理
  • 中间件本质上是一个function处理函数,包含 req, res, next 三个参数,next() 参数把流转关系交给下一个中间件或路由

 注意事项:

  1. 在注册路由之前注册中间件(错误级别中间件除外)
  2. 别忘记调用 next() 函数, next() 函数后别写代码
  3. 多个中间件共享 req、 res对象,基于这个特性,上游的中间件为 req或 res添加自定义的属性或方法,可供下游中间件使用
  •  全局中间件

 

const express = require('express')
const app = express()

//可以定义多个全局中间件,会按照全局中间件定义的先后顺序调用(由上到下)


// 定义第一个全局中间件
app.use((req, res, next) => {
  console.log('调用了第1个全局中间件')
  next()
})
// 定义第二个全局中间件
app.use((req, res, next) => {
  console.log('调用了第2个全局中间件')
  next()
})

app.get('/user', (req, res) => {
  res.send('User page.')
})

app.listen(80, () => {
  console.log('http://127.0.0.1')
})
  •  局部中间件

const express = require('express')
const app = express()

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

const mw2 = (req, res, next) => {
  console.log('调用了第二个局部生效的中间件')
  next()
}

// 两种定义局部中间件的方式
app.get('/hello', mw2, mw1, (req, res) => res.send('hello page.'))
app.get('/about', [mw1, mw2], (req, res) => res.send('about page.'))

app.get('/user', (req, res) => res.send('User page.'))

app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})
  •  中间件分类

 1.应用级别的中间件

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

 2.路由级别的中间件

  • 绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。用法和应用级别中间件没有区别。应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例router.use() 或 router.get() 或 router.post()
const app = express()
const router = express.Router()

router.use(function (req, res, next) {
  console.log(1)
  next()
})

app.use('/', router)

 3.错误级别的中间件

  • 用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
  • 错误级别中间件的处理函数中,必须有 4 个形参,形参顺序从前到后分别是 (err, req, res, next) 。
  • 错误级别的中间件必须注册在所有路由之后
  • 一般不需要加 next() 函数。
const express = require('express')
const app = express()

app.get('/', (req, res) => {
  throw new Error('服务器内部发生了错误!')
  res.send('Home page.')
})

// 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use((err, req, res, next) => {
  console.log('发生了错误!' + err.message)
  res.send('Error:' + err.message)
})

app.listen(80, function () {
  console.log('Express server running at http://127.0.0.1')
})

 4.Express内置中间件

自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:

  • express.static 快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)
  • express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
  • express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)

 4.第三方的中间件

 

5.CORS跨域

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域

  • CORS 中间件解决跨域

  1. CORS 是 Express 的一个第三方中间件,通过以下三步骤
  2. 安装中间件:npm install cors
  3. 导入中间件:const cors = require('cors')
  4. 配置全局中间件:app.use(cors())    在路由之前配置

注意事项:

  • CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
  • CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
  •  CORS 常见响应头解决跨域

   1. Access-Control-Allow-Origin  制定了允许访问资源的外域 URL

res.setHeader('Access-Control-Allow-Origin', 'http://bruceblog.io')
res.setHeader('Access-Control-Allow-Origin', '*')     // *代表允许任何请求

   2. 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 对额外的请求头进行声明,否则这次请求会失败!
//例:允许客户端额外向服务器发送 Content-Type 和 X-Custom-Header 请求头
//注意:多个请求头之间使用逗号隔开
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')

// *代表允许任何额外的请求头
res.setHeader('Access-Control-Allow-Headers', '*')

 3. Access-Control-Allow-Methods

  • 默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
res.setHEader('Access-Control-Allow-Methods', '*')
  •  CORS 请求分类

    1.简单请求

  • 请求方式:GET、POST、HEAD 三者之一
  • HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值 application/x-www-formurlencoded、multipart/form-data、text/plain)

  2.预检请求

主要有以下三种:

  • 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
  • 请求头中包含自定义头部字段
  • 向服务器发送了 application/json 格式的数据

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

 两者区别:

6.编写api接口

创建一个基本的服务器 并 创建API路由模块

//app.js

//导入 express
const express = require('express')
//创建服务器实例
const app = express()

//配置解析url-encoded 格式数据的中间件
app.use(express.urlencoded({ extended: false }))

//一定要在路由之前,配置 cros 中间件,解决跨域问题
const cors = require('cors')
app.use(cors())

//导入路由模块
const router = require('./router')
// 注册路由模块
app.use('/api', router)

//启动服务器
app.listen(80, () => {
  console.log('http://127.0.0.1')
})

编写GET接口

  • req.query 和 req.params 都是获取GET参数,req.body 用于 POST 请求
// router.js

// 1.导入express
const express = require('express')
// 2.创建路由对象
const router = express.Router()


// 3.挂载具体路由
router.get('/user/list', (req, res) => {
  // req.query默认是一个空对象
  // 通过 req.query 可以获取到客户端发送过来的参数(使用?name=zs&age=20这种字符串的形式)
  // 如:req.query.name  ,  req.query.age
  const query = req.query
  //调用 req.send() 方法向客户端响应处理结果
  res.send({
    status: 0, //0 表示处理成功, 1表示处理失败
    msg: 'GET 请求成功!',  //状态的描述
    data: query  //需要响应给客户端的数据
  })

})

// 4.向外导出路由对象
module.exports = router

编写POST接口

// router.js

// 1.导入express
const express = require('express')
// 2.创建路由对象
const router = express.Router()


// 3.挂载具体路由
router.post('/user/list', (req, res) => {
  //通过 req.body 获取请求体中包含的 url-encoded 格式的数据
  const body = req.body
  //调用 req.send() 方法向客户端响应处理结果
  res.send({
    status: 0, //0 表示处理成功, 1表示处理失败
    msg: 'GET 请求成功!',  //状态的描述
    data: body  //需要响应给客户端的数据
  })

})

// 4.向外导出路由对象
module.exports = router

三、在项目中操作MySQL

  • 配置 MySQL 模块

  1. 安装 mysql 模块
npm install mysql

      2. 配置并连接数据库

//1. 导入 MySQL 模块
const mysql = require('mysql')

//2. 建立与数据库的连接
const db = mysql.createPool({
  host: '127.0.0.1',    // 数据库的 IP 地址
  user: 'root',         // 登录数据库的帐号
  password: 'root',     // 登陆数据库的密码
  database: 'test',     // 要连接的数据库名
})

     3. 测试MySQL是否正常工作

//注意:这里的 SQL 语句 'select 1' 没有实际的意义,只是在这里用来测试 MySQL 模块
db.query('select 1', (err, results) => {
  // 如果 mysql 模块工作期间报错
  if (err) return console.log(err.message)
  // 只要能打印出 [ RowDataPacket { '1': 1 } ]  的结果,就证明连接正常
  console.log(results)
})
  • MySQL的增删改查

  1. 查询数据

注意:如果执行的是 select 查询语句,则执行的结果是数组

      2. 插入数据

注意:如果执行的是 insert into 插入语句,则是 results 是一个对象

           可以通过 affectedRows 属性,来判断是否插入成功

 向表中新增数据时,如果数据对象的每个属性数据表的字段一一对应,则可以通过如下方式快速插入数据:

      3. 更新数据

向表中更新数据时,如果数据对象的每个属性数据表的字段一一对应,则可以通过如下方式快速更新数据:

        4.删除数据

在删除数据时,推荐根据 id 这样的唯一标识,来删除对应的数据

 四、前后端分离和身份验证

 1. Web 开发模式

  •  服务端渲染的 Web 开发模式

  • 前后端分离的 Web 开发模式

  •  如何选择

 2. 身份认证

  •  不同开发模式的身份验证

  •  Session 认证机制

    1. Session 的工作原理

    2. 安装 express - session 中间件

npm install express-session

   3. 配置 express - session 中间件

// 1.导入 session 模块
const session = require('express-session')

// 2.配置 session 中间件
app.use(
  session({
    secret: 'niuroufen',        // secret 的值为任意字符串
    resave: false,              // 固定写法
    saveUninitalized: true,     // 固定写法
  })
)

   4. 向 session 存储数据

中间件配置成功后,可通过 req.session 访问 session 对象,存储用户信息

app.post('/api/login', (req, res) => {
  //判断用户提交的登录信息是否正确
  if (req.body.username !== 'admin' || req.body.password !== '000000'){
    return res.send({ status: 1, msg: '登陆失败' })
  }

  req.session.user = req.body      //将用户的信息,存储到 session 中
  req.session.isLogin = true       //将用户的登录状态,存储到 session 中
  
  res.send({ status: 0, msg: '登陆成功' })
})

     5. 从 session 中去数据

// 可以直接从 req.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 })
})

     6. 清空 session 

调用 req.session.destroy() 函数,即可

app.post('/api/logout', (req, res) => {
  // 清空当前客户端的session信息
  req.session.destroy()
  res.send({ status: 0, msg: 'logout done' })
})
  • JWT 认证机制

前后端分离推荐使用 JWT(JSON Web Token)认证机制,是目前最流行的跨域身份认证解决方案。

    1. JWT 的工作原理

用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。

 2. JWT 的组成部分

  • Header(头部)、Payload(有效荷载)、Signature(签名)
  • Payload 是真正的用户信息,它是用户信息加密后的字符串
  • Header 和 Signature 是安全性相关部分,保证 Token 安全性
  • 三者使用 . 分隔

Header.Payload.Signature

// JWT 字符串示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTcsInVzZXJuYW1lIjoiQnJ1Y2UiLCJwYXNzd29yZCI6IiIsIm5pY2tuYW1lIjoiaGVsbG8iLCJlbWFpbCI6InNjdXRAcXEuY29tIiwidXNlcl9waWMiOiIiLCJpYXQiOjE2NDE4NjU3MzEsImV4cCI6MTY0MTkwMTczMX0.bmqzAkNSZgD8IZxRGGyVlVwGl7EGMtWitvjGD-a5U5c

    3. JWT 使用方式

  • 客户端收到服务器的JWT后,会把 JWT 存储在 localStorage 或 sessionStorage 中
  • 此后客户端与服务端通信需要携带 JWT 进行身份认证,将 JWT 存在 HTTP 请求头 Authorization 字段中
  • 加上 Bearer + 空格 前缀
Authorization: Bearer <token>

     4. 在 Express 中使用 JWT

(1)运行如下命令,安装两个 JWT 相关的包

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

 (2)导入刚刚安装的两个 JWT 相关的包

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

 (3)定义 secret 密钥

  • 为保证 JWT 字符串的安全性,防止其在网络传输过程中被破解,需定义用于加密和解密的 secret 密钥
  • 生成 JWT 字符串时,使用密钥加密信息,得到加密好的 JWT 字符串
  • 把 JWT 字符串解析还原成 JSON 对象时,使用密钥解密
// 密钥本质为一个任意字符串
const secretKey = 'Niuroufen'

(4)在登陆成功后生成 JWT 字符串

  • 调用 jsonwebtoken 包提供的 sign() 方法,将用户的信息加密成 JWT 字符串,通过token 属性响应给客户端
app.post('/api/login', (req, res) => {
  // ...省略登陆失败的代码
  res.send({
    status: 200,
    message: '登录成功',
    // jwt.sign() 生成 JWT 字符串
    // 参数:用户信息对象、加密密钥、配置对象-token有效期
    // 尽量不保存敏感信息,因此只有用户名,没有密码
    token: jwt.sign({username: userInfo.username}, secretKey, {expiresIn: '10h'})
  })
})

(5)JWT 字符串还原为 JSON 对象

  • 客户端访问有权限的接口时,需通过请求头的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证,如步骤(3)JWT 使用方式
  • 服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
// 使用 app.use() 来注册中间件
// expressJWT({ secret: secretKey, algorithms: ["HS256"] }) 用来解析 Token 的中间件,解密密钥是 secretKey,algorithms属性,即设置jwt的算法。一般HS256为配置algorithms的默认值
// .unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限

// 注意:只要配置成功了 Express-jwt 这个中间件,就会把解析出来的用户信息,挂载到 req.auth 属性上( 6.x 版本是 req.user 7.x 版本改成 req.auth)
app.use(expressJWT({ secret: secretKey, algorithms: ["HS256"] }).unless({ path: [/^\/api\//] }))

 (6)获取用户信息

  • 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用 req.auth ( 6.x 版本是 req.user 7.x 版本改成 req.auth)对象,来访问从 JWT 字符串中解析出来的用户信息
app.get('/admin/getinfo', (req, res) => {
  // 使用 req.auth 获取用户信息,并通过 data 属性返回给客户端
  console.log(req.auth)
  res.send({
    status: 200,
    message: '获取信息成功',
    data: req.auth,
  })
})

 (7)捕获解析 JWT 失败后的错误

  • 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期不合法,会产生一个解析失败的错误,影响项目的正常运行
  • 通过 Express 的错误中间件,捕获这个错误并进行相关的处理  
app.use((err, req, res, next) => {
  // Token 解析失败导致错误
  if (err.name === 'UnauthorizedError') {
    return res.send({ status: 401, message: '无效的token' })
  }

  // 其他原员导致的错误
  res.send({ status: 500, message: '未知错误' })
})

版权声明:本文为niuroufen5原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>