Node.js 简介
定义:Node.js
是一个跨平台 JavaScript 运行环境
,使开发者可以搭建服务器端的 JavaScript 应用程序
为什么 Node.js 能执行 JS 代码:
- Chrome 浏览器能执行 JS 代码,依靠的是内核中的 V8引擎(即:C++程序)
- Node.js 是基于 Chrome V8 引擎进行封装的
- 注意:Node.js 环境没有 DOM 和 BOM 等内容,但是它也有自己独立的 API
模块化
基本认知
Node.js 中根据模块来源不同,将模块分为了3大类,分别是:
- 内置模块(内置模块是由 Node.js 官方提供的,例如fs、path、http等)
- 自定义模块(用户创建的每一个 .js 文件,都是自定义模块)
- 第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要提前下载)
加载模块
使用强大的 require()
方法,可以加载需要的内置模块、用户自定义模块、第三方模块进行使用(注意:使用 require() 方法加载其他模块时,会执行被加载模块中的代码)
// 加载内置模块
const fs = require('fs')
// 加载用户自定义的模块
const myModule = require('./myModule.js')
// 加载第三方模块
const moment = require('moment')
module 对象
- module 对象
每个自定义的模块中都有一个 module 对象,它里面存储了和当前模块有关的信息(打印结果,如下所示)
Module {
id: '.',
path: 'E:\\Desktop\\jsProject',
exports: {},
filename: 'E:\\Desktop\\jsProject\\test.js',
loaded: false,
children: [],
paths: [
'E:\\Desktop\\jsProject\\node_modules',
'E:\\Desktop\\node_modules',
'E:\\node_modules'
]
}
- module.exports 对象
在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用
外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指向的对象
【myModule.js文件】
module.exports.username = 'Jack'
module.exports.sayHello = () => { console.log('Hello') }
【text.js文件】
const m = require('./myModule')
console.log(m) // { username: 'Jack', sayHello: [Function (anonymous)] }
模块化规范
Node.js 遵循了 CommonJS 模块化规范,CommonJS 规定了模块的特性和各模块之间如何相互依赖
CommonJS 规定:
- 每个模块内部,module 变量代表当前模块
- module 变量是一个对象,它的 exports 属性(即:module.exports)是对外的接口
- 加载某个模块,其实是加载该模块的 module.exports 属性。require() 方法用于加载模块
npm与包
基本介绍
- Node.js 中的第三方模块又叫做包
- npm 公司提供了一个包管理工具,名字叫做 Node Package Manager(简称 npm 包管理工具),这个工具随着 Node.js 的安装被一起安装到了用户的电脑上
- npm 包管理工具能够从 https://registry.npmjs.org/ 服务器把用户需要的包(第三方模块)下载到本地使用
- 可以在终端执行
npm -v
命令,来查看自己电脑上所安装的 npm 包管理工具的版本号 - 如果有需要的话,可以从"全球最大的包共享平台"搜索自己想要的包:https://www.npmjs.com/
npm 体验
目标:通过npm包管理工具,下载 moment 包,并输出格式化时间
- 在终端输入npm命令,下载 moment 包
npm install moment
- 简写
npm i moment
- 指定版本号(不指定版本号,默认安装最新版本的包)
npm i moment@2.22.2
- 输出格式化时间(详细操作,自行查阅官方文档)
// 1. 导入 moment 包
const moment = require('moment')
// 2. 使用包里面的方法
const dt = moment.format('YYYY-MM-DD HH:mm:ss')
console.log(dt) // 输出格式化时间
注意事项
-
初次装包完成后,在项目文件夹下多一个叫做 node_modules 的文件夹和 package-lock.json 的配置文件
-
node_modules 文件夹用来存放所有已安装到项目中的包。require() 导入第三方包时,就是从这个目录中查找并加载包
-
package-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址
-
注意:不要手动修改 node_modules 或 package-lock.json 文件中的任何代码,npm 包管理工具会自动维护它们
-
npm i moment@2.22.2
中的版本号由三位数字组成
每一位数字所代表的含义:
第一位数字:大版本(例如:底层重构)
第二位数字:功能版本(例如:新功能的推出)
第三位数字:Bug修复版本(例如:修复某个Bug)
版本号提升的规则:
只要前面的版本号增长了,后面的版本号"归零"
包管理配置文件
基本认知
npm 规定,在项目根目录中,必须提供一个叫做 package.json 的包管理配置文件。用来记录与项目有关的一些配置信息,例如:
- 项目的名称、版本号、描述等
- 项目中都用到了哪些包
- 哪些包只在开发期间会用到
- 哪些包在开发和部署时都需要用到
问题:
在多人协作时,第三方包的体积过大,不方便团队成员之间共享项目源代码
解决方案:
共享时剔除 node_modules 目录,让其他成员根据 package.json 配置文件,自行下载第三方模块(注意:今后在项目开发中,一定要把 node_modules 文件夹,添加到 .gitignore 忽略文件中)
快速创建
- npm 包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,
快速创建 package.json 配置文件
- 该命令只能在英文的目录下成功运行
- 当我们后续运行 npm install 命令去安装其他的包时,npm 包管理工具会自动更新配置文件的信息,我们不需要手动修改它
npm init -y
一次性安装所有包
- 执行 npm install 命令时,npm 包管理工具会先自动读取 package.json 配置文件中的 dependencies 节点
- 读取到记录的所有依赖包名称和版本号之后,npm 包管理工具会把这些包一次性下载到项目中
npm install
卸载包
- 命令执行成功后,会把卸载的包,自动从 package.json 的 dependencies 中移除掉
npm uninstall 包的名称
devDependencies 节点
- 如果某些包只在项目开发阶段用到,在项目上线之后不会使用,建议把这些包记录到 devDependencies 节点中
npm i 包的名称 -D
或者
npm i 包的名称 --save-dev
npm 镜像源
# 查看当前的下包镜像源
npm config get registry
# 将下包的镜像源切换成淘宝镜像源
npm config set registry https://registry.npmmirror.com
# 检查镜像源是否下载成功
npm config get registry
Node.js 内置模块
fs 文件系统模块
// 1. 导入 fs 模块对象
const fs = require('fs')
// 2. 写入文件
// 参数1:要写入内容的文件的存放路径
// 参数2:表示要写入的内容
// 参数3:写入的格式(默认utf8)
// 参数3:回调函数
const info = 'Hello World'
fs.writeFile('./test.txt', info, 'utf8', function (err) {
if (err) console.log(`错误信息${err}`)
else console.log(`写入数据成功(数据信息:${info})`)
// 3. 读取文件
// 参数1:读取文件的存放路径
// 参数2:读取文件时候采用的编码格式(默认utf8)
// 参数3:回调函数,拿到读取失败和成功的结果
fs.readFile('./test.txt', 'utf8', (err, data) => {
if (err) console.log(`错误信息${err}`)
else console.log(`读取数据成功(数据信息:${data.toString()})`) // data 是 buffer 16 进制数据流对象,需要转换为字符串格式
})
})
path 路径模块
- 获取当前文件所处的目录
// __dirname 表示获取当前文件所处的目录(文件夹)
console.log(__dirname)
- 路径拼接
// 导入 path 模块
const path = require('path')
// 注意: ../会抵消前面的路径
const pathStr = path.join('/a', '/b/c', '../', '/d', '/e')
console.log(pathStr) // 输出路径为:\a\b\d\e
- 获取文件名、扩展名
// 导入 path 模块
const path = require('path')
// 定义文件的"存放路径"
const filePath = '/a/b/c/index.html'
// 从"存放路径"中获取文件名(包含扩展名部分)
const fileName1 = path.basename(filePath)
console.log(fileName1) // 输出结果:index.html
// 从"存放路径"中获取文件名(不包含扩展名部分)
const fileName2 = path.basename(filePath, '.html')
console.log(fileName2) // 输出结果:index
// 从"存放路径"中获取文件扩展名
const extName = path.extname(filePath)
console.log(extName) // 输出结果:.html
http 模块
// 1. 导入 http 模块
const http = require('http')
const { resolve } = require('path')
// 2. 创建 web 服务器实例
const server = http.createServer()
// 3. 为服务器实例绑定 request 事件
// req 是请求对象,包含了与客户端相关的数据和属性
// res 是响应对象
// res.end() 方法用于向客户端发送指定的内容,并结束本次请求的处理过程
server.on('request', (req, res) => {
// req.url 是客户端请求的 URL 地址
const url = req.url
// req.method 是客户端请求的 method 类型
const method = req.method
// 为了防止中文乱码的问题,需要设置响应头
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// res.end() 方法
res.end(`<h1>您的请求方式为:${method} 请求</h1>`)
})
// 4. 启动服务器
server.listen(80, () => {
console.log('server running at http://127.0.0.1:80')
})
第三方 API 模块
express
基本介绍
- Express 是基于 Node.js 平台的 Web 开发框架,专门用来创建 Web 服务器
- Express 本质上就是一个第三方模块,相较与 Node.js 内置的 http 模块,它使用起来更简单,开发效率更高(Express 是基于 http 模块进一步封装出来的)
- Express 中文官网:https://www.expressjs.com.cn/
模块使用
- 安装
npm i express@4.17.1
- 创建 Web 服务器
// 1. 导入 express
const express = require('express')
// 2. 创建 web 服务器
const app = express()
// 4. 监听客户端的 GET 和 POST 请求
// GET app.get('请求URL', (请求对象, 响应对象) => { 处理函数 })
// POST app.post('请求URL', (请求对象, 响应对象) => { 处理函数 })
app.get('/user', (req, res) => {
res.send({ name: 'Jack', age: 20, gender: '男' }) // 响应一个 JSON 对象
})
app.post('/user', (req, res) => {
res.send('订单提交成功,请等待商家发货') // 响应
})
// 5. 获取URL携带的查询参数
app.get('/', (req, res) => {
// 默认情况下,req.query 是一个空对象
// 查询参数 ?name=Jack&age=20
// 可通过 req.query.name 与 req.query.age 获得其值
console.log(req.query)
res.send(req.query)
})
// 6. 获取URL中的动态参数
app.get('/user/:id/:name', (req, res) => {
// req.params 默认是一个空对象,里面存放着,通过冒号识别出来的参数值
res.send(req.params)
})
// 3. 调用 app.listen(端口号, 启动成功后的回调函数)
app.listen(80, () => {
console.log('Server started successfully at http://127.0.0.1/')
})
托管静态资源
- 认识 express.static()
介绍:
express 提供了一个非常好用的函数,叫做 express.static()
通过它,我们可以非常方便地创建一个静态资源服务器
例如:
项目目录下有一个 public 文件夹,里面存放了图片、html、css、js文件,通过下面的代码就可以将它们对外开放访问了
注意:
express 在指定的静态文件中查找文件,并对外提供资源的访问路径,因此,存放静态文件的目录名(本例中为public)是不会出现在URL中的
http://127.0.0.1/images/img0.jpg
http://127.0.0.1/html/index.html
http://127.0.0.1/css/index.css
http://127.0.0.1/js/index.js
app.use(express.static('public'))
- 托管多个静态资源目录
app.use(express.static('public'))
app.use(express.static('files'))
......
- 挂载路径前缀
如果希望在托管的静态资源访问路径之前,挂载路径前缀,可以使用下面的代码
http://127.0.0.1/public/images/img0.jpg
http://127.0.0.1/public/html/index.html
http://127.0.0.1/public/css/index.css
http://127.0.0.1/public/js/index.js
app.use('public', express.static('public'))
nodemon
- 为什么要使用 nodemon
在编写调试 Node.js 项目的时候,如果修改了项目的代码,则需要频繁的手动 close 掉,然后再重新启动,非常繁琐
为了解决这个问题,我们可以使用 nodemon 这个工具,它能够监听项目文件的变动,当代码修改后,nodemon 会自动帮我们重启项目,极大方便了开发和调试
- 安装 nodemon
在终端中运行下面命令,即可将 nodemon 安装为全局可用的工具
npm install -g nodemon
- 使用 nodemon
nodemon 要执行的js文件的路径
例如:nodemon E:\Desktop\jsProject\test.js
路由
直接挂载路由
const express = require('express')
const app = express()
// 直接在app上挂载路由
app.get('/', (req, res) => { res.send('this is get') })
app.post('/', (req, res) => { res.send('this is post') })
app.listen(80, () => {
console.log('Server is running at http://127.0.0.1/')
})
模块化的方式,挂载路由,实现解耦合
- 文件名:routerModule
// 这是路由模块
// 1. 导入 express 模块
const express = require('express')
// 2. 创建路由对象
const router = express.Router()
// 3. 挂载具体的路由
router.get('/list', (req, res) => { res.send('Get user list.') })
router.post('/add', (req, res) => { res.send('Add new user.') })
// 4. 向外导出路由对象
module.exports = router
- 文件名:myApplication
// 这是 app (application-应用)
// 1. 导入 express 模块
const express = require('express')
// 2. 创建应用
const app = express()
// 4. 导入路由模块
const router = require('./routerModule')
// 5. "注册"路由模块
app.use('/user', router)
// app.use(express.static('public'))
// 注意:app.use() 函数的作用,就是用来"注册"全局中间件
// 3. 运行服务器,监听应用的80端口
app.listen(80, () => { console.log('Server is running at http://127.0.0.1/') })
中间键
- 基本认识
// 1. 导入 express 模块
const express = require('express')
// 2. 创建应用
const app = express()
// 4. 定义一个中间件函数
const mw = (req, res, next) => {
console.log('这是最简单的中间件函数')
next() // 把流转关系,转交给下一个中间件或路由
}
// 5. 将 mw 注册为全局生效的中间件
// 当注册成功后,用户每次请求服务时,都会先执行中间键函数里面的内容,再执行路由函数里面的内容
app.use(mw)
// 6. 创建路由
app.get('/', (req, res) => { res.send('home page') })
app.get('/user', (req, res) => { res.send('user page') })
// 3. 运行服务器,监听应用的80端口
app.listen(80, () => { console.log('Server is running at http://127.0.0.1/') })
- 体验它的作用
// 1. 导入 express 模块
const express = require('express')
// 2. 创建应用
const app = express()
// 4. 注册全局中间件
app.use((req, res, next) => {
// 获取请求到达服务器的时间
const time = Date.now()
// 为req对象,挂载自定义属性,从而把时间共享给后面的所有路由
req.startTime = time
next()
})
// 5. 创建路由
app.get('/', (req, res) => { res.send('home page ' + req.startTime) })
app.get('/user', (req, res) => { res.send('user page ' + req.startTime) })
// 3. 运行服务器,监听应用的80端口
app.listen(80, () => { console.log('Server is running at http://127.0.0.1/') })
- 定义多个全局中间件
// 1. 导入 express 模块
const express = require('express')
// 2. 创建应用
const app = express()
// 4. 注册全局中间件
// 4.1 注册第一个中间件
app.use((req, res, next) => {
console.log('调用了第1个中间件')
next()
})
// 4.2 注册第二个中间件
app.use((req, res, next) => {
console.log('调用了第2个中间件')
next()
})
// 4.3 注册第三个中间件
app.use((req, res, next) => {
console.log('调用了第3个中间件')
next()
})
// 5. 创建路由
app.get('/user', (req, res) => { res.send('user page') })
// 3. 运行服务器,监听应用的80端口
app.listen(80, () => { console.log('Server is running at http://127.0.0.1/') })
- 局部生效的中间件
// 1. 导入 express 模块
const express = require('express')
// 2. 创建应用
const app = express()
// 4. 定义一个中间件函数
const mw = (req, res, next) => {
console.log('调用了局部生效的中间件')
next()
}
// 5. 创建路由
app.get('/', mw, (req, res) => { res.send('home page') }) // 使用了局部中间件
app.get('/user', (req, res) => { res.send('user page') }) // 没有使用局部中间件
// 3. 运行服务器,监听应用的80端口
app.listen(80, () => { console.log('Server is running at http://127.0.0.1/') })
- 定义多个局部中间件
// 以下两种写法是等效的
app.get('/', mw1, mw2, (req, res)=>{ res.send('home page') })
app.get('/', [mw1, mw2], (req, res)=>{ res.send('home page') })
- 错误级别的中间件
// 1. 导入 express 模块
const express = require('express')
// 2. 创建应用
const app = express()
// 4. 定义路由
app.get('/', (req, res) => {
throw new Error('The Error made by People.') // 人为制造错误
res.send('home page') // 此处无法执行
})
// 5. 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use((err, req, res, next) => {
console.log('警告!服务器发生了错误。\n错误信息:' + err.message)
res.send('尊敬的用户,很抱歉,服务器出错了~')
}) // 错误级别的中间件一定要放在所有路由之后
// 3. 运行服务器,监听应用的80端口
app.listen(80, () => { console.log('Server is running at http://127.0.0.1/') })
- express 内置的中间件
自从 Express 4.16.0 版本开始,Express 内置了3个常用的中间件,极大的提高了 Express 项目的开发效率和体验
1. express.static 快速托管静态资源的内置中间件,例如:HTML文件、CSS样式、图片等
2. express.json 解析 JSON 格式的请求体数据 (4.16.0+ 版本可用)
3. express.urlencoded 解析 URL-encoded 格式的请求体数据 (4.16.0+ 版本可用)
// 1. 导入 express 模块
const express = require('express')
// 2. 创建应用
const app = express()
// 4.1 通过express.json()这个中间件,解析表单中的JSON
app.use(express.json())
// 4.2 通过express.urlencoded()这个中间件,来解析表单中的url-encoded格式的数据
app.use(express.urlencoded({ extended: false }))
// 5. 创建路由
app.post('/user', (req, res) => {
// 使用 req.body 属性,可用接收客户端发送过来的请求体数据
// 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined
console.log(req.body)
res.send('ok')
})
app.post('/book', (req, res) => {
// 在服务器端,可用通过 req.body 来获取 JSON 格式的表单数据和 url-encoded 格式的数据
console.log(req.body)
res.send('ok')
})
// 3. 运行服务器,监听应用的80端口
app.listen(80, () => { console.log('Server is running at http://127.0.0.1/') })
跨域访问
- 下载并安装 cors 中间件
npm i cors@2.8.5
- 使用 cors 中间件
// 导入 cors 中间件
const cors = require('cors')
// 将 cors 注册为全局中间件
app.use(cors())
跨域资源共享
点击链接:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
mysql
基本内容
- 安装
npm install mysql
- 配置
// 1. 导入 mysql 模块
const mysql = require('mysql')
// 2. 建立与 MySQL 数据库的连接
const db = mysql.createPool({
host: '127.0.0.1', // 数据库的IP地址
user: 'root', // 登录数据库的账号
password: 'admin123', // 登录数据库的密码
database: 'myDatabase' // 指定要操作哪个数据库
})
- 测试
// 检测 mysql 模块能否正常工作
db.query('select 1', (err, results) => {
if (err) return console.log(err.message) // mysql 模块工作期间报错了
console.log(results) // 能够成功的执行SQL语句
})
查询与插入数据
- 查询数据
// 1. 导入 mysql 模块
const mysql = require('mysql')
// 2. 建立与 MySQL 数据库的连接
const db = mysql.createPool({
host: '127.0.0.1', // 数据库的IP地址
user: 'root', // 登录数据库的账号
password: 'admin123', // 登录数据库的密码
database: 'myDatabase' // 指定要操作哪个数据库
})
// 3. 查询 users 表中所有的数据
const sqlStr = 'select * from users'
db.query(sqlStr, (err, results) => {
// 查询数据失败
if (err) return console.log(err.message)
// 查询数据成功
console.log(results)
})
- 插入数据
// 1. 导入 mysql 模块
const mysql = require('mysql')
// 2. 建立与 MySQL 数据库的连接
const db = mysql.createPool({
host: '127.0.0.1', // 数据库的IP地址
user: 'root', // 登录数据库的账号
password: 'admin123', // 登录数据库的密码
database: 'myDatabase' // 指定要操作哪个数据库
})
// 3. 向 users 表中插入数据
const user = { username: 'Jack', password: '123456' }
const sqlStr = 'insert into users set ?'
db.query(sqlStr, user, (err, results) => {
if (err) return console.log(err.message) // 执行SQL语句失败了
if (results.affectedRows === 1) { console.log('插入数据成功!') }
})
更新与删除数据
- 更新数据
// 1. 导入 mysql 模块
const mysql = require('mysql')
// 2. 建立与 MySQL 数据库的连接
const db = mysql.createPool({
host: '127.0.0.1', // 数据库的IP地址
user: 'root', // 登录数据库的账号
password: 'admin123', // 登录数据库的密码
database: 'myDatabase' // 指定要操作哪个数据库
})
// 3. 更新 users 表中数据
const user = { id: 6, username: 'Jack', password: '123456' }
const sqlStr = 'update users set ? where id=?'
db.query(sqlStr, [user, user.id], (err, results) => {
if (err) return console.log(err.message) // 执行SQL语句失败了
if (results.affectedRows === 1) { console.log('更新数据成功!') }
})
- 删除数据
// 1. 导入 mysql 模块
const mysql = require('mysql')
// 2. 建立与 MySQL 数据库的连接
const db = mysql.createPool({
host: '127.0.0.1', // 数据库的IP地址
user: 'root', // 登录数据库的账号
password: 'admin123', // 登录数据库的密码
database: 'myDatabase' // 指定要操作哪个数据库
})
// 3. 删除 users 表中的数据(删除id=5的用户数据)
const sqlStr = 'delete from users where id=?'
db.query(sqlStr, 5, (err, results) => {
if (err) return console.log(err.message) // 执行SQL语句失败了
if (results.affectedRows === 1) { console.log('删除数据成功!') }
})
标记删除法:(扩展)
使用 delete 语句,是真的会把数据从表中抹除掉。为了保险起见,推荐使用标记删除法,来模拟删除的动作。
什么是标记删除法呢?就是在创建表的时候,提前建立好一个 status 状态字段,用来表示当前这条数据是否被删除掉,当我们使用标记删除法后,哪怕用户删除数据了,有一天用户需要找回来,也能找到这条数据。
Web 开发模式
目前主流的开发模式有两种
- 基于服务端渲染的传统 Web 开发模式
- 基于前后端分离的新型 Web 开发模式
服务端渲染
概念
服务端发送给客户端的HTML页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用Ajax这样的技术额外请求页面的数据。
优点
- 前端耗时少(因为服务器端负责动态生成HTML内容,浏览器只需要直接渲染即可,尤其是移动端,更省电)
- 有利于SEO(因为服务器端响应的是完整的HTML页面内容,所以爬虫更容易获取信息,更有利于SEO)
缺点
- 占用服务器资源(服务器端完成HTML页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力)
- 不利于前后端分离,开发效率低(使用服务器端渲染,则无法分工合作,尤其对于前端项目复杂度高的项目,不利于项目高效开发)
前后端分离
概念
前后端分离的开发模式,依赖于Ajax技术的广泛应用。简而言之,前后端分离的Web开发模式,就是后端只负责提供API接口,前端使用Ajax调用接口的开发模式。
优点
- 开发体验好。(前端专注于 UI 页面的开发,后端专注于 API 的开发,而且前端有更多的选择性)
- 用户体验好。(Ajax 技术的广泛应用,极大的提高了用户的体验 ,可以轻松实现页面的局部刷新)
- 减轻了服务器的压力(页面最终是在每个用户的浏览器中生成的)
缺点
- 不利于 SEO (因为完整的HTML页面需要在客户端动态拼接完成,所以爬虫难以爬取页面的有效信息)
解决方案:
利用 Vue、React 等前端框架的 SSR (server side render) 技术能够很好的解决 SEO 问题
身份认证
基本认识
介绍:
身份认证(Authoritarian)又称"身份验证"、"鉴权",它是指通过一定的手段,完成对用户身份的确认
例如:
手机验证码登录、邮箱密码登录、二维码登录等
不同开发模式下的身份认证
1.服务端渲染推荐使用 Session 认证机制
2.前后端分离推荐使用 JWT 认证机制
Session 认证机制
Cookie
- HTTP 无状态性质
HTTP 协议的无状态性质,指的是客户端每次的 HTTP 请求都是独立的,连续多个请求之前没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态
- 突破 HTTP 无状态的限制
使用 Cookie 可以弥补 HTTP 无法记录状态的问题
- 什么是 Cookie
Cookie 是存储在用户浏览器中的一段不超过 4KB 的字符串
它由一个名称(Name)、一个值(value)和其他几个用于控罪 Cookie 有效期、安全性、使用范围的可选属性组成
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器
- Cookie 的几大特性
1.自动发送
2.域名独立
3.过期时限
4.4KB限制
- Cookie 在身份认证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验证客户端的身份
- Cookie 不具有安全性
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器
Session
- Session = Cookie + 认证
- Session 的工作原理
实现 Session 认证
- 安装 express-session 中间件
npm install express-session
- 配置 express-session 中间件
// 1. 导入 session 中间件
const session = require('express-session')
// 2. 配置 Session 中间件
app.use(session({
secret: 'keyboard cat', // secret 属性的值可以为任意字符串
resave: false, // 固定写法
saveUninitialized: true // 固定写法
}))
- session 操作
当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息
// 1. 创建服务器实例
const express = require('express')
const app = express()
// 3. 配置 express-session 中间件
const session = require('express-session')
app.use(session({
secret: 'keyboard cat', // secret 属性的值可以为任意字符串
resave: false, // 固定写法
saveUninitialized: true // 固定写法
}))
// 4. 创建路由
// 4.1 登录接口
app.post('api/login', (req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== 'admin' || req.body.password !== '123456') {
return res.send({ status: 1, msg: '登录失败' })
}
req.session.user = req.body // 将用户的信息,存储到 Session 中
req.session.isLogin = true // 将用户的登录状态,存储到 Session 中
req.send({ status: 0, msg: '登录成功' })
})
// 4.2 获取用户姓名的接口
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
})
})
// 4.3 退出登录的接口
app.post('/api/logout', (req, res) => {
// 清空当前客户端对应的 Session 信息
req.session.destory()
res.send({
status: 0,
msg: '退出登录成功'
})
})
// 2. 启动服务器
app.listen(80, (req, res) => {
console.log('Server is running at http://127.0.0.1/')
})
- Session 认证的局限性
Session 认证机制需要配合 Cookie 才能实现。
由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口时,需要做很多额外配置,才能实现跨域 Session 认证
当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制
当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制
JWT 认证机制
简介
- JWT(JSON Web Token)是目前最流行的跨域认证解决方案
- 工作原理
- **JWT **通常由三部分组成
Header (头部)
Payload (有效载荷)
Signature (签名)
三者之间使用英文的"."进行分隔
Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串
Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性
- JWT 的使用方式
客户端收到服务器返回的 JWT 之后,通常会将它存储在 localStorage 或 sessionStorage 中
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证
推荐的做法是把 JWT 放在 HTTP 请求头的 Authoritarian 字段中
格式如下:
Authorization: Bearer <token>
实现 JWT 认证
- 安装 JWT 相关的包
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
npm install jsonwebtoken express-jwt
- 使用 JWT
// 1. 创建服务器实例
const express = require('express')
const app = express()
// 3. 使用 JWT
// 3.1 安装并导入 JWT 相关的两个包
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 3.2 设置 secret 密钥,保证 JWT 字符串的安全
const secretKey = 'text use ^_^'
// 3.4 将 JWT 字符串解析还原成 JSON 对象
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
const mw = expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] })
app.use(mw) // 将这个中间件注册为全局中间件
// 获取用户信息的接口
app.get('/admin/getInfo', (req, res) => {
// 3.5 使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: "获取用户信息成功",
data: req.user
})
})
// 登录接口
app.post('api/login', (req, res) => {
const userinfo = req.body
// 登录失败
if (userinfo.username !== 'admin' || userinfo.password !== '123456') {
return res.send({ status: 400, message: '登录失败' })
}
// 登录成功
// 3.3 登录成功后,生成 JWT 字符串,并通过 token 属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的密钥
// 参数3:配置对象,可以配置当前的 token 的有效期
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '60s' })
req.send({ status: 200, message: '登录成功', token: tokenStr })
})
// 3.6 使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res) => {
// 判断错误是否为 token 解析失败导致
if (err.name === 'UnauthorizedError') return res.send({ status: 401, message: '无效的token' })
res.send({ status: 500, message: '未知的错误' })
})
// 2. 启动服务器
app.listen(80, (req, res) => {
console.log('Server is running at http://127.0.0.1/')
})