NodeJS笔记-系列
初识NodeJS
Express框架
NodeJS-MongoDB
NodeJS接口、会话控制
Node-回眸[一]
Node-回眸[二]
Node-回眸[三]
QX-AI
GPT-4
QX-AI初始化中...
暂无预设简介,请点击下方生成AI简介按钮。
介绍自己
生成预设简介
推荐相关文章
生成AI简介

接口

接口即API,是前后端通信的桥梁.用于实现前后端通信

在NodeJS中,一个接口就是服务中的一个路由规则

API给客户端返回结果通常是 JSON 格式

后端直接使用ejs、pug渲染好页面再返回是前后端不分离的表现,后端(HTTP服务)通过API将数据交给前端(网页、APP、小程序),后端只需考虑提供前端所需要的数据,而不用管数据的渲染,API 是通用的,不同的类型的前端使用同一个API就能渲染出多种页面。

接口的组成

接口的组成会在API文档中详细说明,随机图片验证码API文档

  1. 请求方式 GET / POST
  2. 接口地址 URL
  3. 请求参数
  4. 响应结果、格式

RESTful API

RESTful API 设计指南—阮一峰
RESTful API 是一种接口的规范,为了减少前后端的沟通成本
同一个URL路径(可带上路由参数)不同的请求方法代表不同的功能,且语义要相符,API 返回的状态码也要相符

操作 请求类型 URL 返回
新增 POST /song 返回新生成的歌曲信息
删除 DELETE /song/10 返回一个空文档
覆盖修改 PUT /song/10 返回更新后的歌曲信息
局部修改 PATCH /song/10 返回更新后的歌曲信息
获取所有 GET /song 返回歌曲列表数组
获取单个(id) GET /song/10 返回单个歌曲信息

状态码语义:404 找不到资源、403 禁止访问、500服务器内部错误

当然通常是POST亿把梭哈

json-server

json-server 是一个 JS 编写的全局工具包,可以快速搭建 RESTful API 服务,也用于前端临时搭建接口使用

安装:npm i -g json-server

新建一个json文件

1
2
3
4
5
6
7
8
{
"song": [
{ "id": 1, "name": "干杯", "singer": "五月天" },
{ "id": 2, "name": "当", "singer": "动力火车" },
{ "id": 3, "name": "不能说的秘密", "singer": "周杰伦" }
]
}

使用:以 JSON 文件所在文件夹作为工作目录 ,执行该命令,端口默认3000
1
json-server --watch <文件名>.json

命令行的提示:

1
2
3
4
5
6
7
8
9
10
11
\{^_^}/ hi!    

Loading 01.json
Done

Resources
http://localhost:3000/song

Home
http://localhost:3000

接口测试工具

常用接口测试工具:apipostapifoxpostman

apipost基本使用:

  1. Header 设置请求头
  2. Query 设置查询字符串
  3. Body 设置请求体
    1. none 没有内容
    2. form-data 表单形式数据
    3. x-www-form-urlencoded Query-String形式的数据
    4. raw 原生请求体,json格式数据

记账本添加API

因为API即请求方法+路由,所以去修改routes文件夹下的路由,将原来的index.js页面路由放到web文件夹,新建账单相关接口路由account.js,放到api文件夹

然后在app.js中使用路由,接口路径添加上api前缀

/app.js
1
2
3
4
// 导入接口路由
var accountRouter = require('./routes/api/account');
// 使用接口路由,并添加api前缀
app.use('/api', accountRouter);

account.js总览,相同路径、不同请求方法、带路由参数来实现增删改查接口,符合RESTful API接口规范

接口的设计:

  1. 接口通常返回的都是 JSON
  2. 一个接口通常由响应编号 code响应信息 msg响应数据 data 组成
  3. 即使出现错误或失败也是通过响应编号去体现错误,而不设置状态响应码
  4. 响应编号,字符串,四个0表示成功,非0(如:1001)表示失败
  5. 响应信息,字符串,如:查找成功、删除成功
/routes/api/account.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
var express = require('express');
var router = express.Router();

// 导入moment处理日期
const moment = require('moment');

// 导入文档模型对象
const AccountModel = require('../../models/AccountModel');

