首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >面试被问懵了?后端说你不懂 Auth?8 种 API 鉴权方案原理+代码一次讲透

面试被问懵了?后端说你不懂 Auth?8 种 API 鉴权方案原理+代码一次讲透

作者头像
前端达人
发布2026-06-12 13:57:21
发布2026-06-12 13:57:21
2470
举报
文章被收录于专栏:前端达人前端达人

你有没有遇到过这种场景:后端同事说"你们前端根本不懂 OAuth",面试官问"JWT 和 Session 有什么区别"你支支吾吾,或者接入第三方登录时一头雾水? 今天咱们一次性把这些全搞清楚——不背概念,从生活场景讲起,再到代码落地。

🔐 先说一个你天天在做却没意识到的事

你每天上班进公司,保安会检查你的工牌。

这个动作背后藏着两个问题:

  • 你是谁?(认证 Authentication)
  • 你能进哪些区域?(授权 Authorization)

API 鉴权干的是同一件事——每次请求到达服务器,服务器都在问:

"你是谁?我为什么要信任你?"

不同的鉴权方案,本质上是对这两个问题给出不同的回答方式。

接下来咱们按照历史顺序,把 8 种方案的来龙去脉讲清楚。每种方案都是为了解决上一种的问题而生的——理解这条进化链,比背八个定义要有用得多。

1. Basic Auth:最原始的方式,"每次都出示身份证"

用生活场景理解

想象你每次进公司,都要把身份证原件交给保安,保安复印存档,再把身份证还给你。

下次再进来?再交一次。每次都这样。

Basic Auth 就是这个逻辑——每次请求都把用户名+密码带上,服务器验证一次才放行。

实际是怎么传的?

浏览器会把"用户名:密码"用 Base64 编码一下(注意:编码≠加密,只是换了个写法,任何人都能解开),然后放进请求头:

代码语言:javascript
复制
Authorization: Basic dXNlcjpwYXNzd29yZA==

// 把上面那串解码,就是:user:password
// 所以必须用 HTTPS,不然在网络上裸奔

服务器收到后,解码,去数据库查用户名密码对不对,对了就放行。

问题出在哪?

每次请求都要查一次数据库。

假设你的 App 有 10 万用户同时在线,每秒产生 100 万次请求,就意味着 100 万次数据库查询。这会把数据库直接压垮。

这个问题推动了 Session 的诞生。

现在还有用吗?

内部管理后台、公司内网工具、快速测试——这些场景用还行。面向普通用户的产品,千万别用。

2. Session:聪明了一步,"登一次,记住你"

用生活场景理解

你去一个高档会所,第一次进来时前台验证了你的身份,然后给你一张会员卡(上面写着你的会员编号)。

以后每次来,你只需要刷会员卡,前台查一下"这个编号对应的是谁"就行了——不需要你每次都掏身份证了

Session 就是这个逻辑。

完整流程

代码语言:javascript
复制
第一步:你登录,发送用户名+密码(只需要这一次)
          │
          ▼
第二步:服务器验证通过,在数据库创建一条 Session 记录
        Session 记录长这样:
        { sessionId: "abc123", userId: 42, 过期时间: 24小时后 }
          │
          ▼
第三步:服务器把 sessionId 写进 Cookie,发给你的浏览器
        Set-Cookie: sessionId=abc123; HttpOnly; Secure
          │
          ▼
第四步:之后每次请求,浏览器自动带上这个 Cookie
        Cookie: sessionId=abc123
          │
          ▼
第五步:服务器拿着 abc123 去查数据库,找到对应用户
        → 知道你是谁,放行 ✓

其中有个关键词:HttpOnly

这个属性告诉浏览器:"这个 Cookie 只能在 HTTP 请求里用,JavaScript 代码不能读它。"这样即使页面被注入了恶意 JS 代码,也偷不走你的 sessionId。

