CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
后端入门实战/课时/认证
RawBlame
课时目录12 节

后端入门实战

  1. 01后端入门概览
  2. 02Git 与托管
  3. 03语言选型
  4. 04PostgreSQL
  5. 05数据库进阶
  6. 06Redis 与缓存
  7. 07REST API
  8. 08认证
  9. 09ORM
  10. 10单元测试
  11. 11集成与功能测试
  12. 12综合练习

认证

JWT 认证流程。

30 分钟

认证

这一课你将能做到

  1. 讲清楚 Session 与 JWT 两种方案的完整流程、存储位置和各自适用场景
  2. 用 bcrypt + JWT 实现注册/登录/鉴权中间件的完整代码并跑通
  3. 说出密码为什么必须用 bcrypt 而不是 MD5,以及 OAuth2 授权码流程在各家「第三方登录」里是怎么发生的

为什么要学

HTTP 是无状态的(第 1 课说过):服务器处理完上一个请求就忘了你是谁。但「改我的笔记、看我的订单」必须知道你是谁、是不是你本人——认证(Authentication)解决「你是谁」,授权(Authorization)解决「你能干什么」,两者合起来是所有业务接口的地基,也是面试的高频题。

人话:认证就像进写字楼的两种方式。Session 是前台发你一张临时工牌,每次进门前台查登记本确认(状态存服务器);JWT 是发你一枚加密徽章,徽章本身写着你的部门和有效期,保安用机器验个章就放行(状态存客户端),不用查本,人再多也快。

先建立图像

两种方案的完整流程对比:

【Session 方案】                          【JWT 方案】
登录: 账号密码 ──▶ 服务器存 Session       登录: 账号密码 ──▶ 服务器签发 JWT
          ◀── SessionID(放进 Cookie)              ◀── token(客户端自己存)
后续: 每个请求自动带 Cookie               后续: 每个请求手动带
      Header: Cookie: sid=xxx                 Header: Authorization: Bearer xxx
服务器 ──▶ 拿 sid 查 Session 存储          服务器 ──▶ 用密钥本地验签,不查库
(多台服务器要共享 Session 存储)          (无状态,天然支持水平扩容)

JWT 长这样(三段用点隔开,前两段只是 Base64URL 编码,不是加密):

eyJhbGciOiJIUzI1NiJ9 . eyJ1c2VybmFtZSI6ImFsaWNlIiwiaWF0IjoxNzM1... . sflKxwRJSMeKKF2QT4f...
        头部(header)                载荷(payload)                    签名(signature)
   声明签名算法 HS256         存用户ID、过期时间等            对前两段的防篡改签名

跟着做

步骤 1:先搞懂密码为什么不能明文存

mkdir bebg-auth && cd bebg-auth && npm init -y && npm install express bcryptjs jsonwebtoken

先做一个小实验,理解哈希与加盐:

// hash-demo.mjs 运行:node hash-demo.mjs(package.json 无 "type":"module" 时请命名为 hash-demo.cjs 并改用 require)
const bcrypt = require('bcryptjs');

(async () => {
  const password = 'my-secret-123';
  const hash1 = await bcrypt.hash(password, 10);   // 10 是 cost 因子,2^10 轮加盐哈希
  const hash2 = await bcrypt.hash(password, 10);
  console.log('同一密码两次哈希:', hash1 !== hash2); // true —— 盐是随机的
  console.log('哈希样例:', hash1);                  // $2a$10$N9qo8uLOickgx2ZMRZoMye...
  console.log('校验正确密码:', await bcrypt.compare(password, hash1)); // true
  console.log('校验错误密码:', await bcrypt.compare('wrong', hash1));  // false
})();

三个关键观察:同一密码每次哈希结果不同(自动随机加盐,彩虹表失效);校验不是重新哈希对比,而是 bcrypt.compare;cost 因子让暴力破解成本可控地变贵。MD5/SHA256 没有内置盐且计算太快,专攻密码存储的 bcrypt/scrypt/argon2 才是正解。

