Article
后端鉴权体系:认证、授权与访问控制
后端鉴权体系:认证、授权与访问控制
后端鉴权先拆成三件事:
| 概念 | 英文 | 解决的问题 |
|---|---|---|
| 认证 | Authentication | 你是谁? |
| 授权 | Authorization | 你能做什么? |
| 访问控制 | Access Control | 是否允许这次操作? |
很多人把它们都叫「鉴权」,但严格来说:
- Authentication = 身份确认
- Authorization = 权限判断
- Access Control = 在具体 API、页面或资源上执行放行或拒绝
认证和授权回答规则问题,访问控制回答执行问题。用户是谁、拥有什么权限,最终都要落到一次具体请求上:这个 API 能不能调,这个页面能不能进,这个文件能不能下载
这篇文章按真实请求链路展开:先看用户如何完成登录,再看系统如何保持登录态;然后讨论机器调用、私有资源访问、访问控制落点和常见安全风险
从登录到访问资源的流程

这张图表达的是一条完整后端鉴权链路:
用户发起请求
↓
后端识别身份:Session / JWT / API Key
↓
后端判断权限:是否能访问接口或资源
↓
访问控制执行:API / 页面 / 资源是否放行
↓
业务代码执行
↓
返回数据或拒绝请求章节与流程对应
这些方案不是并列替代关系,而是落在请求链路的不同阶段
| 章节 | 所在流程 | 主要职责 |
|---|---|---|
| OAuth 2.0 | 登录入口 / 第三方授权委托 | 从 GitHub、Google 等平台确认第三方身份 |
| Session / Cookie | 本站登录态保持 | 让浏览器后续请求自动带上本站会话凭证 |
| JWT | 本站登录态保持 / API 身份凭证 | 让前端、移动端或服务通过 Token 证明身份 |
| API Key | 机器调用身份识别 | 识别哪个应用、服务或开发者在调用接口 |
| Signed URL | 私有资源临时访问 | 把一次被允许的资源访问封装成短期 URL |
| Access Control | API、页面、资源放行判断 | 在具体入口决定这次操作是否允许 |
| Middleware / Guard | 请求链路执行位置 | 统一解析身份,并执行权限与访问控制 |
| CSRF / XSS | 安全风险与防护 | 防止凭证被滥用、窃取或伪造请求 |
后端鉴权不是单个技术点,而是一条请求链路:用户先完成登录,后端再把这次身份确认延续到后续请求里
HTTP 本身是无状态的。用户在登录页输入账号密码,或通过 GitHub / Google 授权,只能证明“这一刻登录成功”。下一次请求进来时,后端仍然需要知道它是谁发来的
登录成功后,后端通常会创建自己的登录态:要么保存 Session,让浏览器带上 session_id;要么签发 JWT,让前端在请求 API 时携带它。OAuth 只负责引入第三方身份,不替你的系统维护本站登录态
OAuth 2.0:第三方登录与授权委托
OAuth 2.0 常用于第三方登录和第三方授权。当身份来源不在你的系统里,而是在 GitHub、Google、Notion 这类平台上,就会进入 OAuth 场景
常见例子包括:使用 GitHub 登录网站、使用 Google 登录后台、允许某个应用访问你的 Notion 数据
OAuth 解决的不是“本站账号密码怎么验证”,而是:让一个应用在用户授权后,有限地访问另一个平台的资源
OAuth 2.0 角色
| 角色 | 说明 |
|---|---|
| Resource Owner | 资源所有者,通常是用户 |
| Client | 第三方应用 |
| Authorization Server | 授权服务器 |
| Resource Server | 资源服务器 |
示例:用 GitHub 登录某网站
- 用户:Resource Owner
- 某网站:Client,也就是你的前端和后端
- GitHub 授权页面:Authorization Server
- GitHub API:Resource Server
你的后端在 OAuth 中做什么
使用 OAuth 并不代表可以省掉自己的后端。GitHub / Google 负责确认第三方账号身份,但你的系统仍然要负责本站用户、权限、订单、文章和私有资源
后端不能直接相信前端传来的用户信息。正确做法是:前端只拿到 GitHub 回跳带回来的 code,再把 code 交给你的后端
你的后端用 code、client_id 和 client_secret 去 GitHub 服务器换取 access token。然后再用 access token 请求 GitHub API,获取可信的用户 ID、邮箱或用户名
拿到第三方身份后,后端再查找或创建本地用户。例如把 GitHub 的 id 绑定到你系统里的 user_id,最后再签发你自己域名下的 Session 或 JWT

