为什么我的 Auth Token 藏在了 Network 面板的 Doc 里?

0 阅读10分钟

最近在调试一个第三方登录功能时,我遇到了一个奇怪的现象:明明看到页面跳转了,用户也成功登录了,但我在 Network 面板里怎么都找不到那个关键的 auth code。习惯性地勾选了 Fetch/XHR 过滤器,翻遍了所有请求,就是没有🤔。

直到我取消过滤,切换到 Doc 类型,才在 URL 参数里看到了 ?code=abc123&state=xyz

这次经历让我开始思考:Network 面板里的 Doc 到底是什么?为什么有些认证信息会出现在这里而不是 Fetch/XHR 里?这背后有什么机制?

问题的起源

一次真实的调试经历

场景是这样的:我在接入 GitHub OAuth 登录,流程看起来很顺利——用户点击"使用 GitHub 登录",跳转到 GitHub 授权页面,授权后跳回我的应用。但问题来了,我需要在回调中获取 GitHub 返回的 authorization code,然后用这个 code 去换取 access token。

按照惯例,我打开 Chrome DevTools,勾选 Network 面板的 Fetch/XHR 过滤器,准备查看 API 请求。结果——什么都没有。没有看到任何包含 code 参数的请求。

困惑了好一会儿,我才想起取消过滤,查看所有类型的请求。这时我注意到有个 Type 为 document 的请求,点开一看,Request URL 是:

https://myapp.com/callback?code=gho_xxxxxxxxxxxx&state=random_string

原来 code 一直在这里!只是它不是通过 Fetch/XHR 发送的,而是作为页面跳转(重定向)的一部分。

这背后的几个疑问

这次经历让我产生了几个问题:

  1. Network 面板里的 Doc 是什么?和 Fetch/XHR 有什么区别?
  2. 为什么 OAuth 的认证信息会出现在 Doc 请求里?
  3. 什么时候我应该去查看 Doc 类型的请求?
  4. Network 面板里其他的过滤类型都代表什么?

接下来,让我们一个个搞清楚。

认识 Network 面板中的请求分类

什么是 Doc 请求?

Doc(Document)请求,指的是 HTML 文档请求,也就是浏览器加载的页面本身。

这个定义听起来有点抽象,我们用代码来看什么情况下会产生 Doc 请求:

// 环境: 浏览器
// 场景: 各种触发 Doc 请求的方式

// 1. 直接在地址栏输入 URL
// https://example.com
// → Type: document

// 2. 点击链接跳转
const link = document.createElement('a');
link.href = '/dashboard';
link.click();
// → Type:document

// 3. JavaScript 页面跳转
window.location.href = '/dashboard';
// → Type:document

// 4. 表单提交(非 AJAX)
const form = document.createElement('form');
form.action = '/login';
form.method = 'POST';
form.submit();
// → Type:document

// 5. 服务端 HTTP 重定向
// Server Response:
// HTTP/1.1 302 Found
// Location:/callback?code=xxx
// → 浏览器自动发起新的 document 请求

相对的,下面这些操作不会产生 Doc 请求:

// 环境: 浏览器
// 场景: 这些是 Fetch/XHR 请求,不是 Doc

// 使用 fetch API
fetch('/api/user')
  .then(res => res.json())
// → Type: fetch

// 使用 XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET''/api/data');
xhr.send();
// → Type: xhr

// 使用 axios 等库
axios.get('/api/posts');
// → Type: xhr

Doc 请求的关键特征:

  • ✅ 会触发页面导航(浏览器地址栏的 URL 会改变)
  • ✅ 浏览器会解析返回的 HTML 并渲染页面
  • ✅ 可能伴随着 Cookie 的设置和自动携带
  • ✅ 会刷新整个页面(除非是 iframe)

Network 面板的过滤类型全解

为了更清楚地理解 Doc 在整个 Network 面板中的位置,我整理了一个对比表:

类型含义何时查看典型例子
All所有请求排查复杂问题,需要看完整链路-
DocHTML 文档页面跳转、重定向、认证回调登录重定向、OAuth 回调
Fetch/XHRAJAX 请求API 调用、异步数据加载fetch('/api/user')
JSJavaScript 文件脚本加载问题、404 错误<script src="app.js">
CSS样式表样式未生效、加载失败<link href="style.css">
Img图片图片显示异常、加载慢<img src="photo.jpg">
Media音视频媒体播放问题<video src="movie.mp4">
Font字体文件字体显示异常、图标不显示@font-face 引用的字体
WSWebSocket实时通信连接问题new WebSocket(url)