步骤 2:注册 + 登录接口

新建 server.js:

// server.js
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const app = express();
app.use(express.json());

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-me';
const users = [];   // 演示用内存存储,真实项目存数据库

// 注册
app.post('/api/auth/register', async (req, res) => {
  const { username, password } = req.body || {};
  if (!username || !password || password.length < 8) {
    return res.status(400).json({ error: '用户名必填,密码至少 8 位' });
  }
  if (users.some(u => u.username === username)) {
    return res.status(409).json({ error: '用户名已存在' });
  }
  const passwordHash = await bcrypt.hash(password, 10);   // 只存哈希,永不存明文
  users.push({ username, passwordHash });
  res.status(201).json({ message: '注册成功' });
});

// 登录:校验密码,签发 JWT
app.post('/api/auth/login', async (req, res) => {
  const { username, password } = req.body || {};
  const user = users.find(u => u.username === username);
  // 用户不存在和密码错误返回同一句话,不给撞库者提示
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: '用户名或密码错误' });
  }
  const token = jwt.sign(
    { sub: username, role: 'user' },        // payload:只放非敏感信息
    JWT_SECRET,
    { expiresIn: '2h', issuer: 'bebg-demo' }
  );
  res.json({ token });
});

app.listen(3000, () => console.log('http://localhost:3000'));

步骤 3:鉴权中间件保护接口

在 server.js 追加:

// 鉴权中间件:从 Authorization 头取 Bearer token 验签
function auth(req, res, next) {
  const header = req.headers.authorization || '';
  if (!header.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未登录' });
  }
  try {
    const payload = jwt.verify(header.slice(7), JWT_SECRET);  // 验签 + 查过期
    req.user = payload;        // 挂到 req 上,后续处理器直接用
    next();
  } catch {
    res.status(401).json({ error: 'token 无效或已过期' });
  }
}

// 受保护接口:谁登录的,就只能看谁的资料
app.get('/api/me', auth, (req, res) => {
  res.json({ username: req.user.username, role: req.user.role });
});

步骤 4:curl 走通全流程

# 注册
curl -i -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password123"}'
# 201

# 登录拿 token
curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password123"}'
# {"token":"eyJhbGciOi..."}

# 不带 token 访问受保护接口
curl -i http://localhost:3000/api/me
# 401 {"error":"未登录"}

# 带 token 访问(把 <token> 换成上一步拿到的值)
curl http://localhost:3000/api/me -H "Authorization: Bearer <token>"
# {"username":"alice","role":"user"}

# 把 payload 解开看看:token 第二段是 Base64URL,直接可解码
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64url').toString()))" <token>

最后一条命令的输出会让你彻底记住:JWT 的 payload 任何人都能读,只是不能改(一改签名就对不上)。

步骤 5:Session 方案对照(express-session)

// session-demo.js —— npm install express-session
const express = require('express');
const session = require('express-session');

const app = express();
app.use(express.json());
app.use(session({
  secret: 'dev-secret-change-me',       // 用来给 SessionID 签名
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, maxAge: 2 * 60 * 60 * 1000 },  // httpOnly: JS 读不到,防 XSS 偷
}));

app.post('/api/login', (req, res) => {
  const { username } = req.body || {};
  if (!username) return res.status(400).json({ error: '用户名必填' });
  req.session.username = username;      // 登录态存在服务端(默认内存存储,生产要换 Redis 存储)
  res.json({ message: 'ok' });
});

app.get('/api/me', (req, res) => {
  if (!req.session.username) return res.status(401).json({ error: '未登录' });
  res.json({ username: req.session.username });
});

app.listen(3001, () => console.log('http://localhost:3001'));

Session 的登录态由浏览器 Cookie 自动携带(curl 用 -c cookies.txt -b cookies.txt 保存和回放),前端零代码;JWT 则要求前端每次手动加 Header。

步骤 6:OAuth2 是怎么一回事