// 获取所有记录接口
router.get('/', function (req, res, next) {

// 读取所有数据,按日期降序
AccountModel.find().sort({ date: -1 })
.then(data => {
// 接口返回json
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '读取成功',
// 响应的数据
data: data
})
})
.catch(err => {
console.log(err);
// 接口的状态信息标识在code中,不设置响应码
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1001',
// 响应的信息
msg: '读取失败',
// 响应的数据
data: null
})
})

});

// 获取单条记录接口
router.get('/:id', (req, res) => {
let { id } = req.params;
AccountModel.findById(id)
.then(data => {
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '读取成功',
// 响应的数据
data: data
});
})
.catch(err => {
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1004',
// 响应的信息
msg: '读取失败',
// 响应的数据
data: null
})
})
})

// 添加记录接口
router.post('/', function (req, res, next) {

// 插入一条数据
AccountModel.create({
// 解构赋值
...req.body,
// 覆盖修改日期为日期对象
date: moment(req.body.date).toDate()
})
.then(data => {
console.log(data);
// 接口返回json
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '添加成功',
// 响应的数据,返回创建成功的文档
data: data
})
})
.catch(err => {
console.log(err);
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1002',
// 响应的信息
msg: '添加失败',
// 响应的数据
data: null
})
});

});

// 根据id删除数据,使用路由参数
router.delete('/:id', (req, res) => {
// 获取路由参数id
let id = req.params.id;
AccountModel.deleteOne({ _id: id })
.then(data => {
console.log(data);
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '删除成功',
// 响应的数据
data: data
})
})
.catch(err => {
console.log(err);
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1003',
// 响应的信息
msg: '删除失败',
// 响应的数据
data: null
})
})

});

// 更新账单接口
router.patch('/:id', (req, res) => {
let { id } = req.params;
AccountModel.updateOne({ _id: id }, req.body)
.then(data => {
res.json({
code: '0000',
msg: '更新成功',
data: data
})
})
.catch(err=>{
res.json({
code: '1005',
msg: '更新失败',
data: null
})
})
})


module.exports = router;

会话控制

每一个 HTTP 请求都是一个会话,但 HTTP 是一种无状态的协议,它没有办法区分不同的请求、不同的会话是否来自于同一个客户端,即无法区分、识别用户

所以需要后端实现会话控制来区分用户,如使用注册登陆来区分用户,以此保护不同用户的数据安全

常见的会话控制技术:cookiesessiontoken

cookie

cookie 是 HTTP 服务器发送到用户浏览器并保存在本地的一小块数据,按域名划分,本质上是Key-Value键值对

浏览器向服务端发送请求时,会自动携带对应域名下的所有 cookie 设置在请求头的 Cookie 属性中

服务端通过响应报文的 set-cookie 在浏览器设置cookie

设置cookie

cookie() 在响应报文set-cookie中携带cookie

设置浏览器关闭时自动销毁的cookie:

1
2
3
res.cookie('name', 'chuckle');
// 响应报文:
Set-Cookie: name=chuckle; Path=/

设置一段时间后过期的cookie,设置时maxAge单位毫秒,响应报文中Max-Age单位秒

1
2
3
4
// 设置maxAge,cookie存活多少ms
res.cookie('user', 'giggles',{maxAge: 60000});
// 响应报文:
Set-Cookie: user=giggles; Max-Age=60; Path=/; Expires=Sun, 23 Apr 2023 01:12:07 GMT

后续请求头中Cookie属性携带cookie,Key-Value

1
Cookie: name=chuckle; user=giggles

删除cookie

clearCookie() 通过设置对应cookie的过期时间为1970年,使cookie过期自动删除

1
2
res.clearCookie('name');
res.clearCookie('user');
响应报文
1
2
Set-Cookie: name=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: user=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT

获取cookie

需要安装 cookie-parser 是一个中间件

使用cookie-parser
1
2
3
4
// 导入
const cookieParser = require('cookie-parser');
// 设置中间件
app.use(cookieParser())

设置完中间件后,通过 req.cookies 即可获取所有cookie

1
2
3
4
5
console.log(req.cookies);
// {
// name: 'chuckle',
// user: 'giggles'
// }

cookie安全

