主题
会话控制介绍与cookie(Day13 2/3)
本页对应 Day13_Node&会话控制,共 7 段视频,约 56 分钟。
学习目标
- 能够配置
express-session与session-file-store,将 session 持久化到文件,并验证不同客户端生成独立 session 文件且文件名对应 sessionID。 - 能够从存储位置、安全性、网络传输量、大小限制四个维度对比 session 与 cookie 的区别。
- 能够设计并挂载用户模块的注册页、执行注册、登录页、执行登录、退出登录 5 个路由,并说明各路由的请求方法与完整路径。
- 能够创建 users 集合对应的 Schema 与 Model,实现注册用户信息写入数据库,并通过
unique: true验证用户名不可重复注册。 - 能够使用 MD5 对注册密码加密后再存入数据库,并验证相同密码加密结果一致、不同密码加密结果不同。
- 能够按步骤分析记账本项目添加登录注册功能的需求,包括注册、登录、退出、全局登录验证以及账单与用户 ID 关联的实现思路。
知识点
从“服务器认不出你”开始
HTTP 协议是无状态协议:每次请求彼此独立,服务器无法仅凭协议本身判断多次请求是否来自同一客户端。为什么需要会话控制?因为实际业务里大量场景都要求“认出你”,例如登录后保持登录状态、购物车、权限校验、只查看自己的账单等。怎么想?不要误以为 HTTP 坏了,无状态是设计使然;我们要在应用层补一层“会话标识”,让服务器能根据标识找回这次交互的上下文。怎么做?客户端第一次请求后,服务器创建会话并返回一个标识,后续请求携带这个标识,服务器据此读取会话数据。易错点:会话标识不是密码,不能随意暴露;会话要有有效期和销毁机制。
会话控制到底控制什么
会话可以理解为“同一客户端与服务器在一段时间内的交互上下文”。会话控制主要做四件事:创建会话、保存会话数据、随请求识别会话、在退出或过期时销毁会话。为什么需要?因为只有识别出“还是你”,服务器才能把登录状态、用户信息等关联起来。怎么想?把“身份标识”和“业务数据”分开:客户端通常只保存标识,真正的数据尽量放服务器。怎么做?登录成功后创建会话,响应客户端;后续请求带着标识来,服务器查会话;退出时删除会话数据。易错点:不要把大量敏感信息直接塞给客户端;不要只做前端校验,后端仍需验证。
cookie、session、token 三种常见方案
本节笔记列出三种常见会话控制解决方案:cookie、session、token。
cookie 是客户端保存的小段数据,请求时会自动携带。为什么需要?它能轻量地保存标识或少量状态。怎么想?cookie 适合存“标识”,不适合存大量敏感数据。怎么做?服务端通过响应设置 cookie,浏览器后续请求自动带上。易错点:cookie 有数量和容量限制,且客户端数据可能被篡改,不能盲目信任。
session 把会话数据放在服务器端,客户端通常只保存 sessionID,而 sessionID 常放在 cookie 中。为什么需要?因为数据在服务器上,不直接暴露给客户端,安全性更好,网络传输量也更小。怎么想?转写稿里反复出现的“sction”“筛线”都应纠正为 session;“芒goDB”是 MongoDB。怎么做?以 Express 为例,用 express-session 中间件;默认存内存,也可改存文件或数据库。转写稿演示了存文件:安装 session-file-store,导入后把 store 配到 session 中间件里;运行后会出现 session 文件,文件名就是 sessionID。调试时可以看到:设置 session 后多出文件;换 IP 或 localhost 访问会产生新会话;有效期内刷新浏览器仍只有一个会话文件。易错点:内存存储不适合生产,重启或多进程会丢/不共享;文件或数据库存储要处理过期清理。
token 是服务端签发、客户端保存并在请求中携带的令牌方案。为什么需要?它常用于前后端分离、跨端请求等场景。怎么想?token 的重点是“携带并校验令牌”,不是把用户信息随便塞进去。怎么做?本节笔记只列出方案,转写稿未展开 token 的具体实现,因此不臆造代码;学习时应结合具体 token 方案再看签发、校验、过期和刷新。易错点:token 泄露同样危险,必须配合过期、刷新和安全传输。
用四个维度对比 session 与 cookie
转写稿总结了四个维度。存储位置:cookie 在客户端浏览器,session 在服务器,可存内存、文件或数据库。安全性:session 通常更安全,因为数据不直接暴露;cookie 在客户端,需谨慎。网络传输量:cookie 若存大量信息,每次请求都要携带;session 通常只带 sessionID,传输量更小。大小:cookie 受浏览器数量和容量限制;session 受服务器硬件限制,理论上限制小,但不能滥用。易错点:安全是相对的;session 存太多会压垮服务器。
放进项目时的推导与调试
转写稿用记账本演示了完整推导:先设计用户路由——GET 显示注册页、POST 执行注册、GET 显示登录页、POST 执行登录、GET 退出登录;注册要把用户信息加入 users 集合,需要 schema 和 model;用户名要设置 unique,否则重复用户名会导致登录时查出多条;密码不能明文存,转写稿用 MD5 加密后再入库,并提醒简单纯数字密码容易被撞库。登录时根据用户名和密码查数据库,找到则把用户信息存入 session,找不到则失败;退出时从 session 删除用户信息;再做全局登录验证,未登录访问受保护页面就跳登录页;账单集合增加用户 ID,查询只查当前用户的账单。易错点:前端验证提升体验,后端验证防伪造请求,二者都要有;MD5 不是“绝对安全”,密码应足够复杂。
本节要解决的问题
本节标题是“cookie”,但转写稿实际把 cookie 与 session 的区别、以及 session 在记账本项目中的落地串起来了。学习时不要只背概念,要能回答:session 存在哪、为什么更安全、为什么网络传输量小,以及登录注册时 session 到底怎么配合数据库使用。
知识点一:把 session 存到文件里
为什么需要:session 默认存在内存里,服务重启或进程变化后容易丢;数据量大时也不适合一直放内存。转写稿要演示“存到文件里”,就是为了让 session 可持久化,并观察每个浏览器客户端对应的 session 文件。
怎么想:session 存文件不是随便写个配置就行,背后要完成创建文件、写文件、按 sessionId 读文件、找到对应客户端等操作。这些读写过程不需要自己实现,可以用第三方模块。转写稿提到 session-file-store 配合 express-session;如果存 MongoDB,则可用 connect-mongo。
怎么做:先安装依赖,再在已有 session 中间件配置中追加 store。转写稿中的关键命令与代码片段是:
bash
npm i express-session session-file-storejs
const FileStore = require('session-file-store')(session);
app.use(session({
store: new FileStore()
}));启动后会出现 session 文件目录,每个浏览器客户端对应一个文件,文件名与 sessionId 相关。换 localhost 或换 IP 访问,会被视为新的会话,可能新建文件。
易错点:不要以为“加了 store 就自动有 secret 等全部配置”,实际是在原有 session 配置上追加 store;不要手动去猜文件名规则,调试时看 sessionId 和生成目录即可;换地址访问时不要误判为代码失效,那可能是新会话。
知识点二:session 与 cookie 的四维区别
为什么需要:这是经典面试题,也是理解登录态的基础。转写稿从存储位置、安全性、网络传输量、大小四个维度总结。
怎么想:cookie 信息存在客户端浏览器;session 存在服务器,可选内存、文件、数据库。安全性上,session 放在服务器,不直接暴露给客户端,通常更安全;cookie 存在用户侧,风险更高。网络传输量上,cookie 若存大量信息,每次请求都要携带;session 只在 cookie 中存一个 sessionId,大量信息留在服务器,所以传输量更小。大小上,cookie 受浏览器数量和容量限制;session 理论上受服务器硬件限制。
怎么做:回答时按四维展开,不要只说“session 更安全”。可以结合登录态:浏览器保存 sessionId,服务器根据 sessionId 找 session 数据。
易错点:不要说 session 一定“无限大”,转写稿明确说受服务器硬件制约;不要把 sessionId 和 session 数据混为一谈;不要忽略 cookie 仍可能参与保存 sessionId。
知识点三:记账本项目的登录注册路由与登录态
为什么需要:只讲概念不会用。项目需要注册、登录、退出、全局登录验证,并让账单与用户关联。
怎么想:先设计路由,再想每个路由做什么。转写稿设计 5 个路由:注册页 GET /users/reg、执行注册 POST /users/reg、登录页 GET /users/login、执行登录 POST /users/login、退出登录 GET /users/logout。注册是把用户信息加入 users 集合;登录是根据提交的用户名和密码查数据库,找到则登录成功,并把用户信息存入 session;退出是从 session 删除用户信息;全局验证是未登录访问受保护页面时跳转登录页;账单集合增加用户 ID,添加账单记录当前用户 ID,查询只查当前用户的账单。
怎么做:路由模块化后挂载;注册页和执行注册使用同一路径不同方法;登录成功后写 session;退出时清 session;受保护页面先判断 session 中是否有用户信息。
易错点:注册页是 GET,执行注册必须是 POST;登录成功才写 session;退出不是删数据库用户,而是删 session 中的登录信息;账单不加用户 ID 就无法区分“谁的账单”。
知识点四:用户名唯一与密码加密
为什么需要:转写稿调试时发现,重复注册 admin 仍能成功,登录时可能查出多条记录,无法判断密码对应谁。密码明文存数据库也不合适,数据库管理员可直接看到。
怎么想:用户名要唯一,可在 schema 中给用户名字段加 unique: true。密码不能明文存,使用 MD5 等哈希算法加密后再存。MD5 会把字符串变成固定 32 位结果,同样内容加密结果相同,不同内容结果不同,且不可反向解密;但简单纯数字密码容易被撞库,所以密码应复杂些。
怎么做:schema 片段可写成:
js
userName: { type: String, unique: true }安装并使用:
bash
npm i md5js
const md5 = require('md5');
userPWD: md5(req.body.userPWD)调试时先重复注册同名用户,观察仍成功;加 unique 后再次注册失败;再看数据库密码由明文变为 32 位哈希值。
易错点:unique 是集合层面的唯一约束,不是前端提示;前端验证提升体验,后端仍需验证,因为客户端可以伪造请求;本案例为流程演示省略了确认密码等验证,实际项目不能省;MD5 不是“绝对安全”,只是避免明文可见,弱密码仍可能被撞库。
本节要解决的问题
这一节围绕 session 做三件事:先把 session 从默认内存改存到文件;再对比 session 与 cookie 的区别;最后把记账本项目改造成“注册—登录—退出—登录校验—账单归属用户”的完整流程。转写稿里反复出现的“sction”“筛线”“三线”都是 session 的口误或同音转写,阅读时要统一纠正。
把 session 存到文件:为什么需要
session 默认存在服务器内存里。开发时够用,但服务重启后 session 会丢失,多进程部署时也不方便共享。转写稿中先选择“存到文件”,后面再演示 MongoDB,是为了让大家理解:session 的存储位置是可替换的,核心是找一个能读写 session 的 store。
怎么想:文件存储要解决“写文件、读文件、按 sessionId 找对应客户端”的问题。这些不用自己实现,直接用第三方模块 session-file-store,它配合 express-session 使用。若存 MongoDB,则对应 connect-mongo 一类模块。
怎么做:安装模块,导入后把它作为 session 的 store 加进原有中间件配置。转写稿中的关键动作是:
js
const session = require('express-session');
const FileStore = require('session-file-store')(session);
app.use(session({
store: new FileStore()
// 其他 session 配置沿用原案例
}));易错点:运行后项目里会多出存放 session 的目录和文件;每个浏览器客户端会对应一个文件,文件名基本就是 sessionId。换 localhost 或换 IP 访问,会被视为新的会话,不能拿“同一个服务器”来判断一定是同一个 session。有效期内反复刷新浏览器不会不断新建 session。
session 与 cookie 的区别
这是面试常见题,转写稿从四个维度总结:
- 存储位置:cookie 存在客户端浏览器;session 存在服务器,可放内存、文件、数据库。
- 安全性:session 相对更安全,因为大量信息不直接暴露给客户端;cookie 存在用户侧,被篡改或泄露的风险更高。
- 网络传输量:cookie 每次请求可能都要携带;session 通常只在 cookie 中保存一个 sessionId,大量数据留在服务器。
- 大小限制:cookie 受浏览器数量与容量限制;session 理论上受服务器硬件限制,但也不能无节制存储。
易错点:不要把“session 更安全”理解成绝对安全;sessionId 仍是身份凭证,泄露后同样有风险。
记账本项目:先做步骤分析
目标不只是注册登录,还要让账单和用户关联。转写稿把步骤拆成:路由设计、注册用户、登录、退出、全局登录验证、修改账单集合。
为什么需要全局登录验证:像 /account 这类页面,未登录不能访问;否则用户直接改 URL 就能进入。怎么想:登录成功后把用户信息存入 session;后续请求检查 session 中是否有用户信息,没有就跳登录页。怎么做:退出登录时删除 session 中的用户信息;账单集合每个文档增加用户 ID,添加账单记录当前用户,查询账单只查当前用户的账单。
易错点:只做前端跳转不够,后端路由也要校验;账单不加用户 ID,后面无法区分“谁的账单”。
用户路由设计
转写稿设计了 5 个路由:注册页 GET /users/reg、执行注册 POST /users/reg、登录页 GET /users/login、执行登录 POST /users/login、退出登录 GET /users/logout。路由模块化后挂载到应用里,模板放在 views/users 下,用 res.render('users/login')、res.render('users/reg') 渲染。
易错点:GET 负责展示页面,POST 负责提交数据;路径和挂载前缀要对应,别把 /users/reg 写成 /reg 后找不到。
注册用户:添加数据与用户名唯一
注册表单提交用户名和密码到 POST /users/reg。转写稿中只给用户名、密码加 name,确认密码不提交,因为本案例不做完整校验。后端通过模型把数据加入 users 集合。
js
const userModel = require('../models/user');
userModel.create({
username: req.body.username,
userPWD: req.body.userPWD
}).then(() => {
// 注册成功,跳转登录页
}).catch(() => {
// 注册失败,提示稍后再试
});调试时发现:重复注册同一个 admin 仍然成功,数据库出现多条同名用户,登录时就无法判断密码属于谁。解决方式是在 schema 中给用户名加唯一约束:
js
username: {
type: String,
unique: true
}设置后再次注册同名用户会失败。易错点:unique 失败时错误对象里可能有原因,但案例中只做了“注册失败,请稍后再试”的提示;另外修改 schema 后要确认服务已重启、字段名没有拼错。
密码加密:MD5 存储
密码明文存数据库不合适,数据库管理员可直接看到。转写稿采用 MD5:安装 md5 模块,注册时把密码加密后再存入数据库。
js
const md5 = require('md5');
userModel.create({
username: req.body.username,
userPWD: md5(req.body.userPWD)
});调试时注册 admin/123123,数据库中的密码变成 32 位字符串;再注册不同密码,加密结果不同。易错点:MD5 不可逆,但简单纯数字密码可能被撞库;实际项目应使用更合适的密码哈希方案。登录时也不是“解密”,而是把用户输入的密码同样加密后与数据库值比较。
本节小结
本节完成了 session 文件存储的配置与验证,梳理了 session 与 cookie 的四点区别,并把记账本项目的注册流程落地:路由设计、模板渲染、用户集合与模型、用户名唯一、密码 MD5 加密。后续还要继续做登录、退出、全局登录验证以及账单与用户 ID 的关联。
课堂画面