一个简单的记忆方法:

  • Doc = 页面本身(会导致页面刷新或跳转)
  • Fetch/XHR = 页面背后的数据请求(页面不刷新)
  • 其他类型 = 页面加载需要的各种资源

什么时候需要查看 Doc?

根据我的经验,以下场景必须查看 Doc 类型的请求:

1. 调试页面跳转流程

用户登录 → 重定向到首页 → 重定向到之前访问的页面

每一次重定向都是一个 Doc 请求,需要追踪完整链路。

2. 排查认证问题

  • OAuth/SAML 等第三方登录的回调
  • Cookie 是否正确设置(查看 Response Headers 的 Set-Cookie)
  • URL 参数中的临时 token 或 code

3. 追踪 HTTP 重定向链路

  • 服务端返回 301/302/303/307/308 状态码
  • 需要查看 Location 头部,了解重定向目标
  • 排查重定向循环问题

4. 分析页面加载性能

  • 首次加载时间(TTFB、DOM 解析时间)
  • 服务端渲染(SSR)的响应时间

调试技巧:

在 Chrome DevTools 的 Network 面板中:

  1. ✅ 勾选 "Preserve log"(保留日志)

    • 页面跳转后不会清空请求记录
    • 可以看到完整的重定向链路
  2. ✅ 取消过滤或选择 "All"

    • 看到所有类型的请求
    • 不会漏掉关键信息
  3. ✅ 关注 Status 列的 3xx 状态码

    • 302 Found, 301 Moved Permanently 等
    • 这些都是重定向请求

为什么需要 Doc 分类?

浏览器的两种数据获取方式

理解 Doc 和 Fetch/XHR 的区别,关键在于理解浏览器获取数据的两种不同方式。

方式 1:页面导航(Doc 请求)

这是浏览器的原生机制,从 Web 诞生之初就存在:

<!-- 环境: 浏览器 -->
<!-- 场景: 传统的页面导航 -->

<!-- 点击链接 -->
<a href="/products">查看产品</a>

<!-- 表单提交 -->
<form action="/login" method="POST">
  <input type="text" name="username">
  <input type="password" name="password">
  <button type="submit">登录</button>
</form>

特点:

  • ✅ 浏览器自动处理 Cookie、缓存、重定向
  • ✅ 不需要写 JavaScript 代码
  • ✅ 天然支持跨域(不受 CORS 限制)
  • ❌ 会刷新整个页面,用户体验较差

方式 2:异步请求(Fetch/XHR 请求)

这是 AJAX 时代引入的技术,由 JavaScript 控制:

// 环境: 浏览器
// 场景: 现代单页应用的数据获取

// 使用 fetch API
async function login(username, password) {
  const response = await fetch('/api/login', {
    method: 'POST'headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username, password })
  });
  
  const data = await response.json();
  return data.token;
}

