一、介绍
会话控制就是 对会话(根据用户)进行控制。
HTTP 是一种无状态的协议,它没有办法区分多次的请求是否来自于同一个客户端(客户),无法区分用户,而产品中又大量存在这样的需求,所以需要会话控制解决该问题。
常见的会话控制技术有三种:
- cookie
- session
- token
二、cookie
1、是什么
cookie 是 HTTP 服务器发送到用户浏览器并保存在本地的一小块数据(不需要前端手动存储,后端设置cookie以后,响应头上会上,之后,前端发送任何请求,都会自动将 cookie 携带在请求头中)。
cookie 是保存在浏览器的一小块数据,是按照域名划分的。
1-2、特点
浏览器向服务器发送请求时,会自动将 当前域名 下可用的 cookie 设置在请求头中,然后传递给服务器。
1-3、cookie 的运行流程
填写账号和密码校验身份,校验通过后下发 cookie
有了 cookie 之后,后续向服务器发送请求时,会自动携带 cookie
1-4、浏览器操作 cookie
浏览器操作 cookie 相对较少,仅做了解
- 禁用所有 cookie
- 删除 cookie
- 查看 cookie
控制台 -》 应用(Application) -》Cookies
控制台 -》 网络(NetWork) -》 请求头
浏览器 -》 设置
1-5、代码对 cookie 增删查
js
const express = require('express')
const app = express()
// 获取 cookie 时需要用到 cookie-parser
const cookieParser = require('cookie-parser')
app.use(cookieParser())
app.get('/', (req, res) => {
res.send('home')
})
// 设置 cookie
app.get('/setCookie', (req, res) => {
// 设置 cookie,会在浏览器关闭(退出后台)的时候销毁
// 如果提前打开了两个 独立 的tab页,把 访问http://127.0.0.1:3000/的页面关掉以后
// 另一个页面还是会存在 cookie
res.cookie('name', 'zhangsan')
// 设置 cookie 键值对和过期时间,maxAge 单位是毫秒,30 * 1000 = 30s
// cookie 会在 30 秒之后销毁,常用于做 7天免登录
// res.cookie('name','zhangsan',{maxAge: 30 * 1000})
res.cookie('theme', 'blue')
res.send('cookie')
})
// 删除 cookie
app.get('/removeCookie', (req, res) => {
// 删除 cookie,参数是必填项
res.clearCookie('theme')
res.send('成功删除cookie')
})
// 获取 cookie
app.get('/getCookie', (req, res) => {
// 获取cookie
// res.send( req.cookies)
res.send( req.cookies.name)
// res.send( req.cookies.theme)
})
app.listen(3000, () => {
console.log('服务器已经启动....')
})
三、session
1、是什么
session 是保存在 服务端的一块数据,保存当前访问用户的信息。
2、作用
实现会话控制,可以识别用户的身份,快速获取当前用户的相关信息。
3、运行流程
填写账号、密码校验身份,校验通过后创建 session 信息,然后将 session_id 的值通过响应头返回给浏览器
有了 cookie ,下次发送请求时会自动携带 cookie,服务器通过 cookie 中的 session_id 的值确定用户的身份
4、代码对 session 增删查
js
const express = require('express')
// 下载 npm i express-session connect-mongo
// 引入 express-session、connect-mongo
const session = require('express-session')
const MongoStore = require('connect-mongo')
const app = express()
app.use(session({
name: 'sid', // 设置 cookie 的 name,默认值是:connect.sid
secret: 'zhangsan', // 参与加密的字符串(又称签名)
saveUninitialized: false, // 是否为每次请求都设置一个cookie用来存储session的id
resave: true, // 是否在每次请求时重新保存session,
// 场景:为 true 时,假如一个网站的session的过期时间时 20 分钟,每次请求之后,会重置这个时间
// 直到不做任何请求之后的 20 分钟,才会销毁session_id
store: MongoStore.create({
mongoUrl: 'mongodb://127.0.0.1:27017/bilibili' // 数据库的连接配置
}),
cookie: {
httpOnly: true, // 开启后前端无法通过 js 操作(document.cookie)获取到 cookie
maxAge: 1000 * 60 * 5 // sessionID 的过期时间 5 分钟
}
}))
// 设置 session
app.get('/login', (req, res) => {
if(req.query.username === 'admin' && req.query.password === 'admin') {
// 设置 session 信息
req.session.username = 'admin'
req.session.uid = '258aefefdf'
res.send('登录成功')
} else {
res.send('登录失败')
}
})
// session 的读取
app.get('/cart', (req, res) => {
// 判断用户是否登录--》检测session 是否存在用户数据
if(req.session.username) {
res.send(`购物车页面欢迎你 ${req.session.username}`)
} else {
res.send('你还没有登录')
}
})
// session 销毁
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.send('退出成功')
})
})
app.get('/', (req, res) => {
res.send('首页')
})
app.listen(3000, () => {
console.log('服务启成功')
})
四、session 和 cookie 的区别
cookie 和 session 的区别主要有以下几点:
- 存放的位置
cookie:浏览器端
session: 服务端 - 安全性
cookie 是以明文的方式存放在客户端的,安全性相对较低
session 存放在 服务端,所以安全性相对较好 - 网络传输量
cookie 设置内容过多会增大报文体积,影响传输效率
session 数据存储在服务器,只能通过 cookie 传递 id,所以不影响传输效率 - 存储显示
浏览器限制单个 cookie 保存的数据不能超过 4k,且单个域名下的存储数量也有限制
session 数据存储在 服务器 中,所以没有这个限制
五、token
1、是什么
token 时服务端生成并返回给 HTTP 客户端的一串加密字符串,token 中保存着用户信息
2、作用
实现会话控制,可以识别用户的身份,主要用于移动端 app
3、token 的工作流程
填写账号和密码校验身份,校验通过后响应 token,token 一般是在响应体中返回给客户端
后续发送请求时,需要手动将 token 添加在请求报文中,一般是放在请求头中
4、特点
- 服务端压力更小
数据存储在客户端 - 相对更安全
数据加密
可以避免 CSRF(跨站请求伪造) - 扩展性更强
服务可以共享
增加服务节点更简单
5、JWT
JWT (JSON Web Token)是目前最流行的跨域认证解决方案,可用于 基于 token 的身份验证
JWT 使 token 的生成与校验更规范
npm i jsonwebtoken
js
const jwt = require('jsonwebtoken')
// 创建 token
// let token = jwt.sign({
// username: 'zhangsan'
// }, 'nodejs-token', {
// expiresIn: 60 // 单位是 秒
// })
// token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwiaWF0IjoxNzIxODkyMjMzLCJleHAiOjE3MjE4OTIyOTN9.EpSHpeYuf7KOjptpTftrSZyAJ1234xvcgnvc6_PBdYQ
// console.log(token)
let t = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwiaWF0IjoxNzIxODkyMjMzLCJleHAiOjE3MjE4OTIyOTN9.EpSHpeYuf7KOjptpTftrSZyAJ1234xvcgnvc6_PBdYQ'
// 校验 token
jwt.verify(t, 'nodejs-token', (err, data) => {
if(err) {
console.log('校验失败')
return
}
console.log(data) // { username: 'zhangsan', iat: 1721892343, exp: 1721892403 }
})
流程:
- 登录成功后,服务端生成 token,并返回给前端
js
const express = require('express')
const router = express.Router()
const md5 = require('md5')
const UserModel = require('../../models/UserModel')
const jwt = require('jsonwebtoken')
// 登录功能的路由
router.post('/login', (req, res) => {
let { username, password } = req.body
UserModel.findOne({username, password: md5(password)}).then((data) => {
if(!data) {
return res.json({
code: '2001',
msg: '账号或密码错误',
data: null
})
}
req.session.username = data.username
req.session._id = data._id
// res.render('success', {msg:'登录成功',url:'/account'})
// 创建当前用户的 token
// nodejs-token 为加密字符串
let token = jwt.sign({
username: data.username,
_id: data._id
}, 'nodejs-token', {
expiresIn: 60 * 60 * 24 * 7 // token 过期时间为 1 周
})
res.json({
code: '0000',
msg: '登录成功',
data: token
})
}).catch(() => {
// res.render('success', {msg:'登录失败',url:'/login'})
// res.status(500).send('登录失败,请稍后重试')
res.json({
code: '2002',
msg: '登录失败,请稍后重试',
data: null
})
})
})
module.exports = router
- 前端拿到 token 后,在每次请求时,都要将 token 写到请求头中
- 前端请求时,将token放入请求头后,后端处理请求:
api/auth.js
js
const express = require('express');
const router = express.Router();
const AccountModel = require('../../models/AccountModel')
const dayjs = require('dayjs')
// 配置中间件 =》校验 token
const CheckTokenMiddleware = require('../../middlewares/CheckTokenMiddleware')
router.post('/account', CheckTokenMiddleware,(req, res, next) => {
AccountModel.create({
...req.body,
time: dayjs(req.body.time).format()
}).then((data) => {
console.log(data)
// res.render('success', {msg:'添加成功了',url:'/account'})
res.json({
code: '0000',
msg: '创建成功',
data: data
})
}).catch(() => {
// res.status(500).send('删除失败了')
res.json({
code: '1002',
msg: '创建失败',
data: null
})
})
});
router.get('/account/remove/:id', CheckTokenMiddleware, (req, res) => {
//获取 params 的 id 参数
let id = req.params.id
AccountModel.deleteOne({_id: id}).then((data) => {
// console.log(data)
// res.render('success', {msg:'删除成功了',url:'/account'})
res.json({
code: '0000',
msg: '删除成功',
data: {}
})
}).catch(() => {
// res.status(500).send('删除失败了')
res.json({
code: '1003',
msg: '删除失败',
data: null
})
})
})
module.exports = router;
models/AccountModel.js
js
const mongoose = require('mongoose')
const AccountSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
time: Date,
type: {
type: Number,
default: -1
},
account: {
type: Number,
required: true
},
remarks: String
})
const AccountModel = mongoose.model('account', AccountSchema)
module.exports = AccountModel
middlewares/CheckTokenMiddleware.js
js
const jwt = require('jsonwebtoken')
module.exports = (req, res, next) => {
const token = req.get('token')
jwt.verify(token, 'nodejs-token', (err, data) => {
if(err) {
return res.json({
code: '2003',
msg: 'token 校验失败',
data: null
})
}
next()
})
}
六、扩展
1、本地域名
所谓本地域名就是 只能在本机使用的域名 ,一般在开发阶段使用
1-1、操作流程
编辑文件 C:\Windows\System32\drivers\etc\hosts
127.0.0.1 www.baidu.com
如果修改失败, 可以修改该文件的权限
1-2、原理
在地址栏输入 域名 后,浏览器会先进行 DNS (Domain Name System)查询,湖区该域名对应的 IP 地址
请求会发送到 DNS 服务器,可以 根据域名返回 IP 地址
可以通过 ipconfig /all
查看本机的 DNS 服务器
hosts
文件也可以设置域名与 IP 的映射关系,在发送请求前,可以通过该文件获取域名的 IP 地址