1、httpOnly 设置cookie时带上 httpOnly 属性,客户端脚本代码(js)尝试读取该cookie,浏览器将返回一个空字符串作为结果

HttpOnly是包含在http返回头Set-Cookie里面的一个附加的属性

1
2
3
res.cookie('name', 'chuckle',{httpOnly: true});
// 响应报文
Set-Cookie: name=chuckle; Path=/; HttpOnly

2、加密cookie,设置cookie时带上 signed 属性,并给 cookieParser(secret) 中间件传入一个字符串secret用于加密,获取时使用 req.signedCookies

1
2
3
4
5
6
// 设置加密字符串
const secret = 'qx';
app.use(cookieParser(secret))
// 设置cookie
res.cookie('number','123456',{signed: true,maxAge: 60*1000});
console.log(req.signedCookies);// { number: '123456' }

在浏览器端查看该cookie,可以看到已经被加密

1
number: s%3A123456.gnY%2Bp%2BEFLYanFxZP9eAYnlyW9IkToo4KlHZyIJ3DJgc

session

session 在服务器端保存当前访问用户的相关信息(用户名、用户id、邮箱等)

作用:识别用户

登陆例子:
当用户输入账号密码传给服务端后,会去数据库查找是否正确,如果正确,服务端会给这个用户创建一个session对象,保存当前用户的基本信息,该对象还会生成并保存一个唯一session_id会话id(sid,在数据库中字段名是_id),然后服务端会将sid以cookie形式返回给浏览器,浏览器之后的请求就会带上这个sid,服务端可以通过这个sid在一堆session对象数组中识别是哪个用户。

使用session

安装 express-sessionconnect-mongo 两个包

express-session用于将session存到内存中,connect-mongo可以将session存到数据库中,方便查看

设置中间件进行配置:

后续的增删改查操作,中间件会自动匹配好当前请求是哪个用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 引入包
const session = require("express-session");
const MongoStore = require('connect-mongo');
// 使用中间件
app.use(session({
name: 'sid', //设置cookie的name,默认值是:connect.sid
secret: 'chuckle', //参与加密的字符串(又称签名)
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session,用于重置session过期时间
// 设置session存储位置
store: MongoStore.create({
// 存储到mongodb中
mongoUrl: 'mongodb://127.0.0.1:27017/test' //数据库的连接配置
}),
// 浏览器端cookie设置
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000 * 60 // 不仅控制cookie,也控制session的生命周期
},
}))

设置session

模拟登陆情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/login', (req, res) => {
let uname = req.query.username;
// 登陆需要传递查询字符串,账号和密码
if(req.query.username === 'chuckle' && req.query.password === '123456'){
// 设置session,就是把用户的基本信息传进去
req.session.username = uname;// 传入用户名
res.send('登陆成功');
return;
}
res.send('登陆失败');
});
// 响应报文
Set-Cookie: sid=s%3A-5tzIG5ZCDk4JaT7POAgODadySTDZVur.H7%2Fh5xrNRApVa%2Fl1J4fynkE48fREOM1yfLjdZd68bps; Path=/; Expires=Sun, 23 Apr 2023 03:16:14 GMT; HttpOnly

数据库中:

读取session

检测session是否存在,不存在则让用户登陆

1
2
3
4
5
6
7
8
9
10
11
app.get('/', (req, res) => {
// 检测session中是否有用户信息
// 中间件已经自动获取当前cookie中的用户信息,从数据库中取出来,存到session内存中
if(req.session.username){
// 有信息则说明已经登陆,直接欢迎
res.send('Welcome');
return;
}
res.send('还未登陆');
});

销毁session

destroy(),如用户主动退出登陆

1
2
3
4
5
6
7
app.get('/logout', (req, res) => {
// 中间件已经自动匹配好当前请求是哪个用户,直接调用destroy()即可销毁
req.session.destroy(()=>{
res.send('退出成功')
})
});

cookie与session区别

  1. 存在的位置
    cookie:浏览器端
    session:服务端
  2. 安全性
    cookie 是以明文的方式存放在客户端的,安全性相对较低
    session 存放于服务器中,所以安全性相对较好
  3. 网络传输量
    cookie 设置内容过多会增大报文体积, 会影响传输效率
    session 数据存储在服务器,只是通过 cookie 传递 id,所以不影响传输效率
  4. 存储限制
    浏览器限制单个 cookie 保存的数据不能超过 4K ,且单个域名下的存储数量也有限制(165)
    session 数据存储在服务器中,所以没有这些限制