OAuth 登录的核心流程可以理解为:
用户点击 GitHub / Google 登录
↓
跳转到第三方授权页面
↓
第三方平台回跳并带回 code
↓
你的后端用 code 换取 access token
↓
你的后端请求第三方用户信息
↓
绑定或创建本站用户
↓
签发本站 Session 或 JWT关键点: GitHub 的 access token 用来访问 GitHub API;你自己系统的 Session 或 JWT 用来访问你自己的后端 API。两者不是同一个登录态
登录完成之后,系统要解决的是“下一次请求怎么认出这个用户”。这就进入本站登录态保持
登录态保持:Session / Cookie 与 JWT
无论用户是账号密码登录,还是 OAuth 登录,进入你的系统之后,都需要一种本站可识别的凭证
常见选择有两类:
| 方案 | 凭证在哪里 | 状态在哪里 | 常见场景 |
|---|---|---|---|
| Session / Cookie | 浏览器 Cookie 中的 session_id | 后端 Session Store | 传统 Web 应用 |
| JWT | Header、Storage 或 Cookie 中的 Token | Token 自身携带声明 | 前后端分离、移动端、API |
这两种方案解决的是同一个问题:用户不用每次请求都重新登录,后端仍然能识别请求身份
Session / Cookie 鉴权
基本原理
Session 鉴权是传统 Web 系统最常见的登录态方案。它的核心是:登录状态保存在你的后端,浏览器只保存一个可以索引这份状态的 ID
这里要先区分两个东西:Session 是后端保存的登录状态,Cookie 是浏览器保存并自动发送的请求凭证
浏览器里通常只存一个 session_id。真正的用户信息、登录时间、过期时间、角色等状态,放在你的后端 Session Store 里,例如 Redis 或数据库

Session / Cookie 的请求流程是:
用户登录成功
↓
后端创建 Session,并保存到 Redis / 数据库
↓
后端通过 Set-Cookie 写入 session_id
↓
浏览器后续请求自动携带 Cookie
↓
后端用 session_id 查 Session
↓
查到后确认当前用户身份Cookie 机制
Cookie 是浏览器自动管理的小段数据。Session / Cookie 登录不是把 Session 整个塞进浏览器,而是让 Cookie 携带一个 session_id,后端再用它查到对应的 Session
后端设置:
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax浏览器携带:
Cookie: session_id=abc123Session 存储
| 存储方式 | 说明 |
|---|---|
| 内存 | 简单,但服务重启会丢失 |
| Redis | 最常见,适合分布式后端 |
| 数据库 | 可持久化,但查询压力较大 |
优缺点
| 维度 | 优点 | 代价 |
|---|---|---|
| 安全性 | 真实用户信息存储在后端,浏览器只拿到 session_id | Cookie 会自动携带,需要额外防 CSRF |
| 状态控制 | 可以主动删除 Session,让用户立即下线 | 后端必须保存 Session 状态 |
| 分布式部署 | 权限变更可以在后端即时生效 | 多台服务器需要共享 Session Store,例如 Redis |
| 使用场景 | 适合普通 Web 应用,浏览器天然支持 Cookie | 移动端和开放 API 不一定适合 Cookie 流程 |
Cookie 安全属性
Set-Cookie: session_id=abc123;
HttpOnly;
Secure;
SameSite=Lax;
Path=/;
Max-Age=7200| 属性 | 作用 |
|---|---|
| HttpOnly | 禁止 JavaScript 读取 Cookie,降低 XSS 窃取风险 |
| Secure | 只允许 HTTPS 传输 |
| SameSite | 限制跨站请求携带 Cookie,缓解 CSRF |
| Max-Age / Expires | 设置过期时间 |
| Path | 限制 Cookie 生效路径 |
| Domain | 限制 Cookie 生效域名 |
推荐配置:
HttpOnly = trueSecure = trueSameSite = Lax或Strict
JWT 鉴权
JWT 全称是 JSON Web Token,常用于前后端分离、移动端和微服务系统
和 Session 相比,JWT 把一部分身份声明放进 Token 里,后端可以通过签名验证它是否可信,而不一定每次都查询 Session Store
JWT 结构
JWT 的格式是 xxxxx.yyyyy.zzzzz
它由三部分组成:Header.Payload.Signature
| 部分 | 作用 |
|---|---|
| Header | 声明算法和 Token 类型 |
| Payload | 存放用户信息和过期时间等声明 |
| Signature | 签名,防止 Token 被篡改 |
Payload 示例:
{
"sub": "user_123",
"role": "admin",
"exp": 1710000000
}注意: JWT 的 Payload 默认只是 Base64URL 编码,不是加密。不能把密码、手机号、身份证号等敏感信息放进去
JWT 鉴权流程