你在很多网站见过「用 GitHub 登录」,背后是 OAuth2 授权码流程:

你的网站                          GitHub 授权服务器              GitHub API
   │ ①跳转 GitHub 授权页(带 client_id+回调地址)    │                 │
   │──────────────────────────────▶│                 │
   │                 ②用户登录并同意授权              │                 │
   │◀───────────③回调你的地址,带一次性 code─────────│                 │
   │ ④用 code + client_secret 换 access_token       │                 │
   │──────────────────────────────▶│                 │
   │ ⑤拿 access_token 调 API 读用户资料             │                 │
   │─────────────────────────────────────────────────────────────────▶│

要点:你的网站全程拿不到用户的 GitHub 密码;client_secret 只能存在服务器端;拿到 access_token 后调 GitHub 的 API 获取用户信息并为自己系统建立会话。严格说 OAuth2 是授权协议,登录场景配合 OIDC(OpenID Connect)才是完整的认证方案——面试能说到这一层就超过大多数人。

讲清楚要点

  1. Session vs JWT 选型:Session 强在「可随时踢人下线」(删服务端记录即可)、前端零成本,代价是服务端要存状态、多实例需共享存储(Redis);JWT 强在无状态、天然跨服务/跨域(App、小程序都好使),代价是签发后无法主动撤销。常见组合拳:短有效期 access token(15 分钟)+ 可撤销的 refresh token(存 Redis,可踢)。
  2. 密码存储三原则:只用 bcrypt/scrypt/argon2;只存哈希不存原文(连日志都不能打);校验失败信息模糊化(用户名或密码错误)。
  3. JWT 的信任来自签名不是加密:payload 透明可读,绝不能放手机号、密码等敏感信息;密钥 JWT_SECRET 从环境变量读,绝不提交进 Git(回想第 2 课的 .gitignore)。
  4. Token 的传输与存储:API 场景标准是 Authorization: Bearer <token>。Web 前端存哪里有取舍——localStorage 方便但 XSS 可偷;httpOnly Cookie 浏览器自动带、JS 读不到但要配 CSRF 防护。企业内两者都有,关键是理解各自的攻击面。
  5. 认证和授权分开实现:中间件只做「验明正身」(401),角色/权限判断是另一层(403)——auth 之后可以再叠 requireRole('admin'),职责不要混在一起。

你可能会懵的地方

  • JWT 签发后想立刻让它失效,怎么办? 签名有效期内服务端默认认账,这是无状态的代价。三个办法:① 缩短有效期 + refresh token(主流);② 服务端维护黑名单(Redis 存被踢 token 的 jti),等于把一部分状态请回来;③ 关键操作(改密码)要求重新验证。改密码后让所有旧 token 失效 = 记录一个「最早有效时间」,payload 里的 iat 早于它就拒绝。

  • 把 secret 硬编码在代码里会怎样? 仓库一旦泄露(公开仓库、员工离职拷代码),任何人可签发任意身份的合法 token,等于整个认证体系裸奔。正确做法:环境变量/密钥管理服务注入,不同环境用不同密钥。

  • 为什么 bcrypt.compare 不需要单独传盐? 盐就拼在哈希结果的字符串里($2a$10$ 后面那串包含算法、cost 和盐),compare 时自动拆出来用。这是设计好的自描述格式,你不用管盐。

  • OAuth2 的四种模式都要记吗? 重点记授权码模式(上面流程图,最安全最常用,PKCE 是它的移动端/前端增强版);隐式模式已废弃;密码模式和客户端凭证知道适用场景(前者仅高度信任的第一方客户端,后者用于服务对服务)。

  • 网关统一鉴权和各服务自己鉴权什么关系? 微服务架构常见做法:网关只做一次 JWT 验签,验完把用户信息通过请求头透传给内部服务;内部服务不再重复验签但必须只信任来自网关的流量。单体应用就是本课的中间件模式。

错法对照

