噗噗的冒险乐园
49.73M · 2026-02-26
最近在调试一个第三方登录功能时,我遇到了一个奇怪的现象:明明看到页面跳转了,用户也成功登录了,但我在 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 是:
?code=gho_xxxxxxxxxxxx&state=random_string
原来 code 一直在这里!只是它不是通过 Fetch/XHR 发送的,而是作为页面跳转(重定向)的一部分。
这次经历让我产生了几个问题:
接下来,让我们一个个搞清楚。
Doc(Document)请求,指的是 HTML 文档请求,也就是浏览器加载的页面本身。
这个定义听起来有点抽象,我们用代码来看什么情况下会产生 Doc 请求:
// 环境: 浏览器
// 场景: 各种触发 Doc 请求的方式
// 1. 直接在地址栏输入 URL
//
// → 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 请求的关键特征:
为了更清楚地理解 Doc 在整个 Network 面板中的位置,我整理了一个对比表:
| 类型 | 含义 | 何时查看 | 典型例子 |
|---|---|---|---|
| All | 所有请求 | 排查复杂问题,需要看完整链路 | - |
| Doc | HTML 文档 | 页面跳转、重定向、认证回调 | 登录重定向、OAuth 回调 |
| Fetch/XHR | AJAX 请求 | API 调用、异步数据加载 | fetch('/api/user') |
| JS | JavaScript 文件 | 脚本加载问题、404 错误 | <script src="app.js"> |
| CSS | 样式表 | 样式未生效、加载失败 | <link href="style.css"> |
| Img | 图片 | 图片显示异常、加载慢 | <img src="photo.jpg"> |
| Media | 音视频 | 媒体播放问题 | <video src="movie.mp4"> |
| Font | 字体文件 | 字体显示异常、图标不显示 | @font-face 引用的字体 |
| WS | WebSocket | 实时通信连接问题 | new WebSocket(url) |
一个简单的记忆方法:
根据我的经验,以下场景必须查看 Doc 类型的请求:
1. 调试页面跳转流程
用户登录 → 重定向到首页 → 重定向到之前访问的页面
每一次重定向都是一个 Doc 请求,需要追踪完整链路。
2. 排查认证问题
3. 追踪 HTTP 重定向链路
4. 分析页面加载性能
调试技巧:
在 Chrome DevTools 的 Network 面板中:
勾选 "Preserve log"(保留日志)
取消过滤或选择 "All"
关注 Status 列的 3xx 状态码
理解 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>
特点:
方式 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();
}
特点:
为什么要区分这两种方式?
因为它们的调试方法、安全模型和适用场景都不同:
| 特性 | Doc(页面导航) | Fetch/XHR(异步请求) |
|---|---|---|
| 触发方式 | 链接点击、表单提交、重定向 | JavaScript 代码 |
| 页面刷新 | 是 | 否 |
| Cookie 携带 | 自动 | 需配置 credentials |
| 跨域限制 | 无(但有其他安全机制) | 严格的 CORS 检查 |
| 调试位置 | Network → Doc | Network → Fetch/XHR |
| 适用场景 | 传统 Web、SSO、OAuth | 单页应用、REST API |
现在让我们回到文章开头的问题:为什么 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=;' +
'state=random_string';
// 页面跳转(产生 Doc 请求)
window.location.href = authUrl;
// Network 面板会看到:
// Type: document
// URL: ?...
// 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 的原因!
可能你会想:为什么不用 Fetch/XHR 来实现 OAuth?这样就不用刷新页面了。
让我解释一下为什么这样做不通:
原因 1:安全性——用户密码不能经过第三方应用
问题情境: 用户(你)想让你的应用(比如 Notion)访问你的 Google Drive
错误做法:
Notion 让你在 Notion 页面输入 Google 密码
→ Notion 拿到了你的 Google 密码
→ 风险:Notion 可以随意访问你的 Google 账户
正确做法(OAuth):
Notion 把你重定向到 Google 登录页面
→ 你直接在 Google 页面输入密码
→ Notion 永远拿不到你的密码
→ Google 只给 Notion 一个有限权限的 token
原因 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!
除了 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 请求?
对比现代方式(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 + Doc | Token + Fetch/XHR |
|---|---|---|
| 请求类型 | Doc(页面跳转) | Fetch/XHR(异步) |
| 认证信息位置 | Cookie(Response Headers) | JSON 响应体 |
| 携带方式 | 浏览器自动 | 手动添加到 Headers |
| 用户体验 | 页面刷新 | 无刷新,流畅 |
| 调试位置 | Doc 请求 | Fetch/XHR 请求 |
| 典型应用 | 传统 Web 应用、企业内部系统 | 单页应用、移动 App API |
让我用实际的调试步骤演示一遍:
问题场景:
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 URL | URL 参数 ?code=xxx&state=yyy | OAuth code、查询参数 |
| Status | 3xx 状态码(301/302/303) | 识别重定向链路 |
| Request Headers | Cookie 字段 | 检查认证信息是否携带 |
| Response Headers | Set-Cookie 字段 | 检查 Cookie 是否正确设置 |
| Response Headers | Location 字段 | 查看重定向目标地址 |
常见问题排查:
为什么看不到某些请求?
为什么 Cookie 没有携带?
为什么一直重定向循环?
为什么 OAuth code 参数没有?
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: n'
# 可以在终端中重放这个请求
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 是什么?
2. 什么时候需要看 Doc?
3. 为什么需要 Doc 分类?
4. Network 分类速查
把 Network 面板想象成一个物流追踪系统:
当你需要亲自去某个地方完成某件事(比如银行签字、OAuth 授权),就是 Doc 请求。当你只需要获取数据(API 调用),就是 Fetch/XHR 请求。