记账本-注册登陆

效果:不同用户登陆后显示对应用户的账单,没有登陆就跳转到登录页面

大概步骤:

  1. 先在app.js中导入session操作相关的包,并设置中间件
  2. 创建注册登陆的ejs页面
  3. 创建用户文档模型,后续通过其将用户信息写入数据库
  4. 修改账单文档模型,添加一个userID字段来保存账单对应用户的_id,用以区分账单属于哪个用户
  5. 创建注册登陆的路由 auth.js,使用 md5 对密码进行加密
  6. 创建中间件 checkLoginMiddleware.js 来检测用户是否登陆
  7. 修改index.js路由,应用检测登陆的中间件,并根据业务需要调整部分代码

业务逻辑:

  1. 用户注册后将用户名和经过md5加密后的密码保存在数据库users集合中,并且每次新注册会检测用户名是否已经存在
  2. 登陆时获取客户端传来的用户名和密码去数据库中找是否有对应的用户,因为保存在数据库中的密码是经过md5加密的,所以要将密码经过一次md5加密后再去查找,同一字符串经过md5加密后结果是相同的
  3. 用户存在即登陆成功,则将用户信息(用户名和_id)写入session
  4. 在新增账单记录时,从session中找到当前用户的_id,然后保存到账单文档的userID字段
  5. 渲染账单列表时,只渲染属于当前用户的账单文档,对比当前登陆用户的_id和每个文档userID是否相同
  6. 退出登陆即销毁当前的session
  7. 对于从请求头cookie中获得sid然后找到对应的session,并提取用户的信息,是express-session包内中间件自动完成的,后续业务只需要req.session.<属性名>即可获取当前用户的信息

数据库:

先在app.js中导入session操作相关的包,并设置中间件

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 导入session操作相关包
const session = require("express-session");
const MongoStore = require('connect-mongo');
// 导入数据库连接配置文件
const { HOST, PORT, NAME } = require('./config');

// 使用中间件
app.use(session({
name: 'sid', //设置cookie的name,默认值是:connect.sid
secret: 'chuckle', //参与加密的字符串(又称签名)
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session,用于重置session过期时间
// 设置session存储位置
store: MongoStore.create({
// 存储到mongodb中
mongoUrl: `mongodb://${HOST}:${PORT}/${NAME}` //数据库的连接配置
}),
// 浏览器端cookie设置
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000 * 60 * 60 * 24 * 7 // 不仅控制cookie,也控制session的生命周期
},
}))

创建注册登陆的ejs页面

/views/auth/reg.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>添加记录</title>
<link rel="stylesheet" href="/css/page.css" />
</head>

<body>
<div class="content">
<div class="content-box">
<div class="title">
<h2>注册</h2>
</div>
<div class="home">
<a href="/login">去登陆</a>
</div>
<form action="/reg" method="post" autocomplete="off">
<div class="form-item">
<label for="username">用户名</label>
<input class="control" type="text" name="username" id="username" required/>
</div>
<div class="form-item">
<label for="password">密码</label>
<input class="control" type="text" name="password" id="password" required/>
</div>
<hr />
<button>注册</button>
</form>
</div>
</div>
</body>
</html>

/views/auth/login.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>添加记录</title>
<link rel="stylesheet" href="/css/page.css" />
</head>

<body>
<div class="content">
<div class="content-box">
<div class="title">
<h2>登陆</h2>
</div>
<div class="home">
<a href="/reg">去注册</a>
</div>
<form action="/login" method="post" autocomplete="off">
<div class="form-item">
<label for="username">用户名</label>
<input class="control" type="text" name="username" id="username" required/>
</div>
<div class="form-item">
<label for="password">密码</label>
<input class="control" type="text" name="password" id="password" required/>
</div>
<hr />
<button>登陆</button>
</form>
</div>
</div>
</body>
</html>

