全栈项目实战②:带登录与缓存的书签应用
JWT 登录 + 书签 CRUD + 热门缓存 + 头像上传的综合毕业项目
- 把注册登录、CRUD、缓存、上传串成一个完整项目
- 理解 JWT 无状态 vs Session 有状态的真实权衡
- 理解密码为什么必须用 bcrypt/argon2 慢哈希
- 知道缓存击穿在热门榜上的真实表现与对策
前面 m3 学了数据库 CRUD、m4-l2 学了错误中间件、m4-l3 学了头像上传。这一课把这些零件组装起来,造出一辆能开的车——一个真要登录才能用、热门书签还会加速的书签应用。
项目要拼哪些零件
书签应用功能:用户注册登录(JWT)、书签增删改查(SQLite)、热门书签走缓存、头像上传、统一错误中间件。这是 s7 的综合毕业项目。
登录认证:m4-l1 的 JWT。书签 CRUD:m3-l1 的 SQL。统一错误:m4-l2 的异常中间件。头像上传:m4-l3 的 multipart。缓存:本节课新加的热门加速。
JWT 无状态 vs Session 有状态
登录态有两种存法,各有代价。Session:服务器把登录态存在内存或 Redis 里,给浏览器一个 sessionId Cookie,每次请求凭 id 去查。优点是登出能立刻失效、能把人踢下线;缺点是多台服务器要共享 session,天生有状态,扩容麻烦。
JWT:登录时服务器签发一个带签名的 token 返回给前端,之后请求都带上,服务器只验签名不查库,天生无状态、多机共享零成本。代价是 token 一旦签发,在过期前没法主动作废(除非额外加一个黑名单),所以 JWT 过期时间一般设短(比如 2 小时),靠 refresh token 续期。书签项目用 JWT,是因为它简单、不需要额外搭 Redis;如果以后要做「异地登录踢人」「管理员强制下线」,再切 Session + Redis。
前端把 token 存哪也有讲究:存 localStorage 简单,但一旦有 XSS,脚本就能把 token 偷走;存 httpOnly Cookie 则前端 JS 读不到、能挡 XSS 窃取,代价是要额外防 CSRF。书签项目为演示简单存在内存/内存态,生产高敏场景应改用 httpOnly Cookie。缓存 key 也要带维度:全站热门用 hot:global,某用户的书签列表用 user:123:bookmarks,别所有用户共用一个 key 串了别人的数据。
关键链路模拟:登录 → 带 token → 缓存
真实项目跑在数据库和文件系统上,这里用纯 Python 把最关键的三条链路打印出来:登录签发 token、带 token 才能访问、热门书签第二次直接命中缓存。注意下面为了演示用了 sha256,真实密码绝不能这么存,下一节讲为什么。
密码为什么不能明文存
上面演示代码用了 sha256 存密码,只是为了把流程跑通。真实系统绝不能明文存密码,也不能只用 sha256 这类普通哈希——它算得太快,彩虹表能把常见密码的哈希反查出来,数据库一泄露,用户密码等于裸奔。
正确做法是用慢哈希算法 bcrypt 或 argon2:它们故意算得慢(单次几十毫秒),还自动加盐,就算数据库泄露,攻击者批量撞库的成本也极高。验密码时把用户输入再过一遍同一算法比对就行。口诀:存密码用 bcrypt/argon2,永远不要自己发明「加密」逻辑,也不要给密码加盐之后还自己迭代折腾。
绝不:明文入库、打日志、在 URL 里传、用 md5/sha256 直接存。要:bcrypt 或 argon2 慢哈希 + 自动加盐。为什么很多人复用密码:数据库一旦泄露明文密码,等于把用户在别的网站的账号也送了人。
CSRF 防护与缓存击穿
CSRF(跨站请求伪造):如果登录态存在 Cookie 里,恶意网站能借用户的浏览器自动带上 Cookie 向你的站发请求。最省事的防护是给登录 Cookie 设 SameSite=Lax 或 Strict,浏览器就不会在跨站请求里自动带上它,顺手再校验一下请求头。
缓存击穿是热门数据最真实的坑:热门书签缓存过期的那一瞬间,成千上万请求同时发现缓存没了,齐刷刷打到数据库,数据库被瞬间打爆——这叫「击穿」。解法有两种:热点 key 不过期、由后台异步刷新;或者加一把互斥锁,同一时刻只放一个请求去重建缓存,其余等它建好直接读。书签热门榜就是典型的读多写极少热点,上线时必须想好这一招。与之相关还有「缓存穿透」(查一个根本不存在的 id,每次都打数据库)和「缓存雪崩」(大量 key 同时过期),三者名字像但解法不同,后面阶段会系统展开。
缓存为什么加速?
热门书签所有人都在读、几乎不写。每次都查数据库太浪费,第一次查完放进内存缓存,后面直接命中。读多写少的数据最适合缓存;要写准的数据(比如用户余额)就不能瞎缓存,否则用户刚充值却读到旧余额。缓存还有个基本原则:缓存的是「查询结果」而不是连数据库的连接,过期时间别设太长,否则改了数据用户却看旧的。
能讲清:登录后 token 存在哪、带 token 请求怎么验、为什么用 JWT 不用 Session、密码为什么用 bcrypt、缓存命中和击穿各发生什么、头像上传走 multipart。能讲清楚这几条链路,s7 后端部分就算出师。
登录成功后,前端在后续请求里应该把 JWT 放在哪里?
关于用户密码存储,下面哪种做法是对的?
资深工程师加餐
底层原理 · 大厂视角 · 工程经验,点卡片展开
网络侧:DNS→TCP/TLS→HTTP 请求与响应;浏览器侧:解析 HTML 建 DOM、解析 CSS 建 CSSOM、合并成渲染树,再经过布局(Layout)、绘制(Paint)、合成(Composite)上屏。前端性能优化几乎都围绕这条链路:减小资源体积、优化关键渲染路径、避免频繁触发重排重绘。
挑战任务
登录态校验中间件
场景:你的书签接口没做登录校验,游客也能翻别人的收藏。写 require_auth(headers):headers 里 Authorization 以 'Bearer ' 开头返回 ok,否则返回 unauthorized——这就是登录中间件的骨架。
课后作业
画出书签项目链路图
场景:这是你的毕业项目。画一张从登录到看热门书签的流程图,标出 token 在哪产生、在哪校验、缓存何时命中;并补一句:为什么密码用 bcrypt 而不是 sha256。