代码语言:javascript
复制
// Node.js + Express 设置 Session 的代码
app.use(session({
  secret: process.env.SESSION_SECRET,    // 加密用的密钥,别写死在代码里
  store: new RedisStore({ client: redis }), // Session 存 Redis,不然重启就丢了
  cookie: {
    httpOnly: true,    // JS 读不到,防止 XSS 盗取
    secure: true,      // 只在 HTTPS 下发送
    sameSite: 'strict', // 防止 CSRF 攻击
    maxAge: 24 * 60 * 60 * 1000  // 24 小时后过期
  }
}));

Session 的麻烦

Session 的数据存在服务器上,这叫"有状态"。

单台服务器没问题。但现代应用通常有很多台服务器同时运行(防止单点故障、应对高并发),问题就来了:

代码语言:javascript
复制
你的请求 → 负载均衡 → 服务器A(有你的 Session)✓ 认识你
你的请求 → 负载均衡 → 服务器B(没有你的 Session)✗ 不认识你!

解决方法是把所有 Session 集中存到 Redis 里,但这又多了一个需要维护的基础设施,而且 Redis 挂了就全完了。

所以:Session 适合单台服务器的应用,在分布式系统里用起来比较麻烦。

3. API Key:给机器用的"永久通行证"

用生活场景理解

你给快递员配了一把小区大门的专用钥匙,这把钥匙只能开大门,进不了你家。

钥匙丢了?再配一把,把旧的作废就行,不影响你家里的其他钥匙。

API Key 就是这个逻辑——给程序用的、可以独立管理的访问凭证

怎么用

你在 Stripe(支付平台)注册,它给你一个 Key:sk_live_abc123...

你的后端代码每次调用 Stripe API 时带上它:

代码语言:javascript
复制
GET /v1/charges HTTP/1.1
Host: api.stripe.com
Authorization: Bearer sk_live_abc123def456

Stripe 服务器查一下这个 Key 对应哪个账户,检查权限,然后处理请求。

有一个血泪教训:绝对不要把 API Key 放在 URL 参数里!

代码语言:javascript
复制
❌ 危险:GET /api/data?api_key=abc123
           ↑ 这个 URL 会出现在服务器日志里,别人一翻日志就能看到

✓ 正确:放在 HTTP Header 里

API Key 的优势

  • 泄露了只需要换一个,其他 Key 不受影响
  • 可以给不同的 Key 设置不同的权限(比如只读 Key、只写 Key)
  • 可以对每个 Key 单独限流(知道是哪个调用方在使用)

一个安全细节:存储时要哈希

代码语言:javascript
复制
import crypto from'crypto';

// 生成 API Key
function generateApiKey(): string {
return`sk_${crypto.randomBytes(32).toString('hex')}`;
}

// ⚠️ 重要:存数据库时不能存明文,要存哈希值
// 就像密码一样,万一数据库泄露,别人也拿不到真实的 Key
const rawKey = generateApiKey();  // 这个只展示给用户一次
const hashedKey = crypto
  .createHmac('sha256', process.env.HASH_SECRET!)
  .update(rawKey)
  .digest('hex');  // 存这个到数据库

await db.apiKeys.create({ hash: hashedKey, userId, scopes: ['read'] });

API Key 识别的是"哪个应用在调用",不是"哪个用户在操作"。 这是它的本质局限。

4. Bearer Token:一个经常被误解的"格式"

先搞清楚一件事

很多人把 Bearer Token 当成一种鉴权方案,其实它只是一个 HTTP 请求头的写法规范,不是鉴权机制本身。

代码语言:javascript
复制
Authorization: Bearer 这里放什么都行

"Bearer"这个词的意思是"持有人"——谁持有这个 token,谁就能访问。

至于 token 里面是什么?可以是:

  • 一个随机字符串(像 API Key)
  • OAuth 服务器发的令牌
  • JWT(最常见的用法)
  • 任何服务端能认出来的东西

你可以把 Bearer Token 理解成一个"信封格式",里面装什么要看具体的鉴权方案。

理解了这一点,下面讲 JWT 和 OAuth 就不会再混淆了。

5. JWT:把"用户信息"直接装进 Token