错法问题正解
密码明文或 MD5 存库拖库即全军覆没,MD5 可被彩虹表秒破bcrypt/scrypt/argon2
JWT payload 放手机号/角色敏感集payload 透明可读只放 sub、过期时间等非敏感声明
token 永不过期泄露后永久有效短有效期 + refresh token
登录失败提示「密码错误」给撞库者「用户名存在」的确认统一提示「用户名或密码错误」
JWT_SECRET 提交进仓库任何人可伪造 token环境变量注入,.gitignore 排除 .env
自己手写加密/签名算法密码学自造轮子必翻车用成熟库的成熟 API

卡点急救

  1. jwt.verify 报 JsonWebTokenError: invalid signature:签发和校验用了不同 secret(两个终端环境变量不一致是最常见原因),统一后重启。
  2. 报 TokenExpiredError:token 过期了,属预期行为。前端应捕获 401 后用 refresh token 换新,练习阶段重新登录即可。
  3. curl 带了 token 还是 401:检查头格式必须精确是 Authorization: Bearer <token>(Bearer 后有一个空格);token 复制时别带引号和换行。
  4. bcrypt.hash 报 data and hash arguments required:入参 undefined,多半是忘了 express.json() 或请求没带 body。
  5. session-demo 刷新后登录态丢失:默认存储在内存且 cookie 没回传。curl 要 -c cookies.txt(存)与 -b cookies.txt(带)配对使用;浏览器测试检查是否拦截了 Cookie。

面试常问

  • Session 和 JWT 的优缺点与选型? Session:服务端有状态、易撤销、浏览器 Cookie 自动携带;缺点是多实例需共享存储、跨域/原生 App 不便。JWT:无状态易水平扩展、跨端通用;缺点是难撤销、payload 可读。选型:传统 Web 单体常用 Session;分布式/多端 API 常用 JWT(短效+refresh);能讲出「JWT 撤销问题的组合拳」是亮点。

  • JWT 由哪几部分组成?如何防篡改? Header(算法)、Payload(声明)、Signature(对前两段用 secret + 指定算法签名),三段 Base64URL 以点相连。服务端重算签名比对,改一个字符即失配——所以防篡改靠签名,内容本身并不保密。

  • 如何安全地存储用户密码? 专用慢哈希(bcrypt/argon2,内置随机盐、可调 cost),注册时哈希入库,登录时 compare;不使用 MD5/SHA 系;配合密码强度校验与登录限流。

  • OAuth2 授权码模式的流程与安全性? 跳授权页(client_id + redirect_uri + state 防 CSRF)→ 用户授权 → 带 code 回调 → 服务端用 code + client_secret 换 access_token → 调 API。密码不经过第三方网站;code 一次性且短时效;secret 只在服务端。

  • CSRF 是什么,和认证方案有什么关系? 跨站请求伪造:浏览器自动带 Cookie 的特性被恶意站点利用,冒充用户发请求。Session+Cookie 方案必须防(SameSite/CSRF Token);Bearer Token 放 Header 不自动携带,天然免疫 CSRF 但要防 XSS 偷 token。

自测三问

  1. 把 server.js 的用户存储从内存数组换成第 4 课的 PostgreSQL(用 pg 驱动即可),注册和登录需要改哪几处?
  2. 设计「管理员才能删除笔记」的鉴权链路:payload 放什么、中间件怎么叠加、401 和 403 分别在什么时机返回?
  3. 产品要求「用户改密码后所有设备下线」,用 JWT 怎么实现?说出至少两种方案及其代价。

小结一句话

认证 = 验明正身(401),授权 = 判定权限(403);密码交给 bcrypt,登录态在 Session(可踢人)和 JWT(无状态)之间按业务取舍,OAuth2 解决的是「借第三方身份而不碰对方密码」。

延伸

  • MDN:Web 安全基础
  • OAuth 2.0 授权框架 RFC 6749
  • 下一课:ORM——不用手拼 SQL 字符串,用对象和关系把数据库操作写优雅。
加载本节练习…
← 上一课REST API下一课 →ORM