Portfolio
← 返回博客列表

2026-09-15 · 阅读 14

认证授权合集

Postgresqlredis

一次讲透认证与授权:Session/JWT 完整版、双 Token、OAuth2 授权码、单点登录三件套与 RBAC

本文是《一次讲透 XSS 与 CSRF》《一次讲透 JWT 与 双 Token 机制》《一次讲透 Redis》《一次讲透 React 渲染与性能优化》《一次讲透 NestJS 与 PostgreSQL》《一次讲透数据库与 Redis 实战》的续篇。前篇讲过 JWT 三段结构和双 Token 的基本盘,这一篇是 认证授权的完整版:Session 和 JWT 到底在争什么、JWT 优缺点 4+4、token 存哪、OAuth2 授权码的参数细节、CAS/OAuth2/OIDC 单点登录三件套,最后落到 RBAC 和 NestJS 的 RolesGuard。全文按"能口述出来"的标准整理,每节末尾附我自己的背诵版口述。

Part 1 Session 与 JWT 完整版(含缺点 4+4)

0. 认证 vs 授权:两种存法

认证和授权:Session 和 JWT 是认证结果的两种存法——Session 是把状态存进服务端,JWT 是把状态存进客户端。

HTTP 是无状态的,每次请求都需要重新认证:

  • 第一代 Cookie + Session:登录成功后服务端记一笔 sessionId → 用户,发个小票给浏览器。缺点:存储压力大、多机部署困难、App/小程序不友好
  • 第二代 JWT:登录成功之后服务端发一个 token 给浏览器,浏览器每次请求都带这个 token,服务端验证是否有效

JWT 和 Session 的差异(CSRF 角度):

Session 的 Cookie 是浏览器自动带(所以招 CSRF);JWT 放 Header 是前端代码主动带,img/form 伪造不了自定义头,CSRF 天然免疫。

1. JWT 四条优点、四条缺点

四条优点一句话
无状态可扩展多台服务器不用共享 Session 存储
自包含payload 中包含用户信息,不用查表
跨语言跨端App、小程序、前后端分离部署都好用
服务端重启不丢登录态服务器根本没存这个 token
四条缺点一句话
无法主动失效签发出去之后就收不回来了,登出/封号只能干等过期
续签/改信息比较难用户角色升级之后被封了,旧的 token 还有时效
体积比较大三段字符串比 session_id 大
不防看不防偷token 中包含用户信息,所以不能放敏感信息
const jwt = require('jsonwebtoken');

// 1. 签发 token
const token = jwt.sign({ userId: 1, role: 'admin' }, SECRET, { expiresIn: '15m' });

// 2. 解开 payload 谁都能看(base64 是编码不是加密)
console.log(Buffer.from(token.split('.')[1], 'base64').toString());

// 3. 校验签名,不查库、不查 Redis——这就是"无状态"
const data = jwt.verify(token, SECRET);

1.1 面试三连

  • JWT 优缺点各 4 条——就是上面两张表
  • "JWT 怎么实现登出?" → 三招:黑名单(Redis 记已注销 token,牺牲无状态)/ 短 access + refresh(我项目的答案)/ 换密钥(伤全体用户,慎用)
  • "Session 和 JWT 怎么选?" → 要"可控"(踢人即时生效、权限频繁变)选 Session;要分布式扩展、多端选 JWT;实践里常是 JWT + Redis 黑名单的混合体

1.2 口述:JWT 最大的缺点是什么?项目怎么补的?补完剩多少伤害?

JWT 有四个缺点:无法主动失效、续签/修改信息比较难、体积比较大、不防看不防偷。我的项目就是针对"无法主动失效"这个缺点采取了双 token 的策略,签发了一个 access token 和一个 refresh token。在登出时删掉 refresh token,旧 access token 最多活十五分钟,也就把伤害从七天降低到十五分钟。

2. 双 Token 体系 + token 的存储位置

双 token 就是拿 access token 来干活,refresh token 来刷新这个 access token 续命;存储策略就是时间越长,暴露面就越小。

access tokenrefresh token
状态无状态有状态(服务端存记录,可吊销)
存放内存 / Authorization 头httpOnly + SameSite + Secure Cookie(一行三防)
怎么"删"删不了,等它过期可以被删——删的是数据库的那条记录,不是 token 本身

2.1 追问四连

  • 双 token 的设计原因:单 token 两难,拆开干活/续命,伤害量化句
  • refresh 也无状态,登出怎么删除? refresh 可以吊销,删除的是数据库的那条记录,不是 token 本身
  • token 放 localStorage 为什么不行? XSS 一行偷走,httpOnly 才读取不到
  • access 放内存刷新就丢怎么办? 丢了就走一次 refresh

refresh 放在 httpOnly Cookie 里面,JS 读取不到,XSS 偷不走——它防的就是偷,软肋是 CSRF,由 SameSite 挡。access 放在内存里,有效期只有十五分钟,到期自灭;想续命必须拿 refresh token 来换,但是 refresh 偷不到,所以攻击者只能拿到 access token——一张十五分钟后就作废的门禁卡。