/public/css/page.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
body{
font-family:"Microsoft YaHei",微软雅黑;
color: #363636;
}
*{
padding: 0;
margin: 0;
box-sizing: border-box;
text-decoration: none;
}
hr{
margin: 20px auto;
border: 0;
border-top: 1px solid rgb(220, 220, 220);
}
.content{
max-width: 600px;
margin: 0 auto;
}
.content-box{
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
margin: 0 10px;
}
.content-box>*{
width: 100%;
}
.home{
margin-top: 10px;
padding-left: 20px;
display: flex;
justify-content: space-between;
}
.home a{
color: rgb(33, 70, 181);
}
.title{
padding: 20px 0;
border-bottom: 1px solid rgb(220, 220, 220);
}
.title h2{
font-size: 30px;
font-weight: 500;
}
.content-box form{
margin-top: 10px;
}
.form-item{
margin-bottom: 15px;
}
.form-item label{
display: block;
height: 32px;
line-height: 32px;
font-size: 18px;
}
.form-item>*{
width: 100%;
}
.form-item>.control{
padding: 6px 12px;
height: 36px;
font-size: 16px;
line-height: 36px;
color: #363636;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
-o-border-radius: 4px;
transition: all 0.3s;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
-ms-transition: all 0.3s;
-o-transition: all 0.3s;
outline: none;
font-family:"Microsoft YaHei",微软雅黑;
}
.form-item>.control:focus{
border-color: #66afe9;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
}
.form-item>textarea.control{
padding: 8px 12px;
height: 100px;
line-height: 1;
resize: none;
}
.content-box form>button{
width: 100%;
padding: 6px 12px;
margin-bottom: 15px;
border: 1px solid rgb(57, 162, 204);
background: rgb(37, 173, 204);
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
-o-border-radius: 4px;
font-size: 17px;
color: #fff;
transition: all 0.3s;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
-ms-transition: all 0.3s;
-o-transition: all 0.3s;
}
.content-box form>button:hover{
background: rgb(32, 153, 190);
}

创建用户文档模型,后续通过其将用户信息写入数据库

/models/UserModel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mongoose = require('mongoose');
// 文档结构对象
let UserSchema = new mongoose.Schema({
username: {
type: String,
required: true
},
password: {
type: String,
required: true
}
});
// 文档模型对象
let AccountModel = mongoose.model('users', UserSchema);
// 将文档模型对象暴露出去
module.exports = AccountModel;

修改账单文档模型,添加一个userID字段来保存账单对应用户的_id,用以区分账单属于哪个用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const mongoose = require('mongoose');
// 文档结构对象
let AccountSchema = new mongoose.Schema({
matter: {
type: String,
required: true
},
date: {
type: Date,
required: true
},
type: {
type: String,
enum: ['支出', '收入'],
default: '支出'
},
account: {
type: Number,
required: true
},
remark: {
type: String,
default: '无'
},
// 通过userID保存账单属于哪个用户的
userID:{
type: String,
required: true
}
});
// 文档模型对象
let AccountModel = mongoose.model('accounts', AccountSchema);
// 将文档模型对象暴露出去
module.exports = AccountModel;

创建注册登陆的路由 auth.js,使用 md5 对密码进行加密

/routes/web/auth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
var express = require('express');
var router = express.Router();

// 导入用户文档模型
const UserModel = require('../../models/UserModel');
// 导入md5对密码进行加密
const md5 = require('md5');

// 注册
// 注册页面路由
router.get('/reg', (req, res) => {
res.render('auth/reg');
})
// 注册操作路由
router.post('/reg', (req, res) => {
// 如果用户名重复就重新注册
UserModel.findOne({ username: req.body.username })
.then(data => {
// data不为空说明用户名重复
if (data) {
res.render('tip', { msg: '用户名已存在', url: '/reg' });
return;
}
// 用户名不重复则创建用户
UserModel.create({
...req.body,
// 使用md5对密码进行加密
password: md5(req.body.password)
})
.then(data => {
res.render('tip', { msg: '注册成功', url: '/login' });
})
.catch(err => {
res.status(500).render('tip', { msg: '注册失败!', url: '/reg' });
})
})
})