JWT 的请求流程是:
用户登录成功
↓
后端签发 JWT
↓
前端保存 JWT
↓
请求 API 时携带 Authorization Header
↓
后端验证签名和过期时间
↓
验证通过后解析用户身份请求示例:
GET /api/user/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...JWT 验证逻辑
后端收到 JWT 后,通常检查:
- Token 格式是否正确
- 签名是否正确
exp是否过期iss是否可信aud是否匹配当前服务sub对应的用户是否仍然有效
常见字段:
| 字段 | 全称 | 作用 |
|---|---|---|
| sub | Subject | 用户 ID |
| exp | Expiration Time | 过期时间 |
| iat | Issued At | 签发时间 |
| nbf | Not Before | 生效时间 |
| iss | Issuer | 签发方 |
| aud | Audience | 接收方 |
| jti | JWT ID | Token 唯一 ID,可用于黑名单 |
优缺点
| 维度 | 优点 | 代价 |
|---|---|---|
| 状态管理 | 后端不一定需要存储 Token,验证签名即可识别身份 | 主动失效困难,Token 未过期前仍可能有效 |
| 分布式系统 | 多服务只要共享密钥或公钥即可验证 | 权限变更不一定即时生效,旧 Token 可能继续携带旧权限 |
| 使用场景 | 适合移动端、前后端分离和开放 API | Token 较长,会增加请求头体积 |
| 跨域请求 | 通过 Authorization Header 携带,不依赖 Cookie 自动机制 | Payload 可被解码查看,不能存敏感数据 |
| 长期登录 | 可以配合 Refresh Token 延长登录态 | 需要额外处理轮换、吊销和泄露检测 |
Access Token + Refresh Token
成熟系统通常不会只用一个长期 JWT,而是拆成:
| Token | 生命周期 | 用途 |
|---|---|---|
| Access Token | 5 分钟到 30 分钟 | 访问 API |
| Refresh Token | 数天到数周 | 换取新的 Access Token |
流程:

Access Token 和 Refresh Token 的协作流程是:
前端携带 Access Token 请求 API
↓
Access Token 未过期,后端正常返回数据
↓
Access Token 过期,前端使用 Refresh Token 请求刷新接口
↓
后端验证 Refresh Token
↓
签发新的 Access Token
↓
前端用新的 Access Token 继续访问 API更安全的设计:Refresh Token Rotation
每次刷新时:
- 旧 Refresh Token 失效
- 新 Refresh Token 发放
这样如果旧 Token 被盗后再次使用,后端可以检测异常
API Key 鉴权
OAuth、Session 和 JWT 多数时候处理“用户身份”。API Key 更常处理“哪个应用、服务或开发者在调用接口”