代码示例
1. 把 session 存储到文件(express-session + session-file-store)
js
// npm i express express-session session-file-store
const express = require('express');
const session = require('express-session');
// 这个包导出的是函数,必须把 session 传进去,才能拿到 FileStore 构造器
const FileStore = require('session-file-store')(session);
const app = express();
// session 相关配置都写在这里:默认存内存,改成存文件
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
store: new FileStore() // 一般不需要额外选项,直接 new FileStore() 即可
}));
// 访问这个地址就会设置一次 session
app.get('/session', (req, res) => {
if (req.session.views) {
req.session.views++;
res.send('访问次数:' + req.session.views);
} else {
req.session.views = 1;
res.send('第一次访问,已创建 session');
}
});
app.listen(3000, () => {
console.log('http://localhost:3000');
});要点:
require('session-file-store')(session)这一句最容易错:漏掉后面的(session),new FileStore()直接报错。store: new FileStore()是把默认的内存存储换成文件存储;不写这一项,服务一重启 session 全丢。- 启动后会自动多出一个
sessions目录,每个浏览器客户端对应一个 json 文件,文件名就是 sessionID,读的时候靠 sessionID 找文件。 - 用
localhost:3000和用本机 IP 访问,虽然指向同一个服务器,但算两个新会话,会各自生成一个文件。 - 有效期内反复刷新浏览器不会新增文件,只有过期后才会创建新的。
2. 注册用户:users 模型 + 唯一用户名 + MD5 加密
js
// npm i express mongoose md5 ejs
const express = require('express');
const mongoose = require('mongoose');
const md5 = require('md5');
const app = express();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false })); // 解析 post 请求体,req.body 才有值
// 1. schema + model
const userSchema = new mongoose.Schema({
userName: {
type: String,
unique: true // 用户名唯一,重复注册会失败
},
userPWD: String
});
const UserModel = mongoose.model('users', userSchema);
// 2. 注册页(get:展示页面)
app.get('/users/reg', (req, res) => {
res.render('users/reg');
});
// 3. 执行注册(post:写入数据库)
app.post('/users/reg', (req, res) => {
const userName = req.body.userName;
const userPWD = md5(req.body.userPWD); // 密码加密后再存
UserModel.create({ userName: userName, userPWD: userPWD }, (err) => {
if (err) {
// 用户名重复等原因会走到这里
return res.render('account/error', {
title: '注册失败,请稍后再试',
url: '/users/reg'
});
}
res.render('account/success', {
title: '注册成功,请登录',
url: '/users/login'
});
});
});
mongoose.connect('mongodb://127.0.0.1:27017/auth').then(() => {
app.listen(3000);
});要点:
unique: true必须写成对象形式{ type: String, unique: true },写在属性上表示每个文档该值不能重复;否则同名用户能注册两次,登录时查出多条就没法比对密码了。md5(req.body.userPWD)只加密密码,用户名不加密;MD5 是不可逆哈希,加密前后长度固定 32 位,登录时把用户输入的密码同样 md5 一遍再比对。- 模板表单必须
action="/users/reg" method="post",input 的name要和后端取的userName/userPWD完全一致,否则req.body拿不到值。 - 确认密码不给
name就不会提交;但格式校验前后端都要做——前端是用户体验,后端是防伪造请求。 - 密码明文存库,数据库管理员一眼就能看到,所以必须加密后再存。
3. 登录、退出登录与全局登录验证
js
const express = require('express');
const md5 = require('md5');
const UserModel = require('./models/User');
const accountRouter = require('./routes/account');
const app = express();
// app.use(session({...})) // 必须在下面的登录验证中间件之前
// 登录页
app.get('/users/login', (req, res) => {
res.render('users/login');
});
// 执行登录
app.post('/users/login', (req, res) => {
const userName = req.body.userName;
const userPWD = md5(req.body.userPWD);
UserModel.findOne({ userName, userPWD }, (err, data) => {
if (err) {
return res.render('account/error', { title: '登录失败', url: '/users/login' });
}
if (!data) {
// 查不到,说明没这个用户或密码不对
return res.render('account/error', { title: '用户名或密码错误', url: '/users/login' });
}
// 登录成功:把用户信息存到 session
req.session.user = data;
res.render('account/success', { title: '登录成功', url: '/account' });
});
});
// 退出登录:把用户信息从 session 中删除
app.get('/users/logout', (req, res) => {
req.session.destroy(() => {
res.render('account/success', { title: '退出成功', url: '/users/login' });
});
});
// 全局登录验证:没登录就跳登录页
app.use('/account', (req, res, next) => {
if (!req.session.user) {
return res.redirect('/users/login');
}
next();
});
app.use('/account', accountRouter);要点:
findOne({ userName, userPWD })同时按用户名和加密后的密码查,查不到data就是null→ 登录失败;用户名做了唯一限制,所以不会查出多条。- 登录成功后
req.session.user = data,之后同一个浏览器的每个请求都能从 session 里拿到用户信息,不用重复登录。 - 退出登录用
req.session.destroy()销毁整个会话;也可以delete req.session.user。 - 登录验证中间件必须放在受保护的
/account路由之前,同时要放行/users/login、/users/reg,否则登录页自己也被拦截,形成死循环。 app.use(session(...))一定要写在登录验证中间件之前,否则中间件里拿不到req.session。
常见错误
现象:配置了文件存储,重启后 session 仍丢失或项目启动报模块错误。
原因: 只装了express-session,漏装/漏引入session-file-store,或store未挂到 session 中间件。
修法: 安装并引入session-file-store,在express-session配置中挂store,重启后检查是否生成 session 文件目录。现象:同一账号换
localhost与127.0.0.1访问后出现两份 session,误判为登录状态异常。
原因: 浏览器按域名/地址隔离 cookie,sessionId 不同就会生成不同会话。
修法: 联调时固定使用同一协议、域名、端口访问;排查 session 问题先看 cookie 中的 sessionId 是否变化。现象:注册页提交后 404,或后端拿不到
req.body。
原因: 表单action/method与路由路径/请求方式不一致,GET 页面路由误当 POST 执行路由使用。
修法: 对齐表单action="/users/reg"、method="post"与router.post('/reg', ...);同时确认已配置请求体解析中间件。现象:
username已加unique: true,重复用户名仍能注册,或直接抛出数据库错误。
原因: 唯一索引未真正建立/历史重复数据未清理;或未捕获 duplicate key 错误。
修法: 清理重复数据并重建索引;注册逻辑捕获唯一键冲突,返回“用户名已存在”的友好提示。现象:数据库中密码仍是明文,或只在前端做了加密/校验就认为安全。
原因: 把前端校验当安全边界;后端入库前未做哈希处理。
修法: 后端入库前对密码做 MD5/哈希处理,登录时同样处理后比对;前端校验只作为体验优化保留。现象:登录成功后刷新仍像未登录,或退出后还能访问受保护页面。
原因: 登录成功未写入 session,退出未销毁 session,或全局登录校验未正确排除登录/注册路由。
修法: 登录成功写req.session.user,退出调用销毁逻辑,受保护路由统一校验 session 并重定向登录页。
练习
- 将案例中的 session 从默认内存存储改为文件存储,验证启动后是否生成 session 文件,并说明如何确认重启后会话是否保留。
- 分别用
localhost和127.0.0.1访问同一服务并登录,观察 session 文件/cookie 变化,写出排查结论。 - 为注册页补充“确认密码”前端校验,并说明后端仍需要补充哪些校验。
- 给用户模型添加用户名唯一约束,构造重复用户名注册场景,捕获错误并返回友好提示。
- 完成登录、退出、全局登录验证三个流程,列出至少 3 个受保护路由及其未登录时的跳转行为。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 设置session存储到文件中 | 07:00 |
| 2 | session和cookie的区别 | 02:09 |
| 3 | 记账本项目-添加注册登录功能 | 05:00 |
| 4 | 记账本项目-步骤分析 | 08:55 |
| 5 | 记账本项目-用户路由设计 | 06:37 |
| 6 | 记账本项目-注册用户01 | 11:54 |
| 7 | 记账本项目-注册用户02 | 15:05 |
小结
- 会话控制用于解决 HTTP 无状态导致的“多次请求无法识别同一客户端”问题。
- session 默认存内存,实际演示或部署可切换为文件、数据库等存储方式。
- session 与 cookie 可从存储位置、安全性、网络传输量、容量限制四个维度对比。
- 记账本项目应先设计用户路由,再补用户模型、注册、登录、退出和全局登录验证。
- 用户名必须唯一,且唯一性应由数据库约束兜底,并处理重复注册错误。
- 密码不能明文入库,后端应在存储前做哈希处理,登录时按同样方式比对。
- 登录成功写 session、退出销毁 session,受保护页面统一做登录校验。
- 账单数据要关联用户 ID,新增和查询都应按当前登录用户隔离。