// 登陆
// 登陆页面路由
router.get('/login', (req, res) => {
res.render('auth/login');
})
// 登陆操作路由
router.post('/login', (req, res) => {
// 获取用户名和密码
let { username, password } = req.body;
// 如果用户名或密码为空则返回失败
if (!username || !password) {
res.render('tip', { msg: '用户名或密码错误', url: '/login' });
return;
}
// 查询数据库,看有没有该用户
// 要对密码也做一次md5加密然后去数据库对比
UserModel.findOne({
username: username,
password: md5(password)
})
.then(data => {
// 如果data为空说明用户不存在
if (!data) {
res.render('tip', { msg: '用户名或密码错误', url: '/login' });
return;
}
// 用户存在则将用户信息写入session
req.session.username = data.username;
req.session._id = data._id;
// 渲染成功提示页
res.render('tip', { msg: '登陆成功', url: '/' });
})
.catch(err => {
res.status(500).render('tip', { msg: '登陆失败!', url: '/login' });
})
})

// 退出登陆,避免跨站请求伪造,使用post
router.post('/logout', (req, res) => {
// 销毁session
req.session.destroy(() => {
res.render('tip', { msg: '退出成功', url: '/login' });
});
});


module.exports = router;

创建中间件 checkLoginMiddleware.js 来检测用户是否登陆

/middleware/checkLoginMiddleware.js
1
2
3
4
5
6
7
8
9
10
const checkLoginMiddleware = (req, res, next) => {
if (!req.session.username) {
// redirect重定向到登陆页面
return res.render('tip', { msg: '还未登陆!', url: '/login' });
}
next();
}

module.exports = checkLoginMiddleware;

修改index.js路由,应用检测登陆的中间件,并根据业务需要调整部分代码

/routes/web/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
var express = require('express');
var router = express.Router();

// 导入moment处理日期
const moment = require('moment');

// 导入文档模型对象
const AccountModel = require('../../models/AccountModel');

// 导入检测是否登陆的中间件、
const checkLoginMiddleware = require('../../middleware/checkLoginMiddleware');

// 将首页重定向到账单页
router.get('/', (req, res)=>{
res.redirect('/account');
})

// 记账本页面路由
router.get('/account',checkLoginMiddleware, function (req, res, next) {
// 读取对应用户所有数据,按日期降序
AccountModel.find({userID: req.session._id}).sort({ date: -1 })
.then(data => {
// 将数据数组传递过去遍历渲染,为了格式化日期,将moment传入
res.render('index', { content: data, moment });
})
.catch(err => {
console.log(err);
// 查找失败则返回500
res.status(500).render('tip', { msg: '查找失败!', url: '/' });
})

});

// 添加记录页面路由
router.get('/add',checkLoginMiddleware, function (req, res, next) {
res.render('add');
});

// 添加记录post接口路由
router.post('/add',checkLoginMiddleware, function (req, res, next) {

// 插入一条数据
AccountModel.create({
// 解构赋值
...req.body,
// 覆盖修改日期为日期对象
date: moment(req.body.date).toDate(),
// 将对应用户id传入
userID: req.session._id
})
.then(data => {
console.log(data);
// 成功跳转到提示页
res.render('tip', { msg: '添加成功!', url: '/' });
})
.catch(err => {
console.log(err);
// 添加失败则返回500
res.status(500).render('tip', { msg: '添加失败!', url: '/' });
});

});

// 根据id删除数据,使用路由参数
router.get('/delete/:id',checkLoginMiddleware, (req, res) => {
// 获取路由参数id
let id = req.params.id;
AccountModel.deleteOne({ _id: id })
.then(data => {
console.log(data);
// 删除成功跳转提示页
res.render('tip', { msg: '删除成功!', url: '/' });
})
.catch(err => {
console.log(err);
res.status(500).render('tip', { msg: '删除失败!', url: '/' });
})

});

module.exports = router;

token

token 是服务端生成并返回给 HTTP 客户端的一串加密字符串, token中保存着用户信息

作用:实现会话控制,识别用户的身份,主要用于移动端 APP

与session区别:token将用户信息存放于客户端,而session将用户信息存放于服务端,在客户端仅存放session的sid(_id)

token工作流程:

  1. 用户填写账号和密码校验身份,校验通过后服务端响应 token,一般放在响应体中
  2. 后续发送请求时,需要手动将 token 添加在请求报文的 token 属性中,一般放在请求头中