// 后续请求手动携带 token
async function getUserInfo(token) {
  const response = await fetch('/api/user', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  
  return response.json();
}

特点:

  • ✅ 无需刷新页面,用户体验好
  • ✅ 完全由 JavaScript 控制,灵活性高
  • ❌ 需要手动处理认证信息(token)
  • ❌ 受 CORS 限制,跨域需要服务端配合

为什么要区分这两种方式?

因为它们的调试方法安全模型适用场景都不同:

特性Doc(页面导航)Fetch/XHR(异步请求)
触发方式链接点击、表单提交、重定向JavaScript 代码
页面刷新
Cookie 携带自动需配置 credentials
跨域限制无(但有其他安全机制)严格的 CORS 检查
调试位置Network → DocNetwork → Fetch/XHR
适用场景传统 Web、SSO、OAuth单页应用、REST API

真实案例:OAuth 认证为什么用 Doc?

现在让我们回到文章开头的问题:为什么 OAuth 的 auth code 会出现在 Doc 请求里?

让我用一个完整的 OAuth 流程来说明:

sequenceDiagram
    participant User as 用户浏览器
    participant App as 你的应用
    participant GitHub as GitHub

    User->>App: 1. 点击"Login with GitHub"
    Note over User,App: Fetch/XHR: GET /api/auth/github
    App->>User: 2. 返回授权 URL
    
    User->>GitHub: 3. 重定向到 GitHub
    Note over User,GitHub: Doc 请求: 页面跳转
    GitHub->>User: 4. 返回登录页面 HTML
    
    User->>GitHub: 5. 用户输入账号密码
    GitHub->>User: 6. 302 重定向回你的应用
    Note over User,App: Doc 请求: HTTP 重定向
    
    User->>App: 7. GET /callback?code=xxx&state=yyy
    Note over User,App: ⭐ code 在这个 Doc 请求的 URL 里
    
    App->>GitHub: 8. 用 code 换 token(服务端)
    Note over App,GitHub: 这个请求不在浏览器里
    GitHub->>App: 9. 返回 access_token

关键时刻分解:

步骤 3:跳转到 GitHub(Doc 请求)

// 环境: 浏览器
// 场景: 用户点击登录按钮后

// 前端构造 GitHub 授权 URL
const authUrl = 'https://github.com/login/oauth/authorize?' + 
  'client_id=your_client_id&' +
  'redirect_uri=https://yourapp.com/callback&' +
  'state=random_string';

// 页面跳转(产生 Doc 请求)
window.location.href = authUrl;

// Network 面板会看到:
// Type: document
// URL: https://github.com/login/oauth/authorize?...
// Status: 200

步骤 6-7:GitHub 重定向回你的应用(Doc 请求)

# GitHub 服务器的响应
HTTP/1.1 302 Found
Location: https://yourapp.com/callback?code=gho_xxxx&state=random_string

# 浏览器自动发起新的请求
GET /callback?code=gho_xxxx&state=random_string HTTP/1.1
Host: yourapp.com

# Network 面板会看到:
# Type: document
# URL: https://yourapp.com/callback?code=gho_xxxx&state=random_string
# Status: 200

这就是你在 Doc 里找到 auth code 的原因!

为什么 OAuth 必须用重定向(Doc)?

可能你会想:为什么不用 Fetch/XHR 来实现 OAuth?这样就不用刷新页面了。

让我解释一下为什么这样做不通:

原因 1:安全性——用户密码不能经过第三方应用

问题情境: 用户(你)想让你的应用(比如 Notion)访问你的 Google Drive

❌ 错误做法:

Notion 让你在 Notion 页面输入 Google 密码

→ Notion 拿到了你的 Google 密码

→ 风险:Notion 可以随意访问你的 Google 账户

✅ 正确做法(OAuth):

Notion 把你重定向到 Google 登录页面

→ 你直接在 Google 页面输入密码

→ Notion 永远拿不到你的密码

→ Google 只给 Notion 一个有限权限的 token

重定向保证了用户直接在资源提供方(Google)的页面输入密码,密码不会经过第三方应用。

原因 2:跨域限制——CORS 会阻止 AJAX 请求

// 环境: 浏览器
// 场景: 如果尝试用 AJAX 请求 GitHub 授权页面

// ❌ 这样做不行
fetch('https://github.com/login/oauth/authorize?...')
  .then(res => res.text())
  .then(html => {
    // 想法:拿到 GitHub 登录页面的 HTML,显示在我的页面里
  });

// 会遇到的问题:
// 1. CORS 错误:GitHub 不允许你的域名跨域请求
// 2. 即使能拿到 HTML,用户在你的页面输入密码也不安全
// 3. 无法获取 GitHub 的 Cookie,登录状态无法维护

原因 3:浏览器的自动行为——重定向无需编写代码

# HTTP 重定向是浏览器的标准功能
# 服务器只需要返回一个响应头:

HTTP/1.1 302 Found
Location: https://yourapp.com/callback?code=xxx

# 浏览器会自动:
# 1. 提取 Location 头的 URL
# 2. 发起新的请求(Doc 请求)
# 3. 更新地址栏 URL
# 4. 渲染新页面

# 不需要写任何 JavaScript

Cookie 认证为什么也在 Doc 里?

除了 OAuth,传统的 Cookie 认证也主要依赖 Doc 请求。让我们看一个例子:

// 环境: 浏览器
// 场景: 传统的表单登录

// HTML 表单
/*
<form action="/login" method="POST">
  <input type="text" name="username">
  <input type="password" name="password">
  <button type="submit">登录</button>
</form>
*/

// 用户提交表单时发生的事情:

// 1. 浏览器发送 Doc 请求
// POST /login HTTP/1.1
// Content-Type: application/x-www-form-urlencoded
// 
// username=alice&password=secret123

// 2. 服务器验证成功,返回重定向 + Set-Cookie
// HTTP/1.1 302 Found
// Set-Cookie: session_id=abc123xyz; HttpOnly; Secure; SameSite=Strict
// Location: /dashboard

// 3. 浏览器自动:
//    - 保存 Cookie
//    - 发起新的 Doc 请求到 /dashboard
//    - 自动携带 Cookie

// GET /dashboard HTTP/1.1
// Cookie: session_id=abc123xyz

为什么是 Doc 请求?

  1. Set-Cookie 在响应头中 → 只有 Doc 请求会自动处理 Set-Cookie
  2. Cookie 自动携带 → Doc 请求会自动带上同域的 Cookie
  3. 表单提交 → 传统 HTML form 产生的就是 Doc 请求

对比现代方式(Token 认证) :

// 环境: 浏览器
// 场景: 现代单页应用的 Token 认证

// 1. 用户登录(Fetch/XHR 请求)
async function login(username, password) {
  const response = await fetch('/api/login', {
    method: 'POST'headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password })
  });
  
  const data = await response.json();
  // { access_token: "eyJhbG...", refresh_token: "..." }
  
  // 手动存储 token
  localStorage.setItem('access_token', data.access_token);
}

