2026-09-15 · 阅读 14
认证授权合集
一次讲透认证与授权: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 token | refresh token | |
|---|---|---|
| 状态 | 无状态 | 有状态(服务端存记录,可吊销) |
| 存放 | 内存 / Authorization 头 | httpOnly + SameSite + Secure Cookie(一行三防) |
| 怎么"删" | 删不了,等它过期 | 可以被删——删的是数据库的那条记录,不是 token 本身 |
2.1 追问四连
- 双 token 的设计原因:单 token 两难,拆开干活/续命,伤害量化句
- refresh 也无状态,登出怎么删除? refresh 可以吊销,删除的是数据库的那条记录,不是 token 本身
- token 放 localStorage 为什么不行? XSS 一行偷走,httpOnly 才读取不到
- access 放内存刷新就丢怎么办? 丢了就走一次 refresh
2.2 口述:XSS 攻击者分别偷内存里的 access、和 httpOnly Cookie 里的 refresh——哪个偷不到?哪个只值 15 分钟?
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)
还没有评论,来抢沙发吧~