token特点:

  1. 数据存储在客户端,服务端压力更小
  2. 数据加密、可以避免 CSRF(跨站请求伪造),相对更安全
  3. 扩展性更强:服务间可以共享,增加服务节点更简单

JWT

JWT(JSON Web Token )是目前最流行的跨域认证解决方案,可用于基于 token 的身份验证

token实现身份验证有很多办法,但 JWT 使 token 的生成与校验更规范

使用 jsonwebtoken 包 来操作 token

使用:

  1. sign() 生成token
    sign(用户信息数据对象, 加密字符串, 配置对象)
  2. verify 校验解析token
    verify(token, 加密字符串, 回调函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const jwt = require('jsonwebtoken');
// 生成token
// sign(用户信息数据对象, 加密字符串, 配置对象)
// 在配置对象中设置生命周期等等
let token = jwt.sign({
username: 'chuckle',
id: '123456'
}, 'qx', {
// 设置生命周期,单位秒
expiresIn: 60
});

console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImNodWNrbGUiLCJpZCI6IjEyMzQ1NiIsImlhdCI6MTY4MjI2NTI3OSwiZXhwIjoxNjgyMjY1MzM5fQ._Or0D76QY78NofSGpZt81-B_T8K4iX_v0jEzDBvnObk
let t = token;

// 校验解析token,获得用户信息
// verify(token, 加密字符串, 回调函数)
jwt.verify(token, 'qx', (err, data) => {
if(err){
console.log('校验失败');
return;
}
console.log(data);
//{ username: 'chuckle', id: '123456', iat: 1682265279, exp: 1682265339 }
});

记账本-接口token

记账本的页面端已经做了会话控制、用户区分,但API接口仍然没有做约束、没区分用户,下面使用token进行会话控制

注册登陆接口

注册登陆接口,即注册成功则在数据库users集合中新建一个用户文档,登陆成功则返回一个token

在api路由文件夹中新建auth.js,作为注册登陆接口的路由

/routes/api/auth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
var express = require('express');
var router = express.Router();

// 导入用户文档模型
const UserModel = require('../../models/UserModel');
// 导入md5对密码进行加密
const md5 = require('md5');
// 导入jwt控制token
const jwt = require('jsonwebtoken');
// 导入配置文件,获取加密字符串
const config = require('../../config');

// 注册
// 注册操作API
router.post('/reg', (req, res) => {
// 如果用户名重复就重新注册
UserModel.findOne({ username: req.body.username })
.then(data => {
// data不为空说明用户名重复
console.log(data);
if (data) {
res.json({
code: '2003',
msg: '用户名已存在',
data: null
})
return;
}
// 用户名不重复则创建用户
UserModel.create({
...req.body,
// 使用md5对密码进行加密
password: md5(req.body.password)
})
.then(data => {
res.json({
code: '0000',
msg: '注册成功',
data: {
username: req.body.username
}
})
})
.catch(err => {
res.json({
code: '2004',
msg: '注册失败',
data: null
})
})
})
})

// 登陆
// 登陆操作API
router.post('/login', (req, res) => {
// 获取用户名和密码
let { username, password } = req.body;
// 如果用户名或密码为空则返回失败
if (!username || !password) {
res.json({
code: '2001',
msg: '用户名或密码错误',
data: null
})
return;
}
// 查询数据库,看有没有该用户
// 要对密码也做一次md5加密然后去数据库对比
UserModel.findOne({
username: username,
password: md5(password)
})
.then(data => {
// 如果data为空说明用户不存在
if (!data) {
res.json({
code: '2001',
msg: '用户名或密码错误',
data: null
})
return;
}
// 创建并返回token
let token = jwt.sign({
username: data.username,
_id: data._id
}, config.token_secret, {
expiresIn: 60 * 24 * 7
});
res.json({
code: '0000',
msg: '登陆成功',
data: {
token: token
}
})
})
.catch(err => {
res.json({
code: '2002',
msg: '登陆出错',
data: null
})
})
})

// 退出登陆
router.post('/logout', (req, res) => {
// 客户端删除token即可
res.json({
code: '0000',
msg: '退出成功',
data: null
})
});


module.exports = router;

其中token的加密字符串 token_secret 保存在配置文件中

/config.js
1
2
3
4
5
6
7
8
9
const config = {
HOST: '127.0.0.1',
PORT: 27017,
NAME: 'test',
session_secret: 'chuckle',
token_secret: 'chuckle'
}
module.exports = config;

业务接口

有了token后,要对 account.js 里原有的业务接口进行修改,并添加一个中间件对token进行校验,校验完后将token中的用户信息(username和userID)写入req中

中间件 checkTokenMiddleware.js

middleware/checkTokenMiddleware.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const jwt = require("jsonwebtoken");
const config = require('../config');

const checkTokenMiddleware = (req, res, next) => {
// 获取请求头中的token
let token = req.get('token');
// 没有token则报错
if (!token) {
res.json({
code: '2008',
msg: '缺失token',
data: null
})
return;
}
jwt.verify(token, config.token_secret, (err, data) => {
if (err) {
res.json({
code: '2009',
msg: 'token校验失败',
data: null
})
return;
}
// 校验成功后,将username和userID绑定到req上
req.username = data.username;
req.userID = data._id;
next();
})
}

module.exports = checkTokenMiddleware;

在业务接口中应用中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
var express = require('express');
var router = express.Router();

// 导入moment处理日期
const moment = require('moment');

// 导入文档模型对象
const AccountModel = require('../../models/AccountModel');

// 导入token校验中间件
const checkTokenMiddleware = require('../../middleware/checkTokenMiddleware');

// 获取所有记录接口
router.get('/', checkTokenMiddleware, function (req, res, next) {

// 读取所有数据,按日期降序
AccountModel.find({userID: req.userID}).sort({ date: -1 })
.then(data => {
// 接口返回json
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '读取成功',
// 响应的数据
data: data
})
})
.catch(err => {
console.log(err);
// 接口的状态信息标识在code中,不设置响应码
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1001',
// 响应的信息
msg: '读取失败',
// 响应的数据
data: null
})
})

});