// 2. 后续请求手动携带 token(Fetch/XHR 请求)
async function getUserInfo() {
  const token = localStorage.getItem('access_token');
  
  const response = await fetch('/api/user', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  
  return response.json();
}

两种方式对比:

特性Cookie + DocToken + Fetch/XHR
请求类型Doc(页面跳转)Fetch/XHR(异步)
认证信息位置Cookie(Response Headers)JSON 响应体
携带方式浏览器自动手动添加到 Headers
用户体验页面刷新无刷新,流畅
调试位置Doc 请求Fetch/XHR 请求
典型应用传统 Web 应用、企业内部系统单页应用、移动 App API

实战调试技巧

案例:找不到 OAuth 的 auth code

让我用实际的调试步骤演示一遍:

问题场景:

  • 接入 GitHub OAuth 登录
  • 用户点击登录,跳转到 GitHub,授权后跳回应用
  • 需要获取 URL 中的 code 参数

错误的调试方法:

  1. 打开 DevTools → Network 面板
  2. 勾选 Fetch/XHR 过滤器
  3. 刷新页面,点击登录
  4. 找不到包含 code 参数的请求 ❌

正确的调试步骤:

✅ Step 1: 取消所有过滤,选择 "All" → 看到所有类型的请求

✅ Step 2: 勾选 "Preserve log"(保留日志) → 防止页面跳转后记录被清空

✅ Step 3: 点击登录,完成授权流程

✅ Step 4: 在 Network 面板中,找 Type 为 "document" 的请求 → 按时间顺序,找最后几个 Doc 请求

✅ Step 5: 点击 Doc 请求,查看详情 → Request URL: yourapp.com/callback?co… → code 就在这里!

✅ Step 6: 查看 Headers 标签

→ Request Headers 可以看到 Cookie、Referer

→ Response Headers 可以看到 Set-Cookie

在 Console 中提取 code:

// 环境: 浏览器 Console
// 场景: 在回调页面提取 URL 参数

// 方法 1: 使用 URLSearchParams
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');

console.log('Auth code:', code);
console.log('State:', state);

// 方法 2: 手动解析(不推荐,但可以理解原理)
const queryString = window.location.search; // "?code=xxx&state=yyy"
const pairs = queryString.substring(1).split('&');

const result = {};
pairs.forEach(pair => {
  const [key, value] = pair.split('=');
  result[key] = decodeURIComponent(value);
});

console.log(result);
// { code: "gho_xxxx", state: "yyyy" }

调试清单

当你需要排查认证或重定向问题时,参考这个清单:

查看 Doc 请求时,重点关注:

位置关键信息用途
Request URLURL 参数 ?code=xxx&state=yyyOAuth code、查询参数
Status3xx 状态码(301/302/303)识别重定向链路
Request HeadersCookie 字段检查认证信息是否携带
Response HeadersSet-Cookie 字段检查 Cookie 是否正确设置
Response HeadersLocation 字段查看重定向目标地址

常见问题排查:

  1. ❓ 为什么看不到某些请求?

    • → 检查是否勾选了 "Preserve log"
    • → 取消类型过滤,选择 "All"
  2. ❓ 为什么 Cookie 没有携带?

    • → 检查 Cookie 的 Domain 是否匹配
    • → 检查 Cookie 的 Path 是否正确
    • → 检查 SameSite 属性(Strict/Lax/None)
  3. ❓ 为什么一直重定向循环?

    • → 启用 "Preserve log",查看完整的重定向链
    • → 找出循环的起点和终点
    • → 检查服务端的重定向逻辑
  4. ❓ 为什么 OAuth code 参数没有?

    • → 检查 state 参数是否匹配(CSRF 防护)
    • → 查看 GitHub 的错误提示(可能在 URL 参数里)
    • → 确认 redirect_uri 配置是否正确

Chrome DevTools 实用技巧

1. 复制请求为 cURL

# 在 Network 面板中:
# 1. 右键请求
# 2. Copy → Copy as cURL

# 得到类似这样的命令:
curl 'https://yourapp.com/callback?code=gho_xxxx&state=yyyy' \
  -H 'Accept: text/html' \
  -H 'Cookie: session_id=abc123' \
  -H 'Referer: https://github.com/login'

# 可以在终端中重放这个请求

2. 保存网络日志(HAR 文件)

右键 Network 面板 → Save all as HAR with content

用途:
- 保存完整的请求/响应记录
- 可以导入到其他工具分析
- 分享给同事协助调试

⚠️ 注意:HAR 文件包含敏感信息(Cookie、Token),不要随意分享

3. 过滤和搜索

在 Network 面板的 Filter 输入框中:

# 按域名过滤
domain:github.com

# 按状态码过滤
status-code:302

# 按请求方法过滤
method:POST

# 按资源大小过滤
larger-than:1M

# 组合使用
domain:api.example.com status-code:200

小结

通过这次对 Network 面板 Doc 类型的探索,我理清了几个关键点:

核心要点

1. Doc 是什么?

  • HTML 文档请求,即浏览器加载的页面本身
  • 所有触发页面导航的操作(链接点击、表单提交、重定向)都会产生 Doc 请求
  • 会刷新页面,会自动处理 Cookie 和重定向

2. 什么时候需要看 Doc?

  • 调试页面跳转重定向流程
  • 排查认证问题(OAuth 回调、Cookie 设置)
  • 追踪 URL 参数中的临时信息(code、state、token)
  • 分析完整的请求链路(配合 Preserve log)

3. 为什么需要 Doc 分类?

  • 浏览器有两种数据获取方式:页面导航(Doc)和异步请求(Fetch/XHR)
  • OAuth/SSO 等认证流程必须用重定向(安全性 + 跨域限制)
  • Cookie 认证依赖浏览器的自动行为(自动携带、自动处理 Set-Cookie)
  • 不同的机制需要不同的调试方法

4. Network 分类速查

  • Doc → 页面跳转、重定向、认证回调
  • Fetch/XHR → API 调用、异步数据加载
  • JS/CSS/Img → 页面资源加载
  • WS → WebSocket 实时通信

一个类比帮助记忆

把 Network 面板想象成一个物流追踪系统:

  • Doc = 你本人去取件(需要走到快递点,拿到包裹后回家)
  • Fetch/XHR = 快递送上门(你在家里,东西直接送到)
  • JS/CSS/Img = 包裹里的物品(笔记本、衣服、配件等)

当你需要亲自去某个地方完成某件事(比如银行签字、OAuth 授权),就是 Doc 请求。当你只需要获取数据(API 调用),就是 Fetch/XHR 请求。

一个调试口诀

看不到数据?先别慌

取消过滤查 All 类型

重定向认证看 Doc

API 数据看 XHR

保留日志 Preserve log

完整链路不会漏

参考资料