3. OAuth2 授权码流程

让用户通过 GitHub 账号登录到我的网站上面,而且我从头到尾都拿不到密码,只是通过授权码来换取 access_token,然后回调到 GitHub 获取我的用户信息,数据库存我的用户信息,签发我自己系统的双 token。

3.1 追问四连

Q1:用户用 GitHub 登录我的博客,他的 GitHub 密码会经过我的服务器吗?在哪输入的?

GitHub 密码不会经过我的服务器,用户的 GitHub 密码是在 GitHub 域名的页面上输入的。

Q2:GitHub 为什么不直接在回调 URL 里给 access_token,非要给个 code 让后端去换?(深挖必考)

因为回调重定向走浏览器,通道不安全,URL 里只能放 code,必须再搭配上我后端的 client_secret 才能换取真正的 token。

Q3:攻击者截获了 code,他能拿到 access_token 吗?为什么?

攻击者截获了 code 他也拿不到 access_token,因为 token 必须搭配 client_secret 才能换取。

Q4:GitHub 发的 access_token 和我自己签的 JWT,是同一个东西吗?OAuth2 和 JWT 什么关系?

OAuth2 是授权协议,我用的是其中授权码流程;JWT 是令牌格式,是电子通行证,所以二者不是同一个东西。GitHub 签发的 access_token 归 GitHub 管,我签发的 JWT 归我系统管理,两套不会互相冲突。

3.2 OAuth2 的授权码流程传哪些参数

发起授权之后 URL 带五个参数:response_type=code 代表走授权码模式;client_id 标识是哪个应用在请求;redirect_uri 是 code 送回哪个门;scope 要最小权限;state 是随机暗号,防登录 CSRF。

为什么不直接回调给 access_token?

回调走浏览器不安全,code 必须配 secret 才能换,并且一次性短时效。SPA 没有 client_secret,使用 PKCE 替代方案。

3.3 PKCE(移动端/SPA 没有 secret 怎么办)

client_secret 存不进 SPA 和 App(前端环境是公开的),使用替代方案 PKCE:发起时随机生成 code_verifier,把它的哈希 code_challenge 发出去;换 token 时带上原文 code_verifier,授权服务器核对哈希对得上才发 token。效果:就算 code 在回调的时候被截获,没有 verifier 也换不到 token。

4. 单点登录三件套:CAS、OAuth2、OIDC

SSO 是需求,CAS 和 OIDC 是满足它的两个认证协议,OAuth2 是授权框架。

单点登录:同组织内部子系统用 CAS——ticket 机制,认证中心记全局会话,跳第二个系统的时候不需要密码直接发送票据;互联网第三方场景用 OIDC——OAuth2 授权码流程加发一个 JWT 格式的 ID Token,把"用户是谁"标准化。我项目里 GitHub 登录就是 OAuth2 被借来做登录用的典型。

  • OIDC 和 OAuth2 的关系:OAuth2 管授权,OIDC 在它之上加了 ID Token 管认证
  • CAS 和 OAuth2 的 code 有什么区别:骨架一样,但是信任模型不同(自家子系统和第三方),所以 CAS 无 secret/scope,验证方式是后端直接问认证中心

4.1 口述:你们项目用 GitHub 登录,严格来说算 OAuth2 还是 OIDC?升级成 OIDC 要多发什么?

我们这个项目是 OAuth2 被借来做登录,GitHub 只发 access_token 不发 ID Token。如果要升级成 OIDC 需要加发一个 ID Token,也就是 sub(用户的唯一标识)、iss(谁签发的 = GitHub)、aud(签发给哪个应用 = client_id)、iat/exp(签发/过期时间)。

5. RBAC:权限不挂人,挂角色

RBAC = 权限不直接挂人,挂角色:用户领取角色,角色带权限。老师/学生判断发生在后端,依据也是 token 里面的角色;前端只起提示的作用,不是安全边界。

核心两行:

  • 前端存角色 → 菜单显隐、优化体验
  • 后端校验角色 → 每个接口 Guard 拦住检查 → 真正的安全边界

角色的位置:

小系统的角色进 token,配合十五分钟短 access,降级最多延迟十五分钟生效;要即时生效就每次查库或查 Redis。

老师和学生同一个链接怎么分流:

同一个 URL 下,路由不区分角色,后端 Guard 解出 token 里的 role 放行或拒绝,业务层按照 role 返回不同的数据。

  • 角色存哪:五表结构 + 前端只做显隐 + 后端 Guard 是安全边界(token 带 role 或者查库)
  • 为什么不能相信前端传来的 role:前端代码可以被绕过,并且代码和参数都在攻击者的手里,接口必须亲自验证身份

5.1 NestJS RolesGuard:贴标签 + 保安