为什么需要 JWT?

回顾一下 Session 的问题:用户信息存在服务器,分布式系统里很麻烦。

JWT 的思路反过来:把用户信息直接打包进 Token,发给客户端存着,服务器不用存任何东西。

就像把你的个人档案装进一个防伪信封,信封上有官方印章(签名)。任何机构拿到这个信封,验证印章真实,就知道里面的内容是可信的——不需要打电话去总部查档案

JWT 的结构

一个 JWT 长这样(三段用 . 分隔):

代码语言:javascript
复制
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcwMDAwMDAwMH0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

把这三段解码,分别是:

代码语言:javascript
复制
第一段(Header):告诉你用什么算法签名的
{
"alg": "HS256",
"typ": "JWT"
}

第二段(Payload):用户信息,随便放什么
{
"sub": "user_123",         // 用户 ID
"role": "admin",           // 角色
"email": "zhang@example.com",
"exp": 1700003600          // 过期时间(Unix 时间戳)
}

第三段(Signature):签名,用来防止有人篡改前两段
HMACSHA256(第一段 + "." + 第二段, 服务器的密钥)

关键:Payload 不是加密的,只是 Base64 编码!任何人都能解开看到里面的内容。所以不要往里面放密码、手机号这类敏感信息。

使用流程

代码语言:javascript
复制
登录
  │
  ▼  服务器验证通过,用密钥签名生成 JWT,返回给你
  │
  ▼  你的浏览器/App 保存这个 JWT
  │
  ▼  之后每次请求,带上 JWT:
     Authorization: Bearer eyJhbGci...
  │
  ▼  服务器收到后:
     1. 用同一个密钥验证签名(确认没被篡改)
     2. 检查 exp 字段有没有过期
     3. 从 Payload 里直接读取用户信息
     → 全程不需要查数据库!
代码语言:javascript
复制
import jwt from'jsonwebtoken';

// 登录成功后,生成 JWT
const token = jwt.sign(
  {
    sub: user.id,
    role: user.role,
    email: user.email
    // ❌ 不要放:password、phone、idCard 等敏感信息
  },
  process.env.JWT_SECRET!,  // 这个密钥要好好保存,泄露就完了
  { expiresIn: '15m' }      // 15 分钟过期,为什么这么短?下面解释
);

// 每次请求时,验证 JWT 的中间件
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
try {
    // 从请求头里取出 token
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) {
      return res.status(401).json({ error: '没有提供 token' });
    }

    // 验证签名 + 检查过期时间(jwt.verify 自动做这两件事)
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
    req.user = decoded;  // 把用户信息挂到请求上,后续代码直接用
    next();
  } catch (err) {
    res.status(401).json({ error: 'token 无效或已过期' });
  }
};

JWT 最大的硬伤:没法主动让它失效

想象你的会员卡丢了,你去前台挂失,前台说:"没办法,这张卡在过期之前都有效,等它自己过期吧(还有 30 天)。"

这就是 JWT 的问题——你没法强制让一个 JWT 立刻失效,只能等它自己过期。

用户被封号了?Token 照样能用。用户改了密码想踢出所有设备?做不到。

解决方案:双 Token 机制

代码语言:javascript
复制
登录成功,同时颁发两个 token:
  ├── access_token(15分钟过期)← 用来访问 API,过期了就换
  └── refresh_token(7天过期,存数据库)← 用来换新的 access_token

正常请求:用 access_token
access_token 过期:用 refresh_token 换一个新的 access_token
退出登录:从数据库删除 refresh_token(立刻失效,下次刷新就换不出来了)
封号用户:删掉他的 refresh_token,access_token 最多再撑 15 分钟

这样,"无法主动吊销"的影响窗口最大只有 15 分钟,大多数场景够用了。

四个常见坑,别踩

原因

正确做法

Payload 放密码/手机号

Base64 可以直接解码,不是加密

只放用户ID、角色等非敏感信息

Token 存 localStorage

JS 可以读取,XSS 攻击直接偷走

存 HttpOnly Cookie

