写在前面:此笔记来自b站课程尚硅谷Node.js零基础视频教程 P168-P192 / 资料下载 提取码:s3wj
会话:客户端向服务端发送一次请求,服务端响应一次信息,就是一次会话 HTTP是一种无状态的协议,无法区分多次请求是否来自同一个客户端,即无法区分用户。这时就需要会话控制解决该问题,常见的会话控制有三种: - cookie - session - token
cookie是HTTP服务器发送到用户浏览器并保存在本地的一小块数据,是按照域名划分的,形式类似于键值对
特点:浏览器向服务器发送请求时,会自动将当前域名下可用的cookie设置在请求头中,传递给服务器
这个请求头的名称也叫cookie
,因此也可将cookie理解成一个HTTP请求头
运行流程:以登录操作为例
- 在浏览器输入账号密码后,该信息被发送给服务器,服务器校验正确后,将属于该客户的cookie返回(响应头
set-cookie
) {:width=300 height=300} - 有了cookie之后,再向服务器发送请求时,就会自动携带cookie {:width=250 height=250}
使用较少,了解即可
- 禁用所有cookie:打开网页设置页面,搜索
cookie
,找到管理和删除 cookie 和站点数据
->阻止第三方Cookie
,开启后很多网页无法正常使用 {:width=150 height=150} - 删除cookie:还是上面的页面,点击
查看所有 Cookie和站点数据
,就可以查看和删除各网页的cookie {:width=200 height=200} 如果在一个网站上已经登录,此时删除了该网站的cookie,就无法自动登录了 - 查看cookie:
- edge:还是上面的
查看所有 Cookie和站点数据
,点击右侧箭头展开 {:width=100 height=100} {:width=500 height=500} 其中名称是键、内容是值 - chrome:新版chrome无法通过简单的方式查看,以下是旧版的操作 {:width=150 height=150} 点击箭头展开 {:width=300 height=300}
- 所有浏览器都可使用的方法:f12->控制台->
document.cookie
{:width=100 height=100} 注:不同浏览器的cookie不共享。在一个浏览器中登录一个网页,在另一个浏览器中打开这个网页时仍需登录 为什么打开一个网页,会有显示有其它网页的cookie:网页中有其它的组件也需要cookie,进入一个网页时,会向其它多个网页发送cookie。此网页的cookie被称为第一方cookie,其它发送的cookie被称为第三方cookie
- edge:还是上面的
设置cookie:
res.cookie(cookie名, cookie值)
这种方式在浏览器关闭时销毁cookieres.cookie(cookie名, cookie值, {maxAge: cookie存在时间})
指定cookie的存在时间,单位是ms
例1:
const express = require("express");
const app = express();
app.get('/set-cookie', (req, res) => {
res.cookie('name', 'abc'); //设置cookie
res.send("home");
});
app.listen(9000);
进入http://127.0.0.1:9000/set-cookie
后,响应头中可以看到set-cookie
{:width=150 height=150}
刷新页面,重新发送请求,就可以在请求体中看见刚才设置的cookie
{:width=250 height=250}
一个问题:如果访问http://127.0.0.1:9000
,请求中会不会携带cookie?因为域名没变,所以还是会带这个cookie
例2:cookie的生命周期
const express = require("express");
const app = express();
app.get('/set-cookie', (req, res) => {
res.cookie('name', 'abc', { maxAge: 30*1000 }); //设置cookie生命周期
res.send("home");
});
app.listen(9000);
先进入http://127.0.0.1:9000/set-cookie
设置cookie,再打开http://127.0.0.1:9000
,可以在请求头中看到cookie,30s后再次进入,就不会看到cookie了
{:width=150 height=150}
可以看到cookie的生命周期、过期时间
注:这里面Max-Age
的单位是s
删除cookie:res.clearCookie(cookie名)
例:
const express = require("express");
const app = express();
app.get('/set-cookie', (req, res) => {
res.cookie('name', 'abc');
res.cookie('theme', 'blue');
res.send("home");
});
app.get('/remove-cookie', (req, res) => {
res.clearCookie('name', 'abc');
res.send("删除成功");
});
app.listen(9000);
进入http://127.0.0.1:9000/set-cookie
设置cookie
{:width=35 height=35}
进入http://127.0.0.1:9000/remove-cookie
删除cookie
{:width=50 height=50}
刷新页面,请求头中cookie只剩下theme
{:width=50 height=50}
可以看出删除cookie的原理:将过期时间设为1970年,让cookie过期
读取cookie:使用包cookie-parser,安装npm i cookie-parser
,它本质是一个中间件
使用req.cookies
获取,返回一个对象,键是cookie名,值是cookie值
例:
const express = require("express");
const cookie_parser = require("cookie-parser");
const app = express();
app.use(cookie_parser());
app.get('/set-cookie', (req, res) => {
res.cookie('name', 'abc');
res.cookie('theme', 'blue');
res.send("home");
});
app.get('/get-cookie', (req, res) => {
console.log(req.cookies);
res.send(`你好,${req.cookies.name}`);
});
app.listen(9000);
进入http://127.0.0.1:9000/set-cookie
设置cookie,之后访问http://127.0.0.1:9000/get-cookie
{:width=300 height=300}
就可以获取到之前设置的cookie
基于cookie实现,形式类似于cookie(对象--键值对)
在cookie中,设置完cookie后,访问网址时会发送一个请求头Cookie
,其中包含例如name/theme等信息;而在session中,它发送一个唯一的sessionid,用于识别每个用户的session数据,这个sessionid的键名可以自定义
与cookie的区别:
- 存放位置:cookie在浏览器端,session在服务器端
- 安全性:cookie以明文形式存放在客户端,安全性低;session存放在服务器端,用户无法查看,安全性相对较好
- 存放数据大小:
- cookie设置内容过多会增大报文体积,影响传输效率,并且浏览器限制单个cookie最大存放量为4K,单个域名下存储数量也有限制
- session只通过报文传输sid,数据都在服务器中,不影响传输效率,也没有存储限制
使用express-session
包对session进行操作,使用connect-mongo
包将session存放到mongodb中(因为默认情况下express-session是将session存到内存中,不易查看)
安装:npm i express-session connect-mongo
const session = require("express-session");
const mongo_store = require("connect-mongo");
app.use(session({})); //设置中间件
//路由中
req.session.键名 = 键值; //获取/设置session
req.session.destroy(()=>{}); //销毁session,回调函数在删除成功时执行
session({})
中传入的对象参数:
name
sessionid的键名,默认为"connect.sid"secret
密钥/签名/加盐,提高加密等级,有了密钥之后,即使知道sid也无法破解saveUninitialized
是否为每次请求都自动设置一个cookie来存储sid,就是如果用户没有用session时,还要不要创建一个session。如设置为false,就是不创建;如果想对匿名用户也做信息记录,就可设为trueresave
是否在每次请求时重新保存session。因为session有生命周期,假设生命周期为20min,第一次访问后设置session,正常情况下20min后session过期;在这20min内,客户向服务器发送请求,如果设为true,则发送时更新session,重新计时20min;反之不重新计时,仍会在20min后过期store
存储位置,这里使用mongo_store.create({mongoUrl:"mongodb://主机名:端口号/数据库名称"})
连接数据库进行存储,它会创建一个名为session
的集合cookie
服务器响应cookie(set-cookie)的特性httpOnly
设置为true时,限制前端通过js操作cookie(例如使用document.cookie
获取当前页面的cookie)maxAge
session生命周期,单位为ms
为什么用的是req.session
而不是res.session
:可以理解为用户发送请求,中间件自动去查询session,将结果返回,之后我们根据结果渲染页面就行了
例:
const express = require("express");
const session = require("express-session");
const mongo_store = require("connect-mongo");
const app = express();
app.use(session({
name: "sid",
secret: 'jiamizifuchuan',
saveUninitialized: false,
resave: true,
store: mongo_store.create({
mongoUrl: "mongodb://127.0.0.1:27017/test"
}),
cookie: {
httpOnly: true,
maxAge: 1000 * 3600 //1h
}
}));
app.get('/login', (req, res) => { //设置
//这里通过查询字符串判断是否满足登录条件
if (req.query.username === "admin" && req.query.password === "woshimima") {
//设置session
req.session.username = 'admin';
//登录成功
res.send("登录成功");
} else {
res.send("登录失败");
}
});
app.get('/cart', (req, res) => { //获取
if (req.session.username) {
res.send(`您好${req.session.username},这里是购物车页面`);
} else {
res.send("您还没有登录");
}
});
app.get('/logout', (req, res) => { //销毁
req.session.destroy(() => {
res.send("退出成功");
})
});
app.listen(9000);
- 登录:进入
http://127.0.0.1:9000/login?username=admin&password=woshimima
登录,可以看到响应头中包含set-cookie,之后再次访问,请求头中可以看到cookie。在session的有效期内,访问页面时,请求头中会一直包含cookie {:width=150 height=150} {:width=300 height=300} 数据库也可以看到设置的session,其中包含username项 {:width=70 height=70} 注:响应头中的set-cookie只在第一次登录成功时才有,因为后面已经登录,不用再设置 - 访问页面:进入
http://127.0.0.1:9000/cart
{:width=70 height=70} 请求头中包含cookie(sid) - 退出登录:进入
http://127.0.0.1:9000/logout
,之后再进入http://127.0.0.1:9000/cart
,就是未登录状态了 {:width=150 height=150} 虽然请求头中也会包含cookie,但因为数据库中这条sid已被删除,其sid不能与数据库匹配,所以是未登录状态
即B网站向A网站发送请求,同时携带A网站的cookie 比如我现在在A网站上已经登录,在另一个标签页中打开B网站,它有一个link链接
<link rel="stylesheet" href="A网站/logout">
如果A网站的logout路由(退出登录)是get请求,就能实现访问B网站,退出A网站的登录 一种解决方法:在A网站中使用post路由 因为link、script、img等标签的src/href都是get请求,所以如果是增删改数据、需要对cookie进行操作的路由尽量用post
在routes/web中新建文件auth.js
和login.js
,分别用于实现注册和登录功能,并在app.js中引入
var authRouter = require('./routes/web/auth');
var loginRouter = require('./routes/web/login');
app.use('/reg', authRouter);
app.use('/login', loginRouter);
在models中新建模型文件UserModel.js
,用于存放用户的session数据
const mongoose = require('mongoose');
let UserSchema = new mongoose.Schema({
username: {
type: String,
require: true
},
password: {
type: String,
require: true
}
});
let UserModel = mongoose.model('users', UserSchema);
module.exports = UserModel;
注册功能:将注册的静态网页存入views/auth中,该文件夹用于存放注册相关内容 修改表单:为输入框添加name属性,为表单添加action,并将后缀从html改成ejs
<!-- reg.js -->
<form method="post" action="/reg">
<input name="username" type="text" class="form-control" id="item" />
<input name="password" type="text" class="form-control" id="time" />
</form>
密码加密:使用md5包,它提供一种单向加密算法(只能通过原数据找到加密数据,不能通过加密数据找原数据),安装npm i md5
/* auth.js */
const express = require("express");
const md5 = require("md5");
const router = express.Router();
const UserModel = require("../../models/UserModel"); //导入模型
router.get('/', (req, res) => { //渲染注册页面
res.render("auth/reg");
});
router.post('/', (req, res) => { //登录的post请求
const { username, password } = req.body;
if (!(username && password)) {
res.render('error', { msg: "注册失败", url: "/reg" });
}
const md5_pw = md5(password);
UserModel.create({ username: username, password: md5_pw }, (err, data) => {
if (err) {
res.render('error', { msg: "注册失败", url: "/reg" });
return;
}
res.render('success', { msg: "注册成功", url: "/login" });
});
});
module.exports = router;
{:width=250 height=250} {:width=250 height=250}
登录功能:在views/auth
中新建login.ejs
,将“注册”改成“登录”,表单提交路径改成/login
/* login.js */
const express = require("express");
const md5 = require("md5");
const router = express.Router();
const UserModel = require("../../models/UserModel"); //导入模型
router.get('/', (req, res) => { //渲染注册页面
res.render("auth/login");
});
router.post('/', (req, res) => { //登录的post请求
const { username, password } = req.body;
if (!(username && password)) {
res.render('error', { msg: "用户名或密码不能为空", url: "/login" });
}
UserModel.findOne({ username: username, password: md5(password) }, (err, data) => {
if (err) {
res.render('error', { msg: "登录,请稍后再试", url: "/login" });
return;
}
if (!data) {
res.render('error', { msg: "账号或密码错误", url: "/login" });
return;
}
res.render('success', { msg: "登录成功", url: "/account" });
});
});
module.exports = router;
{:width=400 height=400} {:width=400 height=400}
登录后写入session,并返回sessionid,一遍下次能自动登录
- 在app.js中引入包,并设置中间件
/* app.js */ const session = require("express-session"); const mongo_store = require("connect-mongo"); const { DBHOST, DBPORT, DBNAME } = require("./config/config"); app.use(session({ name: "sid", secret: 'jiamizifuchuan', saveUninitialized: false, resave: true, store: mongo_store.create({ mongoUrl: `mongodb://${DBHOST}:${DBPORT}/${DBNAME}` }), cookie: { httpOnly: true, maxAge: 1000 * 3600 * 24 * 7 //7天 } }));
- 因为是在登录成功后才写入session,所以代码应写在
login.js
的res.render('success', { msg: "登录成功", url: "/account" })
前/* login.js */ router.post('/', (req, res) => { //登录的post请求 ... //获取输入的用户名和密码 UserModel.findOne({ username: username, password: md5(password) }, (err, data) => { ... //登录失败return //写入session req.session.username = data.username; req.session._id = data._id; //数据库中id,方便后续查询 res.render('success', { msg: "登录成功", url: "/account" }); }); });
登录成功后,可以看到响应头中设置的session和数据库中存储的session {:width=400 height=400}
登录检测:当用户访问account/create页时,根据是否登录以及登录的session来返回结果,因此应写在account.js和create.js的路由中 因为account.js的所有路由都需要进行登录检测,所以封装到中间件中,新建一个文件夹middleware来存储中间件,登录检测的中间件写在其中的check_login.js中
/* check_login.js */
//检测登录中间件
module.exports = (req, res, next) => {
if (!req.session.username) { //没登录
return res.redirect('/login'); //跳转到登录界面
}
next();
}
在account.js和create.js中调用:
/* account.js */
//导入检测登录中间件
const check_login = require("../../middleware/check_login");
//响应表单提交的post请求
router.post('/', check_login, function (req, res) {...});
router.get('/', check_login, function (req, res) {...});
router.get('/:id', check_login, function (req, res) {...});
/* create.js */
//导入检测登录中间件
const check_login = require("../../middleware/check_login");
router.get('/', check_login, function (req, res, next) {...});
退出登录,同时防止CSRF跨站请求伪造:在routes/web文件夹中新建路由规则logout.js,并在app.js中调用
/* app.js */
var logoutRouter = require('./routes/web/logout');
app.use('/logout', logoutRouter);
/* logout.js */
const express = require("express");
const router = express.Router();
router.post('/', (req, res) => {
req.session.destroy(() => {
res.render('success', { msg: "退出登录成功", url: "/login" });
});
});
module.exports = router;
在list.ejs中添加对应的按钮:因为是post请求,要用表单而不能是a
<!-- list.ejs -->
<div class="row">
<div class="col-xs-12 col-lg-8 col-lg-offset-2">
<div class="row text-right" style="padding-top: 5px;">
<div class="col-xs-12">
<form action="/logout" method="post">
<button class="btn btn-danger">退出登录</button>
</form>
</div>
</div>
<div class="row">...</div>
...
</div>
</div>
首页和404页面:在routes/web文件夹中新建首页路由规则index.js,并在app.js中调用;404的路由在app.js中已经提供了,直接更改即可
/* app.js */
var indexRouter = require('./routes/web/index');
app.use('/', indexRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
res.render('404');
});
/* index.js */
const express = require("express");
const router = express.Router();
router.get('/', (req, res) => {
res.redirect('/account');
});
module.exports = router;
在views中新建404.ejs
:
<!-- 404.ejs -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404</title>
</head>
<body>
<h1>404 NOT FOUND</h1>
</body>
</html>
如果还想插入图片,要注意保存到public文件夹中,不要放到views中
token是服务端生存并返回给HTTP客户端的一串加密字符串,其中保存有用户信息,主要用于移动端APP 工作流程:
- 和cookie/session类似,都是先根据请求校验身份,校验通过后响应token(一般是在响应体中) {:width=300 height=300}
- 后续发送请求时,将token添加在请求报文中(一般是请求头),服务器对token校验并取出用户信息 {:width=300 height=300} 注:token必须手动添加在请求报文中,而cookie/session都是自动携带
特点:
- 服务端压力更小:数据存储在客户端
- 相对更安全:数据加密、可避免CSRF跨站请求伪造(不能自动携带token)
- 扩展性更强:服务间可以共享(一个token用于多个服务)、增加服务器节点更简单(服务器只需检验token,无需存储,这样增加服务器时就不用将原来的信息全部移入新服务器)
JWT(JSON Web Token)目前最流行的跨域认证解决方案,可用于token的身份验证
使用jsonwebtoken包创建/校验token,安装:npm i jsonwebtoken
基本使用:
const jwt = require("jsonwebtoken");
//创建token
let token = jwt.sign(用户数据, 加密字符串, 配置对象);
//校验token
jwt.verify(token, 加密字符串, (err, data)=>{});
- 用户数据一般用对象形式
- 配置对象:
{expiresIn: 生命周期}
,单位是秒 err
是错误对象;data
是一个对象,包括用户数据、token创建时间和过期时间- 当token过期时,就无法通过token校验
例:
const jwt = require("jsonwebtoken");
let token = jwt.sign({
name: "abc"
}, 'woshijiamizifuchuan', {
expiresIn: 10 //10s
});
console.log(token);
jwt.verify(token, 'woshijiamizifuchuan', (err, data) => {
if (err) {
console.log('校验失败');
return;
}
console.log(data);
});
过10s后进行校验:
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWJjIiwiaWF0IjoxNzI3NjAwNDAxLCJleHAiOjE3Mjc2MDA0MTF9.cdI2VXez3Akd2JzSrYN07zAlNWdrwnCzkIqVJJR2--U";
jwt.verify(token, 'woshijiamizifuchuan', (err, data) => {
if (err) {
console.log('校验失败');
return;
}
console.log(data);
});
显示“校验失败” {:width=100 height=100}
在session的记账本案例中,我们进行了网页端的登录校验,但没有进行接口(APP端)的登录校验
接口端只作登录校验,因此只需将之前在routes/web中写的login.js复制到routes/api文件夹中,将res.render改成res.json,并在app.js中引入
注:首先需要安装jwtnpm i jsonwebtoken
登录响应token:
将密钥写在config/config.js中:
/* config.js */
module.exports = {
DBHOST: '127.0.0.1',
DBPORT: 27017,
DBNAME: "test",
secret: 'woshijiamizifuchuan'
}
/* login.js */
const jwt = require("jsonwebtoken");
const { secret } = require("../../config/config");
router.post('/', (req, res) => { //登录的post请求
const { username, password } = req.body;
UserModel.findOne({ username: username, password: md5(password) }, (err, data) => {
if (err) {
res.json({
code: '2001',
msg: "数据库读取失败",
data: null
});
return;
}
if (!data) {
res.json({
code: '2002',
msg: "用户名或密码错误",
data: null
});
return;
}
//创建token
const token = jwt.sign({
username: data.username,
_id: data._id
}, secret, {
expiresIn: 3600 * 24 * 7 //7天
});
//响应token
res.json({
code: '0000',
msg: "登录成功",
data: token
});
});
});
/* app.js */
const loginRouterAPI = require('./routes/api/login');
app.use("/api/login", loginRouterAPI);
{:width=400 height=400} {:width=230 height=230}
token校验:在获取记账本列表的路由(account.js)中进行
一个问题:如何获取服务端传来的token?这是服务端自行决定的,一般放在请求头中,名称不固定(token/tk/userkey/...)
因为其中的所有路由都需要token校验,所以封装成中间件check_token.js存入middleware文件夹中
改进:校验token成功后,保存用户的信息
因为在校验token后的查询数据库操作中,需要根据用户的相关信息进行查询,而不是查询全部数据
方法:将用户的信息保存在请求对象req
中
/* check_token.js */
const jwt = require("jsonwebtoken");
const { secret } = require("../config/config");
module.exports = (req, res, next) => {
//获取token
const token = req.get('token');
if (!token) {
res.json({
code: "2003",
msg: 'token缺失',
data: null
});
return;
}
//校验token
jwt.verify(token, secret, (err, data) => {
if (err) {
res.json({
code: "2004",
msg: 'token校验失败',
data: null
});
return;
}
//保存用户信息
req.user = data;
//校验成功,继续执行代码
next();
});
}
/* account.js */
const check_token = require("../../middleware/check_token");
router.post('/', check_token, function (req, res) {
console.log(req.user); //获取用户信息
});
//主页面
router.get('/', check_token, function (req, res) {...});
//删除数据
router.delete('/:id', check_token, function (req, res) {...});
//获取单个账单信息
router.get('/:id', check_token, function (req, res) {...});
//更新单个账单信息
router.patch('/:id', check_token, function (req, res) {...});
{:width=400 height=400} 注:参数值的token不加引号 {:width=100 height=100} 这里的思路其实与req.session是一样的,为什么req中有用户数据?是因为中间件函数对req对象进行了处理
即只能在本机使用的域名,一般在开发阶段使用
编辑文件C:\Windows\System32\drivers\etc\hosts
加上127.0.0.1 www.baidu.com
,此时在新窗口中进入网址www.baidu.com:3000
,就可以访问之前的127.0.0.1:3000
还可以更改服务的端口号,这样直接输入www.baidu.com
就可以访问127.0.0.1
(HTTP改成80端口)
在express框架中,是修改/bin/www的var port = normalizePort(process.env.PORT || '3000')
的3000为80
原理:在地址栏输入域名后,浏览器会先进行DNS(Domain Name System)查询,获取该域名对应的IP,浏览器向这个IP发送请求
- DNS查询:浏览器向DNS服务器发送请求,它根据域名返回IP
(可通过命令行
ipconfig /all
查看本机的DNS服务器) - hosts文件可以设置域名与IP的映射关系。请求发送前,会优先查询该文件来获取域名的IP地址;如果hosts文件中有该域名,就不会再向DNS服务器发送请求,而是直接使用hosts文件中对应的IP