// 1. 接口上声明角色
@Post('classroom/join')
@Roles('teacher', 'student') // 自定义装饰器,用于校验角色
join(@Req() req) { ... }

// 2. 守卫统一校验:角色来自 token
@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride('roles', [
      ctx.getHandler(), // 方法上的便签(优先)
      ctx.getClass(),   // 类上的便签(兜底)
    ]);
    const { user } = ctx.switchToHttp().getRequest(); // JWT 策略验签之后挂上来的 payload
    return required.includes(user.role);             // user.role 来自 token 的角色
  }
}

一句话:装饰器是贴标签(启动时把"此门要什么角色"挂到方法上,不校验任何东西),Guard 是保安(每个请求先过它,返回 false 直接 403),reflector 是翻登记簿(方法级优先、类级兜底)。角色来自验过签的 token,不是前端传的。

5.2 口述:老师和学生点同一个链接,校验发生在哪几处?降级多久生效?

校验发生在三处:前端一处、后端两处。前端按照角色做菜单显隐、按钮置灰,用来优化用户体验;后端第一处 JWTAuthGuard 验签,把 payload(含 role)挂上 req.user,第二处 RolesGuard 拿 token 里的角色对照接口标签,不过 403——这两处是安全边界。老师学生同一个 URL,路由不区分,后端按照 role 放行并且返回不同的数据:同链接不同命,后端判断命。

角色写进 token 的话,老师被降级成学生最多十五分钟生效:因为 token 无服务端状态,库里面改了角色,旧的 token 里的 role 和签名都还没有变化,也可以过 Guard;要等到 access 过期、refresh 重签时从库里重读角色,新的 token 才是学生。这就是 JWT 改信息难这个缺点的体现,短 access 把伤害从七天压到十五分钟。

6. 项目上线的安全措施

上线之前我有一套自查清单:

  • 全站 HTTPS 强制跳转,所有密钥环境变量不进仓库
  • Cookie 加 httpOnly / SameSite / Secure 三防
  • CORS 白名单指定允许的域名,不用星号
  • NestJS 带上 CSP 等安全响应头
  • 数据库和 Redis 只在容器内网暴露,不暴露公网
  • 登录接口加上了限流

7. 总结记忆卡

  • 认证 vs 授权:你是谁 / 你能干什么;Session 把状态存服务端,JWT 把状态存客户端
  • Session 招 CSRF(Cookie 自动带),JWT 走 Header 免疫(img/form 伪造不了自定义头)
  • JWT 四优:无状态可扩展、自包含、跨语言跨端、重启不丢;四缺:无法主动失效、续签/改信息难、体积大、不防看不防偷
  • 登出三招:黑名单(牺牲无状态)/ 短 access + refresh(项目答案)/ 换密钥(伤全体);选型:要可控选 Session,要扩展多端选 JWT
  • 双 token:access 干活(无状态、内存、15 分钟),refresh 续命(有状态、httpOnly 一行三防、7 天);删的是数据库记录,不是 token 字符串
  • 伤害量化句:登出删 refresh,旧 access 最多活十五分钟,伤害从七天压到十五分钟
  • XSS 场景:refresh 放 httpOnly 偷不到(软肋 CSRF 由 SameSite 挡),access 偷到了也只值十五分钟——时间越长暴露面越小
  • OAuth2:我从头到尾拿不到密码;code 是一次性回执,必须配后端的 client_secret 才能换 token;五参数:response_type / client_id / redirect_uri / scope / state(防登录 CSRF);SPA 没 secret 用 PKCE(哈希去、原文回)
  • SSO 三件套:SSO 是需求;CAS = ticket + 全局会话,自家子系统;OIDC = OAuth2 + ID Token(sub / iss / aud / iat / exp),互联网第三方
  • CAS vs OAuth2 的 code:骨架一样,信任模型不同,CAS 无 secret/scope、后端直接问认证中心
  • RBAC:权限挂角色不挂人;前端显隐只是体验,后端 Guard 才是安全边界;同链接不同命,命在后端判;降级最多延迟十五分钟(refresh 重签时从库里重读角色)
  • RolesGuard:装饰器贴标签 + Guard 保安 + reflector 登记簿(方法级优先类级兜底);角色来自验签的 token,永远不信前端

本文基于个人面试复盘与背诵笔记整理(D3 认证授权体系),如有疏漏欢迎指出。系列前篇:《一次讲透 XSS 与 CSRF》《一次讲透 JWT 与 双 Token 机制》《一次讲透 Redis:为什么快、缓存三兄弟、从单机到集群》《一次讲透 React 渲染与性能优化》《一次讲透 NestJS 与 PostgreSQL:IoC、DTO 两道防线、JOIN 家族、索引与事务》《一次讲透数据库与 Redis 实战:B+ 树索引、EXPLAIN、深度分页、分布式锁与秒杀五层》。

评论 (0)

0 / 1000

还没有评论,来抢沙发吧~