过期时间设 30 天

等于永不过期,泄露了无法挽救

access_token 最长 15 分钟

不验证签名算法

攻击者可以把算法改成 "none"

明确指定接受的算法

6. OAuth 2.0:解决"我不想给你密码,但允许你访问我的数据"

先用生活场景理解

你租了一间民宿,房东给你一把只能开你住的那间房的钥匙,你不需要知道房东家里主卧的密码,你也进不了其他房间。

"用 GitHub 登录某个网站"——你不需要把 GitHub 密码告诉那个网站,但那个网站可以读取你 GitHub 上的公开信息(你允许的范围内)。

这就是 OAuth 2.0 解决的问题:在不交出密码的情况下,授权第三方访问你的数据。

先烙印这句话

OAuth 2.0 是授权协议,不是认证协议。 它告诉第三方 "你可以访问用户的这些数据",但不告诉第三方"这个用户是谁"。

四个角色,一个例子

以"用 GitHub 账号登录掘金"为例:

代码语言:javascript
复制
你(资源所有者)             ← 你的 GitHub 数据的主人
掘金网站(客户端)           ← 想要访问你数据的第三方应用
GitHub 授权服务器(Auth Server)← 负责问你"同意吗"的中间人
GitHub API(资源服务器)    ← 真正存着你数据的地方

授权码流程(最安全、最常用,逐步拆解)

代码语言:javascript
复制
第一步:你在掘金点击"用 GitHub 登录"
          │
          ▼
第二步:掘金把你的浏览器跳转到 GitHub 授权页
        URL 里带着:掘金的 client_id、跳回哪里、要什么权限
        GET https://github.com/login/oauth/authorize
          ?client_id=掘金的应用ID
          &redirect_uri=https://juejin.cn/callback
          &scope=read:user,user:email    ← 申请读取用户信息的权限
          &state=随机字符串              ← 防止 CSRF 攻击用的
          │
          ▼
第三步:你在 GitHub 上点"同意授权"
          │
          ▼
第四步:GitHub 把你的浏览器跳回掘金,带着一个短命的"授权码"
        https://juejin.cn/callback?code=abc123&state=随机字符串
          │
          ▼
第五步:掘金后端服务器拿着这个 code,悄悄去找 GitHub 换 token
        (这一步在服务端发生,你的浏览器看不见)
        POST https://github.com/login/oauth/access_token
        { client_id, client_secret, code }
          │
          ▼
第六步:GitHub 返回 access_token
          │
          ▼
第七步:掘金用 access_token 调用 GitHub API,读取你的公开信息

为什么要多一个 code,不直接返回 token?

因为第四步的 URL 会出现在浏览器地址栏、历史记录、服务器日志里,太危险了。code 是一次性的而且极短命,即使被截获也没用。真正的 token 在第五步通过服务端安全通道获取。

代码语言:javascript
复制
// 第五步:用 code 换 token(在你的后端实现)
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  },
  body: JSON.stringify({
    client_id: process.env.GITHUB_CLIENT_ID,
    client_secret: process.env.GITHUB_CLIENT_SECRET,  // 这个绝不能泄露
    code: req.query.code,
  })
});

const { access_token } = await tokenRes.json();

// 第七步:用 access_token 获取用户信息
const userRes = await fetch('https://api.github.com/user', {
  headers: { Authorization: `Bearer ${access_token}` }
});
const githubUser = await userRes.json();
// { login: "zhangsan", email: "...", avatar_url: "..." }

再强调一次:OAuth 不告诉你用户是谁

你拿到 access_token 后,你知道的只是"这个 token 有权限访问某个 GitHub 账号",但你不知道这个账号对应的用户信息(除非再调一次 API)。

这就是 OIDC 要解决的问题。

7. OIDC:OAuth 的升级版,"我不但授权,还告诉你我是谁"

一句话理解

OIDC = OAuth 2.0 + 用户身份信息

OAuth 给你一把钥匙,OIDC 在钥匙上刻了"这把钥匙的主人叫张三,邮箱是 xxx"。