API Key 的调用流程是:
服务端应用保存 API Key
↓
调用接口时在 Header 中携带 API Key
↓
API 服务校验 Key 是否存在、是否过期、权限是否匹配
↓
校验通过后识别调用方应用
↓
继续执行接口逻辑API Key 常用于服务端调用服务端,不适合普通用户登录
示例:
GET /v1/models
Authorization: Bearer sk_xxxxx或者:
X-API-Key: abc123API Key 的本质
API Key 不是用户会话,而是调用方身份凭证,通常对应 application_id、client_id 或 service_id
它适合第三方平台接入、内部服务调用、开放平台和脚本访问,不适合直接代表终端用户
API Key 首先属于 Authentication:它识别调用方是谁。生产环境里,它通常还会绑定 scope、配额、环境、来源限制和资源范围,因此也会参与授权与访问控制
| 判断点 | 对应概念 | 示例 |
|---|---|---|
| 这个 Key 是否存在、是否有效 | Authentication | sk_xxx 对应 client_id=app_123 |
| 这个 Key 有哪些 scope | Authorization | models:read、orders:write |
| 这次请求能不能访问这个接口或资源 | Access Control | 只能读当前项目下的数据 |
所以 API Key 可以这样理解:
API Key 识别调用方是谁
↓
Scope / Role 决定它理论上能做什么
↓
Access Control 判断这次具体请求是否放行安全风险
| 风险 | 说明 |
|---|---|
| 容易泄露 | 写在前端代码里会被直接看到 |
| 权限过大 | 一个 Key 可能能访问大量资源 |
| 不适合浏览器前端 | 前端无法安全保存 Secret |
| 轮换成本 | 泄露后需要重新生成和替换 |
重要原则: 不要把真正的 Secret API Key 放进 React / Vue / 小程序前端代码里。因为前端代码最终会暴露给用户
Signed URL
用户登录态解决的是“谁在访问 API”。但图片、视频、附件下载通常不通过 JSON API 返回,而是经过对象存储或 CDN
Signed URL 就是把一次资源访问授权封装进临时 URL。后端先确认当前用户有权限,再生成带签名和过期时间的链接
常用于:
- 私有图片访问
- 私有视频访问
- 私有文件下载
- 对象存储访问
- CDN 防盗链
- R2 / S3 私有资源访问
Signed URL 的本质
Signed URL 是把权限编码进 URL 的临时访问凭证。它不是登录态,也不表示“当前用户是谁”,更像后端在确认权限后,为某个资源发放的短期通行证
示例:
https://media.example.com/file/abc.jpg?exp=1710000000&sig=xxxx| 参数 | 作用 |
|---|---|
| exp | 过期时间 |
| sig | 签名 |
| path | 被保护的资源路径 |
| user_id | 可选,绑定用户 |
| nonce | 可选,防重放 |
| ip | 可选,绑定 IP |
Signed URL 要按阶段拆开看:
| 阶段 | 对应概念 | 说明 |
|---|---|---|
| 生成前 | Authentication | 先确认当前请求是谁发来的 |
| 生成前 | Authorization | 判断这个身份是否有下载、查看或播放权限 |
| 生成时 | Access Control | 只给允许访问的资源生成短期 URL |
| 使用时 | Access Control | CDN、Worker 或存储服务校验签名、路径和过期时间 |
所以 Signed URL 本身更接近 Access Control 的执行结果:它把一次已经被允许的资源访问,封装成短期、可校验的 URL
用户请求查看私有图片
↓
Session / JWT 确认用户身份
↓
Policy 判断用户是否能访问这张图片
↓
后端生成只针对这张图片的 Signed URL
↓
CDN / Worker 校验签名后返回图片这里的 Session / JWT 负责认证,Policy 负责授权判断,Signed URL 负责把访问控制落到私有资源读取上
适用场景
| 场景 | 是否适合 |
|---|---|
| 私有图片临时访问 | ✓ 适合 |
| 私有视频播放 | ✓ 适合 |
| 文件下载 | ✓ 适合 |
| 大文件 CDN 分发 | ✓ 适合 |
| 用户登录状态维护 | ✗ 不适合 |
| API 登录鉴权 | ✗ 不适合 |
它更像是:资源级临时通行证,不是完整的用户登录系统
与防盗链的区别
Referer 防盗链主要依赖请求来源,Referer 可能缺失或被伪造,只适合轻量防盗刷
Signed URL 依赖服务端签名,可以绑定路径、过期时间、用户或 IP,更适合私有资源访问
Access Control:访问控制落在哪里
Authentication 和 Authorization 之后,还需要 Access Control
认证确认当前请求是谁发来的。授权告诉系统这个身份拥有哪些角色、权限或策略。访问控制则是在具体位置做最终判断:这一次操作到底放不放行
这三层可以这样理解:
| 层次 | 关注点 | 典型问题 |
|---|---|---|
| Authentication | 身份 | 你是谁? |
| Authorization | 权限 | 你拥有什么权限? |
| Access Control | 执行 | 这次 API、页面或资源访问是否允许? |
API 层访问控制
API 层访问控制判断的是:当前身份能不能调用这个接口
例如:
DELETE /api/posts/post_123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...这时只知道用户已登录还不够。后端还要判断:
- 这个用户是否有
post:delete权限 - 这个用户是否是文章作者
- 这个接口是否只允许管理员调用
- 这个操作是否需要二次确认或审计日志
所以 API 层常见的判断不是单纯 isLogin,而是:
can(user, "delete", post)页面层访问控制
页面层访问控制判断的是:当前用户能不能进入某个页面或看到某块界面
例如后台系统里:
| 页面 | 访问要求 |
|---|---|
/admin | 必须登录 |
/admin/users | 必须是管理员 |
/billing | 必须属于当前团队 |
/settings/security | 可能需要重新验证密码 |
页面层控制可以放在服务端路由、SSR Loader、前端 Router Guard 或边缘函数里。但敏感页面不能只依赖前端隐藏菜单,因为用户仍然可以直接请求接口
正确做法是:页面层负责用户体验和入口拦截,API 层仍然负责真正的数据保护
资源层访问控制
资源层访问控制判断的是:当前身份能不能访问某一个具体对象
这是最容易被忽略的一层。很多系统只检查“用户已登录”,却没有检查“这个订单、文件、文章或图片是不是属于这个用户”
典型资源包括:
| 资源 | 判断方式 |
|---|---|
| 订单 | order.user_id == current_user.id |
| 团队文档 | 用户是否属于该团队 |
| 私有图片 | 用户是否拥有该图片访问权 |
| 文章草稿 | 用户是否是作者或编辑 |
| 下载附件 | 用户是否有该文件的下载授权 |
常见漏洞就是 IDOR:Insecure Direct Object Reference。用户把 URL 里的 order_id=1001 改成 order_id=1002,如果后端只检查登录态、不检查资源归属,就可能泄露别人的数据
资源层访问控制通常要在数据库查询或业务服务层完成:
先查当前用户可访问的资源
↓
再按资源 ID 查询
↓
查不到就返回 404 或 403例如不要先查任意订单,再判断能不能看:
SELECT * FROM orders WHERE id = :order_id;更安全的写法是把访问范围放进查询条件:
SELECT * FROM orders
WHERE id = :order_id
AND user_id = :current_user_id;如果是团队、组织、项目空间,还要把 team_id、成员关系或角色关系纳入查询
中间件与 Guard:把鉴权落到请求链路
前面的方案最终都要落到请求处理链路里。实际项目中,鉴权通常做成 Middleware / Guard / Interceptor
中间件负责统一解析身份:从 Cookie 里取 session_id,或从 Authorization Header 里取 JWT / API Key。解析成功后,再把用户或调用方信息挂到请求上下文
Guard 或 Interceptor 负责访问控制:当前身份能不能访问这个接口、页面、资源或操作
流程:
Request → Auth Middleware:解析身份
↓
Access Control Guard:检查接口、页面或资源访问
↓
Controller → Service → Database实际代码里可以拆成两类函数:
authenticate(request) → user / client / anonymous
authorize(context, action, resource) → allow / deny前者负责识别身份,后者负责把权限规则落到这次接口、页面或资源访问上
CSRF 与 XSS
鉴权设计还必须考虑两个经典风险:CSRF 和 XSS
前面讨论的是凭证如何产生、携带和验证;这里要看凭证会怎样被滥用。CSRF 和 XSS 分别对应 Cookie 自动携带、前端脚本执行这两类风险
CSRF
全称: Cross-Site Request Forgery(跨站请求伪造)
问题来源: 浏览器会自动携带 Cookie
攻击示意:
用户已登录 bank.com
↓
用户访问恶意网站 evil.com
↓
evil.com 诱导浏览器请求 bank.com/transfer
↓
浏览器自动带上 bank.com 的 Cookie
↓
如果后端没有 CSRF 防护,可能执行操作防护方式:
- SameSite Cookie
- CSRF Token
- Origin / Referer 校验
- 关键操作二次确认
XSS
全称: Cross-Site Scripting(跨站脚本攻击)
问题来源: 攻击者让恶意 JavaScript 在你的网站页面中执行
风险:
- 读取 localStorage 中的 Token
- 发起用户身份下的请求
- 篡改页面内容
- 窃取用户信息
防护方式:
- 输入过滤
- 输出转义
- Content Security Policy
- HttpOnly Cookie
- 避免
dangerouslySetInnerHTML - 依赖库安全更新
如何选择
不同鉴权方案不是互相替代,而是服务于不同位置
| 场景 | 更常见的选择 |
|---|---|
| 第三方登录 | OAuth 2.0 + 本站 Session / JWT |
| 普通 Web 应用 | Session / Cookie |
| 前后端分离或移动端 | JWT |
| 后端服务调用 | API Key |
| 私有图片、视频、附件 | Signed URL |
| API、页面、资源放行判断 | Access Control Guard / Policy |
如果只做普通网站登录,Session / Cookie 往往更直接。如果需要移动端、开放 API 或多服务验证,JWT 会更常见。OAuth 只是登录入口,真正访问你自己后端时,仍然要落到你的 Session 或 JWT
无论选择哪种登录态,最终都不能跳过访问控制。登录态只能说明请求来自谁,权限策略只能说明它理论上能做什么,真正的系统安全来自每一次 API、页面和资源访问时的实际校验
总结
后端认证、授权与访问控制可以按一条链路理解:先确认身份,再保持登录态,然后在每次请求里解析身份、判断权限,并在具体 API、页面或资源层执行放行或拒绝
OAuth 负责引入第三方身份;Session / Cookie 和 JWT 负责维持本站登录态;API Key 负责机器调用;Signed URL 负责私有资源的短期访问;Access Control 负责把权限规则落到一次具体操作上
真正落地时,所有凭证都要经过中间件统一解析,并配合 CSRF、XSS、过期时间、吊销、权限策略和访问控制,才能构成完整的后端鉴权体系
参考文献
- Auth book:Pilcrow 维护的一本免费、无广告的 Web 应用认证指南,侧重认证与登录系统的实践实现,内容覆盖认证方式、Session、认证会话、邮箱、密码、浏览器端存储、CSRF、Passkeys、WebAuthn 等主题,并提供 Go 语言完整示例项目