// 获取单条记录接口
router.get('/:id', checkTokenMiddleware, (req, res) => {
let { id } = req.params;
AccountModel.findById(id)
.then(data => {
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '读取成功',
// 响应的数据
data: data
});
})
.catch(err => {
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1004',
// 响应的信息
msg: '读取失败',
// 响应的数据
data: null
})
})
})

// 添加记录接口
router.post('/', checkTokenMiddleware, function (req, res, next) {

// 插入一条数据
AccountModel.create({
// 解构赋值
...req.body,
// 覆盖修改日期为日期对象
date: moment(req.body.date).toDate(),
userID: req.userID
})
.then(data => {
console.log(data);
// 接口返回json
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '添加成功',
// 响应的数据,返回创建成功的文档
data: data
})
})
.catch(err => {
console.log(err);
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1002',
// 响应的信息
msg: '添加失败',
// 响应的数据
data: null
})
});

});

// 根据id删除数据,使用路由参数
router.delete('/:id', checkTokenMiddleware, (req, res) => {
// 获取路由参数id
let id = req.params.id;
AccountModel.deleteOne({ _id: id })
.then(data => {
console.log(data);
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '0000',
// 响应的信息
msg: '删除成功',
// 响应的数据
data: data
})
})
.catch(err => {
console.log(err);
res.json({
// 响应编号,四个0表示成功,非0表示失败
code: '1003',
// 响应的信息
msg: '删除失败',
// 响应的数据
data: null
})
})

});

// 更新账单接口
router.patch('/:id', checkTokenMiddleware, (req, res) => {
let { id } = req.params;
AccountModel.updateOne({ _id: id }, req.body)
.then(data => {
res.json({
code: '0000',
msg: '更新成功',
data: data
})
})
.catch(err=>{
res.json({
code: '1005',
msg: '更新失败',
data: null
})
})
})


module.exports = router;

最后在app.js中应用路由

1
2
3
4
// 导入注册登陆API路由
var authApiRouter = require('./routes/api/auth');
// 使用注册登陆API路由
app.use('/api', authApiRouter)

记账本完全体

记账本演示:KeepingBook

github地址:KeepingBook-vercel

API文档:KeepingBook-api