怎么开启?

在请求权限的时候,scope 里加上 openid

代码语言:javascript
复制
scope=openid email profile

这样授权服务器除了返回 access_token,还会额外返回一个 id_token

id_token 是一个 JWT,里面直接包含了用户信息:

代码语言:javascript
复制
{
  "iss": "https://accounts.google.com",   // 谁签发的(Google)
"sub": "1234567890",                    // 用户在 Google 的唯一 ID
"email": "zhang@gmail.com",
"name": "张三",
"picture": "https://头像URL",
"aud": "你的应用ID",                    // 这个 token 是给谁的
"exp": 1700003600                       // 过期时间
}

你验证这个 JWT 的签名(用 Google 的公钥,会自动从固定地址获取),验证通过,就立刻知道用户是谁了——不需要额外再调 API

每次你点"用 Google/GitHub/Apple 登录",背后跑的都是 OIDC 流程。

8. SSO:一次登录,畅通所有系统

你一定体验过这个

进公司,用工号密码登录 OA 系统。然后打开飞书——已经登录了。打开内部 GitLab——也已经登录了。

你只登录了一次,但所有系统都认识你,这就是 SSO(Single Sign-On,单点登录)。

原理

有一个所有系统都信任的"大哥"——身份认证中心(IdP,Identity Provider)。

代码语言:javascript
复制
所有系统都问大哥:这个人可以信任吗?

┌─────────────────────────────────────────────┐
│          身份认证中心(大哥/IdP)             │
│    (Okta、Azure AD、或公司自建的系统)        │
└──────────┬──────────────────────────────────┘
           │ 管理所有用户的登录状态
           │
    ┌──────┼──────────┐
    ▼      ▼          ▼
   OA    飞书     内部 GitLab
  (小弟们,都听大哥的)

流程:

代码语言:javascript
复制
你第一次访问飞书
  → 飞书:"你没登录,去大哥那里确认一下"
  → 大哥:"你有登录记录吗?"
  → 没有 → 你输入工号密码 → 大哥验证通过 → 建立 SSO Session
  → 大哥给飞书一个证明 → 飞书放你进去 ✓

你之后访问 OA
  → OA:"你没登录,去大哥那里"
  → 大哥:"你有登录记录!" → 直接给 OA 一个证明
  → OA 放你进去 ✓ (你全程不用再输密码)

在代码里接入 SSO

代码语言:javascript
复制
// 用 passport.js 接入公司的 Okta SSO(基于 OIDC 协议)
passport.use('sso', new OIDCStrategy({
  issuer: 'https://your-company.okta.com',
  clientID: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  callbackURL: '/auth/callback',
  scope: 'openid email profile',
}, (tokenSet, userInfo, done) => {
  // userInfo 里就是用户信息,直接用
  // 通常还要查本地数据库,看这个用户在你系统里有没有账号
  return done(null, userInfo);
}));

微服务里的 SSO 怎么做?

代码语言:javascript
复制
所有外部流量
      │
      ▼
┌─────────────┐
│  API 网关   │ ← 统一在这里验证用户身份
│ 验证 token │   然后把用户信息注入到请求头里
└──────┬──────┘
       │ X-User-Id: 42
       │ X-User-Role: admin
       ▼
  ┌────┼────┬───────┐
  ▼    ▼    ▼       ▼
服务A 服务B 服务C  服务D
(各自只处理业务,不用重复验证身份)

🧩 全局图:现代系统怎么组合使用这些方案

真实的生产系统,不同场景用不同方案:

代码语言:javascript
复制
┌──────────────────────────────────────────┐
│         面向用户的 Web App                │
│  登录:OIDC(对接公司 SSO 或三方登录)    │
│  维持登录状态:JWT + HttpOnly Cookie      │
│  服务间调用:Bearer JWT 透传              │
└──────────────────────────────────────────┘

┌──────────────────────────────────────────┐
│         对外开放的 API(给开发者用)       │
│  认证:API Key                           │
│  权限控制:OAuth 2.0 Scope               │
│  限流:按 API Key 单独限制               │
└──────────────────────────────────────────┘

┌──────────────────────────────────────────┐
│         公司内部服务互相调用              │
│  认证:OAuth 2.0 Client Credentials      │
│  没有用户参与,纯机器对机器              │
└──────────────────────────────────────────┘

🛡️ 安全底线:这 8 条不讲条件

  1. 全程用 HTTPS,没有 HTTPS,所有鉴权都是裸奔
  2. Cookie 加 HttpOnly + Secure,防止 JS 偷走 Token/SessionId
  3. access_token 过期时间控制在 15 分钟以内,配合 refresh_token 使用
  4. 移动端和单页应用用 PKCE,不要在客户端代码里放 client_secret
  5. 验证 JWT 时检查三个字段:iss(谁签发的)、aud(给谁用的)、exp(有没有过期)
  6. API Key 存数据库时要哈希,和存密码是同等级别的操作
  7. OAuth 流程里带 state 参数,防止 CSRF 攻击
  8. 只申请必要的权限,用不到的 scope 不要加

🧠 一句话记住每种方案

方案

一句话

适合用在

Basic Auth

每次请求都带用户名密码

内部工具、快速原型

Session

登一次,拿票,凭票进场

单体应用、管理后台

API Key

机器用的永久通行证

服务端 API、第三方集成

Bearer Token

一个请求头格式,装什么都行

配合 JWT/OAuth 使用

JWT

用户信息打包进 Token,验签不查库

微服务、分布式系统

OAuth 2.0

授权别人访问我的数据,不给密码

三方登录、开放 API

OIDC

OAuth + 顺便告诉你用户是谁

所有需要用户身份的场景

SSO

一次登录,所有系统都放行

企业内部多系统

鉴权不是魔法,就是工程师们一个接一个地解决问题。理解每种方案解决了什么问题,面试里任何相关问题你都能从容展开来讲。

你在项目里用的是哪种方案?或者踩过哪些 Auth 相关的坑?

评论区说说——踩过的坑够多够典型,下期可以专门出一篇《真实项目里的 Auth 翻车故事》😄


关注「前端达人」,每周持续更新深度技术文章。 觉得有用的话,转发给还在懵圈的朋友——前端也能把 Auth 讲得比后端更清楚 😎

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 🔐 先说一个你天天在做却没意识到的事
  • 1. Basic Auth:最原始的方式,"每次都出示身份证"
    • 用生活场景理解
    • 实际是怎么传的?
    • 问题出在哪?
      • 现在还有用吗?
  • 2. Session:聪明了一步,"登一次,记住你"
    • 用生活场景理解
    • 完整流程
    • Session 的麻烦
  • 3. API Key:给机器用的"永久通行证"
    • 用生活场景理解
    • 怎么用
    • API Key 的优势
    • 一个安全细节:存储时要哈希
  • 4. Bearer Token:一个经常被误解的"格式"
    • 先搞清楚一件事
  • 5. JWT:把"用户信息"直接装进 Token
    • 为什么需要 JWT?
    • JWT 的结构
    • 使用流程
    • JWT 最大的硬伤:没法主动让它失效
    • 四个常见坑,别踩
  • 6. OAuth 2.0:解决"我不想给你密码,但允许你访问我的数据"
    • 先用生活场景理解
    • 先烙印这句话
    • 四个角色,一个例子
    • 授权码流程(最安全、最常用,逐步拆解)
    • 再强调一次:OAuth 不告诉你用户是谁
  • 7. OIDC:OAuth 的升级版,"我不但授权,还告诉你我是谁"
    • 一句话理解
    • 怎么开启?
  • 8. SSO:一次登录,畅通所有系统
    • 你一定体验过这个
    • 原理
    • 在代码里接入 SSO
    • 微服务里的 SSO 怎么做?
  • 🧩 全局图:现代系统怎么组合使用这些方案
  • 🛡️ 安全底线:这 8 条不讲条件
  • 🧠 一句话记住每种方案
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档