更新记录(仅首版,2026年08月20日 发布)
- 首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176238)
以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。
Go Komura(2026)。《企业内网代理与 Windows 应用——梳理 WinINET、WinHTTP 与 .NET 的代理解析》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-proxy-wininet-winhttp-dotnet/
- DOI(已登记存档)
- 10.5281/zenodo.22176238
- DOI(上次登记版本)
- 10.5281/zenodo.22176239
“浏览器能打开外部网站,只有业务应用连不上外部 API。”“手动运行没问题,一改成 Windows 服务就立刻失败。”在有企业内网代理的环境里,这类错位很常见。
排查的出发点是:这个应用以谁的账户运行、读取哪套代理配置。Windows 的代理配置并不只有一套。浏览器、服务、.NET 的 HttpClient,可能各自引用着不同的配置。
大多数情况下,原因既不是代理服务器故障,也不是应用的缺陷,而是配置体系与运行账户之间的错位。本文面向中小企业的信息系统负责人和 Windows 应用开发者,按配置全貌、PAC 与认证与证书、排查步骤的顺序依次梳理。
| 你遇到的问题、想了解的内容 | 先读哪一节 |
|---|---|
| 只有浏览器能通信/配置改了但行为没变 | 三套配置体系 |
| 手动运行正常,做成服务就失败 | 运行账户的差异 |
| 只有特定 URL 不通/PAC 的配置没有被使用 | PAC 与 WPAD |
| 从 .NET Framework 迁移到 .NET 后行为变了 | .NET 的解析顺序 |
| 返回 407 | 代理认证 |
| 出现证书错误 | TLS 检查 |
| 不知道该从哪里查起 | 五步排查 |
HttpClient 的创建模式和超时设计本身,在“不要用 using 包裹 HttpClient”中讲解。本文的重点是:从哪套配置解析出代理,以及之后通信在哪一步停住。
1. 先说结论
首先需要把握的是以下三点。
- 看配置之前,先确定应用和运行账户。WinINET 的按用户配置、WinHTTP 的计算机配置、环境变量,读哪一套由应用端决定。管理员在自己屏幕上看到的配置,服务未必看得到。123
- 不要用能通的 URL,要用出问题的 URL 去查。PAC 会按 URL 分别返回代理或 DIRECT。在 .NET 中,运行时的差异、环境变量、处理程序上的显式指定,也会改变路径。456
- 把路径、认证、证书分开来查。407 是代理认证,与目标服务器的 401 是两回事。TLS 检查引发的证书错误,不是靠关闭验证来解决,而是靠分发企业内部 CA 和设置必要的排除项来处理。783
flowchart TB
accTitle: 代理排查中要对齐的信息
accDescr: 确定应用的 HTTP 栈与运行账户,对齐该账户能看到的配置和出问题的 URL,再分别检查路径、认证与证书
app["HTTP 栈与账户"] --> settings["确认该账户的配置"]
settings --> url["用出问题的 URL 确认路径"]
url --> error["认证与证书也分开查"]
图1:先对齐“哪套配置”“从谁的视角看到的配置”“哪个 URL”,再去查失败发生在哪里。
用一句话概括就是:说“已经确认过代理配置”时,要能说清看的是三套体系中的哪一套、是从哪个账户的视角看的。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 19 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. Windows 的“代理配置”有三套体系
Windows 上的应用找到企业内网代理的途径,大致分为三套体系。
| 配置体系 | 配置位置与命令 | 作用范围 | 主要由谁读取 |
|---|---|---|---|
| ① WinINET(Internet 选项) | 设置应用→网络和 Internet→代理,inetcpl.cpl |
按用户(默认) | 浏览器、交互式桌面应用、.NET Framework 的默认值 |
| ② WinHTTP(计算机配置) | netsh winhttp set proxy / set advproxy |
计算机 | Windows 服务、部分操作系统组件 |
| ③ 环境变量 | HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY |
进程(按定义位置继承) | .NET(Core 及以后)的 HttpClient、curl、Node.js 与 Python 等跨平台工具 |
“Windows 的代理配置”通常指的是①
设置应用里看到的“代理”,从历史上说就是 Internet Explorer 的 Internet 选项,也就是 WinINET 的配置。默认按用户分别保存。3
②是为服务这类“没有已登录用户的场景”准备的计算机级默认值。③主要是跨平台工具的做法,在 Windows 上,.NET(Core 及以后)和 curl 等也会读取它。5
读哪一套由应用决定,而不是由配置的人决定
使用 WinINET 的应用读①,使用 WinHTTP 的读②或应用自行指定的值,.NET(Core 及以后)则按③→①的顺序,引用对象是固定的。
因此,遇到“配置是对的却连不上”时,先确认你检查的配置和应用实际读取的配置是否属于同一套体系。在把三套体系都改成同一个值之前,先确定目标应用的入口更重要。
也有把按用户改成按设备的配置方式
启用组策略“按计算机(而不是按用户)进行代理设置”后,可以把①切换为计算机级,对所有用户应用同一份配置。使用 MDM(如 Intune)时,可以用 NetworkProxy CSP 按设备配置。3
flowchart TB
accTitle: 改变按用户配置的适用范围
accDescr: WinINET 的配置默认按用户,但可以用组策略切换为按计算机配置,MDM 则可以用 NetworkProxy CSP 按设备配置
user["WinINET 配置默认按用户"] --> gp["用 GPO 改为按计算机"]
gp --> all["对所有用户应用同一配置"]
mdm["MDM 的 NetworkProxy CSP"] --> device["按设备配置"]
图2:①的配置范围也可以改成按设备,所以“按用户”只是默认状态。
3. WinINET 与 WinHTTP——一个面向交互式应用,一个面向服务
3.1. 职责的差异
WinINET 和 WinHTTP 都是 Windows 自带的 HTTP 客户端栈,但它们设想的运行形态不同。
WinINET 面向交互式桌面应用。它会自动沿用用户的 Internet 选项、代理、Cookie 和凭据缓存,必要时还能弹出输入凭据的界面。另一方面,它不支持在服务或类似服务的进程中使用。1
WinHTTP 面向服务和服务器端。它支持以服务账户运行、线程模拟(impersonation)和会话隔离。作为代价,它不共享浏览器的配置、Cookie 和凭据,也不显示界面。2
Microsoft 给出的选型指引也是:只要不是在服务内部运行、也不是需要会话隔离或模拟的类服务进程,就用 WinINET;是服务就用 WinHTTP。1 不过,这并不意味着所有以服务方式运行的应用都会读取 WinHTTP 的计算机配置。.NET 编写的服务,放在 3.3 节单独说明。
3.2. netsh winhttp 的基本操作
WinHTTP 的计算机默认代理用 netsh 操作。9
:: 显示当前的 WinHTTP 代理配置
netsh winhttp show proxy
:: 设置静态代理(带绕过列表)
netsh winhttp set proxy proxy-server="proxy.example.co.jp:8080" bypass-list="*.example.co.jp;<local>"
:: 导入 Internet 选项(WinINET)的配置
netsh winhttp import proxy source=ie
:: 恢复为默认值(DIRECT)
netsh winhttp reset proxy
不要混淆静态配置、导入和自动配置
set proxy 是静态配置,不处理自动检测、PAC URL 的指定和代理认证。3
import proxy source=ie 只是把执行那一刻的静态配置复制过来。之后再修改 Internet 选项,WinHTTP 一侧不会跟着变。
flowchart TB
accTitle: 代理配置的导入只是一次性复制
accDescr: import proxy source=ie 只是把当时的静态配置复制到 WinHTTP,之后修改 Internet 选项也不会自动跟随
source["执行时刻的静态配置"] --> copy["import proxy source=ie"]
copy --> dest["复制到 WinHTTP"]
source -.-> later["日后的修改不会自动跟随"]
图3:import 不是同步配置,而是把当时的静态配置导入进来的操作。
要把 PAC 和 WPAD 自动检测也一并按计算机配置,就用 netsh winhttp set advproxy。JSON 形式的详细配置项有 Proxy、ProxyBypass、AutoconfigUrl、AutoDetect。9
3.3. 最常见的坑:服务不会读取用户的 IE 配置
典型例子是:开发者把在自己电脑上运行的工具,改成以 LocalSystem 运行的 Windows 服务。
开发时用自己的按用户配置(①)能通信,但 LocalSystem 能看到的是另一套配置。如果 WinHTTP 的计算机配置未设置(DIRECT),它就会尝试直连外部 API 并超时。改成服务本身的步骤,在“Windows 服务的做法与运维”中讲解。
flowchart TB
accTitle: 从手动运行改为服务后的错位
accDescr: 原本在开发者的按用户配置下运行的工具,一旦改成 LocalSystem 的服务,能看到的配置就变了,若 WinHTTP 的默认值是 DIRECT,它会尝试直连并失败
dev["以开发者身份手动运行"] --> works["用自己的配置能通信"]
works --> service["改为 LocalSystem 的服务"]
service --> changed["能看到的配置变了"]
changed --> direct["WinHTTP 未配置则直连"]
direct --> failure["访问外部 API 超时"]
图4:即使是同一台计算机,运行账户一变,能引用到的配置也会变。
配置要按目标 HTTP 栈读取的形式来准备
对于没有用户登录也要通信的进程,要准备计算机级的配置。但配置方式要与 HTTP 栈匹配。
| 对象 | 配置的准备方式 |
|---|---|
| 使用 WinHTTP 的原生应用与 Windows 组件 | 准备 netsh 的 WinHTTP 配置3 |
| 使用 .NET(Core 及以后)HttpClient 的服务 | 用系统环境变量(如 HTTPS_PROXY),或从应用配置中显式指定 HttpClientHandler.Proxy |
.NET(Core 及以后)的 HttpClient 不读取 WinHTTP 的计算机配置。请不要因为“它是服务,所以配 netsh 就行”而下判断。详细的优先级在第 5 章确认。
给外带电脑写死静态配置,会在反方向上出问题
如果给笔记本电脑固定写上企业内网的静态代理,到了公司外面就到不了这个代理,也就无法通信。计算机级静态配置应当视为适用于网络结构不变的服务器的手段。3
4. PAC 与 WPAD——“自动配置”的内部构造
PAC 负责计算路径,WPAD 负责找到 PAC 的位置。把“自动配置”拆成这两件事,就能看清该检查哪里。
4.1. PAC 文件与 FindProxyForURL
PAC(Proxy Auto-Configuration)是用 JavaScript(ECMAScript)编写的文件。其中必需的 FindProxyForURL(url, host) 函数,针对给定的 URL 和主机,返回要使用的代理列表,或表示直接连接的 DIRECT。10
function FindProxyForURL(url, host) {
// 企业内部域名和私有地址直连
if (dnsDomainIs(host, ".example.co.jp") ||
isInNet(host, "10.0.0.0", "255.0.0.0")) {
return "DIRECT";
}
// 其余走代理。第一个不可用就回退到下一个
return "PROXY proxy1.example.co.jp:8080; PROXY proxy2.example.co.jp:8080; DIRECT";
}
在这个例子里,企业内部域名和指定的私有地址直连,其余走代理。后者在第一个代理不可用时前进到下一个,最后回退到 DIRECT。
flowchart TB
accTitle: PAC 按目标改变路径
accDescr: 文中的 PAC 示例会判断 URL 和主机,若是企业内部域名或指定的私有地址就返回 DIRECT,否则返回按顺序排列的代理列表
target["传入 URL 和主机"] --> check{"匹配示例中的内网条件?"}
check -->|"是"| direct["返回 DIRECT"]
check -->|"否"| proxies["代理 1、代理 2、DIRECT 的顺序"]
图5:PAC 的答案随 URL 而变,所以别的网站访问成功,并不能验证出问题的 API 走的是哪条路径。
由此引出两点排查上的注意事项。
要用出问题的那个 URL 去确认。PAC 对不同 URL 可能给出不同的答案。WinHTTP 的自动代理功能同样被设计成每次都传入请求目标 URL 去查询。“浏览器上别的网站能打开”并不能证明出问题的 API 走的是同一条路径。4
代理日志里没有的通信,也要怀疑 DIRECT 和绕过。DIRECT 的含义是“不要走代理,直接去”。发往企业内部的通信没有出现在日志里时,就检查 PAC 的 DIRECT 判定和绕过列表是否命中。
4.2. 基于 WPAD 的自动检测
打开“自动检测设置”后,系统会用 WPAD(Web Proxy Auto-Discovery)去找 PAC 文件的位置。常见做法是用 DHCP 下发 PAC URL,或用 DNS 解析名为 wpad 的主机,再从 http://wpad/wpad.dat 这样的 URL 获取。11
flowchart TB
accTitle: 从自动检测到获取 PAC
accDescr: WPAD 会用 DHCP 或 DNS 查找 PAC 文件的位置,从该位置获取 PAC 并用于计算路径
auto["启用自动检测"] --> find["用 DHCP 或 DNS 查找位置"]
find --> pac["获取 PAC 文件"]
pac --> route["按目标计算路径"]
find -.-> missing["没有相应机制就检测失败"]
图6:自动检测需要网络侧用 DHCP/DNS 事先搭好机制。
在没有这套机制的网络里只打开自动检测并不会生效,反而会增加等到检测失败为止的时间。选了“自动”并不等于到哪里都能解析出来。
4.3. 读不了 PAC 的客户端的行为
即使分发了 PAC,也不是所有客户端都会去求值。netsh winhttp set proxy 是静态配置,而采用 HTTP_PROXY 环境变量方式的工具,原则上也没有地方可以写 PAC URL,只能指定固定的代理 URL。35
对于直接使用 WinHTTP 的原生应用,要确认会话的打开方式。
| WinHttpOpen 的用法 | 自动代理的处理 |
|---|---|
在 Windows 8.1 及以后指定 WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY |
WinHTTP 会按系统/用户的配置(含 WPAD 与 PAC)对每个请求自动解析12 |
指定传统的 WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 等 |
需要应用自己调用 WinHttpGetProxyForUrl,再把结果设置到请求上。DEFAULT_PROXY 在 8.1 及以后已不推荐1012 |
flowchart TB
accTitle: 确认 WinHTTP 是否会用到 PAC
accDescr: 若 WinHTTP 会话以 AUTOMATIC_PROXY 打开就会自动解析,而用传统方式打开时需要应用自己调用 AutoProxy API 并设置结果
open["确认 WinHttpOpen 的指定"] --> mode{"是否为 AUTOMATIC_PROXY?"}
mode -->|"是"| automatic["WinHTTP 自动解析"]
mode -->|"传统的打开方式"| api["应用调用 AutoProxy API"]
api --> apply["把结果设置到请求上"]
图7:只知道“它用的是 WinHTTP”,并不能判断 PAC 也会自动生效。
在较旧的实现里,即使有 PAC 也可能不会被使用。在以 PAC 运维的网络中,还要事先定好给读不了 PAC 的客户端用静态配置或环境变量下发什么。
5. .NET 的代理解析——Framework 与 Core 及以后是两回事
.NET Framework 和 .NET(Core 及以后)决定默认代理的方式不同。用 Framework 时代的知识去查 .NET 8 的应用,可能会查错配置。
5.1. .NET Framework——默认是 Internet 选项,用 defaultProxy 覆盖
.NET Framework 的 HttpWebRequest,以及构建在它之上的 HttpClient,只要不显式指定 Proxy 就会使用默认代理。默认代理由当前运行账户的 Internet 设置(相当于 WinINET)和配置文件组合决定,其中配置文件的设置优先。6
用 app.config 或 machine.config 的 system.net/defaultProxy 控制。13
<configuration>
<system.net>
<!-- useDefaultCredentials:是否向需要认证的代理发送默认凭据 -->
<defaultProxy enabled="true" useDefaultCredentials="true">
<proxy usesystemdefault="true"
proxyaddress="http://proxy.example.co.jp:8080"
bypassonlocal="true" />
<bypasslist>
<add address="[a-z]+\.example\.co\.jp$" />
</bypasslist>
</defaultProxy>
</system.net>
</configuration>
defaultProxy 为空时使用 Internet 选项的配置,写了 proxyaddress 等属性则以该指定为准。在程序里可以用 WebRequest.DefaultWebProxy 替换同一个默认值。136
flowchart TB
accTitle: .NET Framework 的默认代理
accDescr: .NET Framework 以当前运行账户的 Internet 设置为基础,并优先采用配置文件中指定的值来决定默认代理
account["运行账户的设置"] --> config["优先采用配置文件的指定"]
config --> default["Framework 的默认代理"]
replace["用 DefaultWebProxy 替换"] -.-> default
图8:Framework 的默认值取自“当前运行账户”的配置,未必是管理员屏幕上看到的那份。
以服务账户运行时,需要和 3.3 节一样的注意。默认读取的是该账户的 Internet 选项,与管理员桌面上看到的是两份不同的配置,而且通常是空的。
5.2. .NET(Core 及以后)——先看环境变量,再看操作系统的用户配置
.NET(Core 及以后)有一个静态属性 HttpClient.DefaultProxy。它是处理程序未显式指定时使用的默认值,在 Windows 上按先读环境变量,未定义时再读用户的代理配置的顺序初始化。5
| 环境变量 | 含义 |
|---|---|
HTTP_PROXY |
HTTP 请求使用的代理 |
HTTPS_PROXY |
HTTPS 请求使用的代理 |
ALL_PROXY |
上述变量未定义时的回退项 |
NO_PROXY |
不走代理的主机列表,用逗号分隔 |
区分指定代理的三个变量与 NO_PROXY
只要 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 之中定义了任意一个,它就优先于操作系统一侧的配置。如果测试用的 HTTPS_PROXY 忘了删,或者 CI/CD 的模板注入了它,实际路径就会与界面上看到的配置不同。
另一方面,只定义 NO_PROXY 并不会构成由环境变量指定的代理。在 Windows 上仍然会继续使用操作系统的用户代理配置。
flowchart TB
accTitle: Windows 上 .NET 默认代理的初始化
accDescr: HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 之中定义了任意一个就优先使用环境变量,都未定义时则使用 Windows 的用户配置。只有 NO_PROXY 不会构成由环境变量指定的代理
init["默认代理的初始化"] --> env{"是否有指定代理的三个变量?"}
env -->|"是"| useenv["优先使用环境变量"]
env -->|"否"| user["Windows 的用户配置"]
init -.-> no["只有 NO_PROXY 不构成代理"]
图9:先确认是否存在由环境变量指定的代理,并与只设了 NO_PROXY 的状态区分开。
NO_PROXY 的“前导点”与在 Linux 上的差异
NO_PROXY 不支持通配符(*)。带前导点的 .example.com 能匹配 www.example.com,但匹配不到 example.com 本身。5
在 Linux 容器等环境中,环境变量未定义时会以“不使用代理”初始化。Windows 与 Linux 的默认行为不同,因此迁移到容器时也要确认。5
5.3. 显式指定——HttpClientHandler.Proxy 与 UseProxy
无论哪个运行时,对 HttpClientHandler.Proxy 的显式指定都拥有最高优先级,优先于操作系统配置和配置文件。UseProxy = false 时完全不使用代理。11
using System.Net;
// 显式使用从应用配置中读取的代理
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://proxy.example.co.jp:8080")
{
BypassProxyOnLocal = true,
BypassList = new[] { @"^intra\.example\.co\.jp$" },
UseDefaultCredentials = true // 遇到需要认证的代理时,用运行账户的凭据应答
},
UseProxy = true
};
var client = new HttpClient(handler);
// 完全不使用代理的客户端(用于直连企业内部 API)
var directHandler = new HttpClientHandler { UseProxy = false };
var directClient = new HttpClient(directHandler);
flowchart TB
accTitle: 从处理程序确定实际生效的代理
accDescr: UseProxy 为 false 则直连,为 true 则使用处理程序上对 Proxy 的显式指定,没有显式指定时使用默认代理
use{"UseProxy 为 true?"} -->|"否"| direct["直连"]
use -->|"是"| explicit{"是否显式指定了 Proxy?"}
explicit -->|"是"| proxy["使用指定的代理"]
explicit -->|"否"| default["使用默认代理"]
图10:在查默认代理之前,先确认处理程序上的显式指定和 UseProxy。
也要注意发往本地时的自动绕过
在没有显式指定、遵循操作系统配置的情况下,不含点的扁平名称、环回地址、与本机域后缀相同的目标等,可能会被视为“本地”而被绕过。11
遇到“直接写 IP 地址和写名称行为不同”“改成 FQDN 后开始走代理了”时,就检查这个判定。
把上面的优先级汇总起来,如下所示。
| 优先级(高→低) | .NET Framework | .NET(Core 及以后) |
|---|---|---|
| 1 | HttpClientHandler.Proxy 等的显式指定 |
同左 |
| 2 | app.config 的 defaultProxy |
对 HttpClient.DefaultProxy 赋值 |
| 3 | 运行账户的 Internet 选项 | 环境变量(如 HTTP_PROXY) |
| 4 | ― | Windows 的用户代理配置 |
6. 需要认证的代理——407 是“代理的”认证错误
6.1. 不要混淆 407 和 401
即使到达了代理,过不了认证也无法继续。用状态码和标头来区分是谁在要求认证。7
| 响应 | 要求认证的一方 | 要检查的标头 |
|---|---|---|
| 407 Proxy Authentication Required | 代理 | Proxy-Authenticate |
| 401 | 目标服务器 | WWW-Authenticate |
遇到 407 时,先确认 Proxy-Authenticate 中列出的方案。Basic 是发送用户名和密码的方案,Negotiate(Kerberos/NTLM)等则是质询/响应方案。后者不会把密码本身发到网络上,而是经过多轮交互完成认证。7
sequenceDiagram
accTitle: 向需要认证的代理应答的流程
accDescr: 客户端从代理收到 407 和认证方案的通知,再按该方案用相应的凭据应答。质询响应方案需要多轮交互
participant C as 客户端
participant P as 代理
C->>P: 发起通信请求
P-->>C: 返回 407 与 Proxy-Authenticate
C->>P: 按方案应答认证
Note over C,P: 按方案可能需要多轮交互
图11:遇到 407 时,要确认的是发给代理的认证信息,而不是发给目标服务器的。
会“回退”到哪种认证方案,在“图解 NTLM 与 Kerberos”中有详细说明。
6.2. 在 .NET 中传递凭据的方式
遵循默认代理和显式指定代理,设置的位置不同。
使用默认代理并需要传递认证信息时,用 HttpClientHandler.DefaultProxyCredentials。它是在 UseProxy = true 且 Proxy = null 时,发给该默认代理的凭据。14
using System.Net;
var handler = new HttpClientHandler
{
UseProxy = true, // 默认值。与 Proxy = null 组合时使用系统默认的代理
Proxy = null,
// 用运行账户(已登录用户或服务账户)的凭据应答 407
DefaultProxyCredentials = CredentialCache.DefaultCredentials
};
var client = new HttpClient(handler);
显式指定代理时,把凭据放在 WebProxy 一侧。在多数客户端场景中,推荐使用已登录用户的默认凭据,而不是单独的用户名和密码,对应的写法就是 WebProxy.UseDefaultCredentials = true。15
6.3. 服务账户的 407 问题
“默认凭据”指的是运行该进程的账户的凭据。交互式用户会按该用户认证,LocalSystem 的服务则按计算机账户认证。
flowchart TB
accTitle: 改成服务后被认证的主体会变
accDescr: 使用默认凭据时,交互式运行按该用户认证,LocalSystem 的服务按计算机账户认证,因此要确认代理能否认证这个主体
run{"运行账户是什么?"} -->|"交互式用户"| user["该用户的凭据"]
run -->|"LocalSystem"| machine["计算机账户"]
user --> check["确认代理能否完成认证"]
machine --> check
图12:即使代码里一直选的是“默认”,改成服务后代理所认证的主体也会变。
在与 AD 联动、按用户认证的代理上,计算机账户或本地账户可能无法通过认证,407 会一直持续。反过来,也有环境按源 IP 或按账户为服务设置了免认证的例外。
因此,对要做成服务的应用,应在设计阶段就决定:是使用域的服务账户(如 gMSA),还是在代理侧设置免认证的例外,或者准备一个无需认证的内部中转代理。排查 407 需要同时看应用的配置,以及“代理能否认证这个运行账户”。
也可以像 HTTP_PROXY=http://user:pass@proxy:8080 这样把凭据嵌进环境变量。5 但明文密码会暴露在环境变量、也就是进程信息里,不建议作为长期运维手段。
7. HTTPS 与代理——CONNECT 隧道和 TLS 检查
7.1. HTTPS 以“隧道”的方式穿过代理
HTTPS 走代理时,客户端先发送 CONNECT 目标主机:443,打通一条 TCP 隧道。成功时代理返回 200,之后客户端与目标服务器在隧道内进行 TLS 握手。隧道没打通时会返回 407 或 502 等。16
flowchart TB
accTitle: HTTPS 隧道打通之前
accDescr: HTTPS 用 CONNECT 请求打开 TCP 隧道,返回 200 之后在隧道内进行 TLS 握手。隧道没有打通时会返回 407 或 502 等
connect["用 CONNECT 指定目标"] --> result{"隧道是否打通?"}
result -->|"成功、200"| tls["在隧道内建立 TLS 连接"]
result -->|"失败"| error["407 或 502 等"]
图13:把打开隧道阶段的失败和之后 TLS 连接的失败分开来看。
在这种“透传型”下,代理读不到加密后的 HTTPS 内容。日志里只能看到目标主机名和连接成败,看不到 URL 路径。
7.2. TLS 检查型代理与证书错误
TLS 检查(SSL 解密、break and inspect)型的做法是:代理先把 TLS 终结,解密并检查后再重新加密。呈现给客户端的证书,也被换成用代理自己的 CA 重新签名的证书。8
flowchart TB
accTitle: TLS 检查会换掉证书
accDescr: TLS 检查中代理会终结 TLS 并检查内容,向客户端呈现用自己的 CA 重新签名的证书,因此必须信任该 CA
proxy["代理终结 TLS"] --> inspect["解密、检查、重新加密"]
proxy --> cert["用企业内部 CA 重新签名的证书"]
cert --> trust["客户端一侧必须信任该 CA"]
图14:在检查型下,问题不只是目标服务器的证书,还在于能否信任企业内部 CA。
先确认是用哪个信任存储在做验证
这种结构要求把代理的 CA 证书分发到所有客户端的受信任根中。不仅是未分发的计算机,使用自带信任存储、不看 Windows 证书存储的运行时,同样会出现证书验证错误。
在 .NET 中,它通常表现为 HttpRequestException 以及其内部的 AuthenticationException。要确认其中类似“远程证书无效”的消息。
正确的处理是分发 CA,而不是关闭验证
企业内部 CA 证书通常分发到本地计算机的“受信任的根证书颁发机构”。与用户存储如何取舍,请参见“Windows 证书存储实务指南”。
不要采用让 ServerCertificateCustomValidationCallback 始终返回 true 这类绕过手段。它会作为一个在外部网络中使用时也无法检测中间人攻击的漏洞长期留存。
使用证书固定的通信,要排除在检查之外
使用证书固定的通信,在代理换掉证书的那一刻就会失败。对于验证特定 Microsoft 证书的 Windows 组件等,没有规避办法,必须设置排除项。3
flowchart TB
accTitle: 区分分发 CA 与排除证书固定的通信
accDescr: TLS 检查引发证书错误时要确认对 CA 的信任,而使用证书固定的通信,换掉证书本身就是失败原因,因此要从检查中排除
failure["证书验证错误"] --> pinned{"是否固定了证书?"}
pinned -->|"是"| exclude["从检查中排除"]
pinned -->|"否"| store["确认使用的信任存储"]
store --> ca["确认企业内部 CA 的分发"]
图15:分发企业内部 CA 的处理,和避免证书被替换的处理是两回事。
对于发往 Microsoft 365 等 SaaS 的流量,Microsoft 同样建议将其排除在网络层的解密与检查之外。8 如果只有特定的云服务出现证书错误,就怀疑检查排除列表与证书固定的组合。
8. 排查步骤——用五步定位真凶
在实际排查中,按以下顺序确认前面讲过的机制。
| 步骤 | 要做的事 | 能确定什么 |
|---|---|---|
| ① 复现 | 用 curl.exe -v 或 Invoke-WebRequest 访问出问题的 URL(尽量在同一台计算机、同一个账户下) |
是应用自身的问题,还是环境的问题 |
| ② 采集配置 | 采集 netsh winhttp show proxy、按用户配置、环境变量这三套体系 |
哪套体系里放着什么 |
| ③ 确定账户 | 确定目标应用的运行账户(是服务、任务计划程序,还是其他用户) | 它在用哪套配置、哪份凭据运行 |
| ④ 错误分类 | 区分 407 / 403 / 名称解析失败 / 超时 / 证书错误 | 区分代理认证、策略拒绝、路径与 TLS 检查 |
| ⑤ 代理日志 | 在代理服务器的访问日志中确认对应时刻 | 通信究竟有没有到达代理,以及被认证成了谁 |
① 复现:即使 URL 相同,也要对齐工具的配置体系
尽量从同一台计算机、同一个账户访问出问题的 URL。也要注意所用工具的差异。
| 工具 | 排查时要留意的点 |
|---|---|
Windows 自带的 curl.exe |
可以用 -x http://proxy:8080 显式指定代理。TLS 验证通常使用操作系统的证书存储(Schannel) |
Windows PowerShell 5.1 的 Invoke-WebRequest |
走 .NET Framework 一侧的解析。默认是 Internet 选项 |
PowerShell 7 的 Invoke-WebRequest |
走 .NET 一侧的解析。优先使用环境变量 |
“curl 能通,应用不通”是配置体系错位的线索。不能仅凭另一个工具成功,就断定应用也走了同一条路径。
② 采集配置:把三套体系一并记录下来
在 PowerShell 中可以这样采集。按用户配置所在的 HKCU,是执行这条命令的那个账户的。
# ① 按用户(WinINET)配置 —— 注意读的是运行账户的 HKCU
Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' |
Select-Object ProxyEnable, ProxyServer, ProxyOverride, AutoConfigURL
# ② 计算机(WinHTTP)配置
netsh winhttp show proxy
# ③ 环境变量
Get-ChildItem env: | Where-Object Name -match 'proxy'
③ 确定账户:是服务就用同一个账户重新确认
确定目标是服务、任务计划程序,还是由其他用户运行。若是服务,就用同一个账户重做①的复现和②的采集。在管理员自己的会话里成功,并不能证明 LocalSystem 看到的情况。
flowchart TB
accTitle: 对齐运行账户后再复现
accDescr: 在管理员会话中采集到的结果不能证明服务看到的配置,因此要先确定目标的运行账户,再用该账户重新确认复现与配置采集
admin["在管理员会话中确认"] --> identify["确定目标的运行账户"]
identify --> same["用同一账户复现并采集"]
same --> compare["核对配置与凭据"]
图16:确认①②采集到的信息,是否就是从③确定的目标账户视角看到的信息。
④ 错误分类:把“连不上”拆开
遇到 407 就看第 6 章的代理认证,遇到证书错误就看第 7 章的 TLS 检查。遇到超时,则把“没能到达代理”作为首要候选,检查路径、名称解析和防火墙。403 的策略拒绝也要与认证和超时分开。
flowchart TB
accTitle: 按错误区分排查方向
accDescr: 把通信失败分成 407、403、超时或名称解析失败、证书错误,分别检查认证、策略拒绝、路径和 TLS 检查
error["通信失败"] --> auth["407 则查代理认证"]
error --> deny["403 则查策略拒绝"]
error --> route["超时或名称解析则查路径"]
error --> tls["证书错误则查 TLS"]
图17:把状态码和异常分开,就能缩小要检查的配置和日志范围。
原因不在代理而在 Windows 防火墙入站规则的情形,在“Windows 防火墙与业务应用”中讲解。
⑤ 代理日志:确认是否到达以及被认证成哪个账户
在对应时刻的访问日志里,确认通信是否到达了代理、被认证成了谁。如果没有任何痕迹,就按“通信没有送达代理”来处理,检查 PAC 的 DIRECT、绕过列表,以及忘了删除的环境变量。
必要时用抓包确认实际的目标地址。采集方法请参见“Windows 抓包实务 —— pktmon、netsh trace、Wireshark 的取舍”。
9. 设计建议——把应用做成“代理可配置”的应用
排查的难易程度取决于应用的配置项和日志。要交付到有企业内网代理的环境的应用,请准备好以下四点。
除了遵循默认值,也要能选择显式指定和直连
默认设为“遵循操作系统配置”,并在读不了 PAC、以服务运行、结构特殊等情况下,允许从配置文件指定代理 URL、绕过列表和“不使用代理”。实现点就是 5.3 节的 HttpClientHandler.Proxy 和 UseProxy。11
.NET(Core 及以后)的默认行为里,还有 5.2 节说明的环境变量优先。即使交给默认值处理,也要按那套规则去判断实际会使用哪份配置。
把发往企业内部的例外,写成能放进部署手册的形式
把发往 API、数据库、许可证服务器等企业内部目标的通信,究竟用 PAC 的 DIRECT、绕过列表还是 NO_PROXY 来排除,明确写下来。NO_PROXY 不支持通配符、以及前导点的含义,要配上示例说明。5
超时和重试也要以走代理为前提
面对代理停机或认证等待,一直等待冗长的默认超时会让界面和运维都卡住。把连接超时单独设得短一些,重试只限于幂等的请求。详情在“不要用 using 包裹 HttpClient”中讲解。
从实际配置好的处理程序把路径记入日志
如果只把 HttpClient.DefaultProxy 打进日志,就会漏掉处理程序上的显式指定和 UseProxy = false,记下与实际不符的路径。重要的是从创建 HttpClient 所用的那个处理程序中挑出实际生效的配置,并把目标的绕过判定也算进去。
using System.Net.Http;
// handler 与创建 HttpClient 时使用的是同一个实例
// UseProxy=false 时始终直连。有显式指定就用它,没有则使用 DefaultProxy
var effectiveProxy = handler.UseProxy
? handler.Proxy ?? HttpClient.DefaultProxy
: null;
var target = new Uri("https://api.example.com/v1/orders");
var route = effectiveProxy is null || effectiveProxy.IsBypassed(target)
? "DIRECT"
: effectiveProxy.GetProxy(target)?.ToString() ?? "DIRECT";
logger.LogInformation("HTTP 发送 {Target} 路径 {Route} 运行账户 {User}",
target, route, Environment.UserName);
flowchart TB
accTitle: 从通信客户端的配置记录路径
accDescr: 从创建 HttpClient 所用的处理程序中挑出实际生效的代理,做目标的绕过判定与代理解析,再把路径和运行账户记入日志
handler["与创建时相同的处理程序"] --> effective["体现 UseProxy 与显式指定"]
effective --> target["目标的绕过判定与解析"]
target --> log["记录路径与账户"]
图18:不要只看默认代理,要基于客户端自身的配置来记录路径。
在启动时记录下主要目标的路径和运行账户,第 8 章的①~③就更容易追查。当有人说“浏览器明明能连上”时,应用能说清自己的配置和路径,才是经得起排查的设计。
10. 小结
排查 Windows 的代理问题时,先对齐配置体系、运行账户、目标 URL。
WinINET 的按用户配置、WinHTTP 的计算机配置和环境变量是彼此独立的体系。改成服务后,能看到的配置和凭据都会变;.NET Framework 与 .NET(Core 及以后)的默认顺序也不同。只靠 netsh winhttp set proxy,处理不了 PAC、自动检测和认证。
PAC 会按 URL 返回代理或 DIRECT,WPAD 则需要网络侧事先搭好机制。路径确定之后,还要另行确认 407 的代理认证和 TLS 检查带来的证书验证。不要关闭证书验证,而应分发 CA,并把使用证书固定的通信排除在外。
排查的顺序是:复现→采集三套体系的配置→确定运行账户→错误分类→代理日志。应用一侧要准备好代理的显式指定与直连、例外配置、超时和路径日志。
下次有人来问“只有业务应用连不上”时,请先这样确认。
这个应用以谁的账户在运行,读的是三套体系中的哪一套代理配置?
这一问就是排查的入口。
相关文章
- 不要用 using 包裹 HttpClient —— C# 业务应用的 HTTP 通信实务(创建模式、超时、重试)
- Windows 抓包实务 —— pktmon、netsh trace、Wireshark 的取舍
- Windows 防火墙与业务应用 —— 入站规则要通过安装程序注册
- Windows 服务的做法与运维 —— 从与任务计划程序的取舍到把 BackgroundService 做成服务
- 图解 NTLM 与 Kerberos —— 为什么认证会“回退”到 NTLM
- Windows 证书存储实务指南 —— 放进用户存储还是计算机存储
相关咨询领域
小村软件有限公司承接这样的咨询:“开发机上能跑,到客户网络就无法通信”“做成服务后连不上外部 API”这类在企业内网代理、需要认证的代理、TLS 检查环境下的 Windows 应用通信故障排查,以及以代理环境为前提的业务应用通信设计(配置项、超时、日志设计)。从梳理现象的复现步骤和日志采集方法开始也没问题。
参考链接
-
Microsoft Learn, WinINet vs. WinHTTP. 关于“只要不是服务、也不是需要模拟或会话隔离的进程就使用 WinINET”这一选型指引,以及凭据缓存、凭据提示、服务支持、模拟、会话隔离等功能对比表。 ↩ ↩2 ↩3
-
Microsoft Learn, About WinHTTP. 关于 WinHTTP 是面向服务与服务器端用途设计的 HTTP 栈,支持以服务账户运行和模拟,同时不共享浏览器的 Cookie、缓存、凭据和用户的 Internet 选项。 ↩ ↩2
-
Microsoft Learn, Using a proxy with Delivery Optimization. 关于 netsh winhttp set proxy 是不支持自动检测、PAC URL 和代理认证的静态配置,为没有已登录用户的场景准备的按设备代理配置(NetworkProxy CSP、“按计算机进行代理设置”策略),以及使用证书固定的通信会在 TLS 检查下失败、需要设置排除项。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, WinHttpGetProxyForUrl function. 关于它作为 WPAD 协议的实现,因为 PAC 文件对不同 URL 可能返回不同的代理,所以需要按 URL 逐个调用,以及它同时支持显式指定 PAC URL 和从网络自动检测。 ↩ ↩2
-
Microsoft Learn, HttpClient.DefaultProxy Property. 关于在 Windows 上先读 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY、NO_PROXY 环境变量,未定义时再读用户的代理配置;在 Linux 上没有环境变量时以不使用代理初始化;NO_PROXY 不支持通配符、而是用前导点做子域匹配;以及可以在代理 URL 中包含用户名和密码。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Configuring Internet Applications. 关于在 .NET Framework 中 defaultProxy 元素定义默认代理、没有 Proxy 属性的 HttpWebRequest 会使用默认代理,以及系统的 Internet 设置与配置文件的设置会被组合、且以配置文件一侧优先。 ↩ ↩2 ↩3
-
Microsoft Learn, Authentication in WinHTTP. 关于需要代理认证时会返回状态码 407 和 Proxy-Authenticate 标头(服务器认证则是 401 与 WWW-Authenticate)、Basic 认证与 Kerberos 等质询/响应方案的差异,以及质询/响应方案中用户名和密码不会在网络上传输。 ↩ ↩2 ↩3
-
Microsoft Learn, Understanding implications when using network intermediation to decrypt or manipulate Microsoft 365 traffic at the network layer. 关于 TLS 检查(SSL 解密)是在代理或防火墙上解密、检查并重新加密 TLS 的结构,可能导致以端到端 TLS 为前提的服务功能异常或性能下降,以及建议把发往 Microsoft 365 的流量排除在网络层的解密与检查之外。 ↩ ↩2 ↩3
-
Microsoft Learn, netsh winhttp. 关于 netsh winhttp show/set/import/reset 的语法、set proxy 的 proxy-server 与 bypass-list、import proxy source=ie,以及用 set advproxy 以 JSON 形式(Proxy、ProxyBypass、AutoconfigUrl、AutoDetect)进行详细代理配置。 ↩ ↩2
-
Microsoft Learn, WinHTTP AutoProxy Support. 关于 PAC 脚本包含 FindProxyForURL(url, host) 函数并为每个请求计算代理列表、可以直接连接时用特定返回值表示,以及传统 AutoProxy API 没有把自动代理集成进 HTTP 栈、需要应用自己调用 WinHttpGetProxyForUrl。 ↩ ↩2
-
Microsoft Learn, Make HTTP requests with the HttpClient class. 关于 HttpClient.DefaultProxy 与 HttpClientHandler.Proxy 这两种配置方式、Proxy 指定优先于配置文件和本地计算机的配置、WPAD 通过 DNS 的 wpad 名称或 DHCP 获取 PAC 文件(如 wpad.dat)的常见结构,以及依据扁平名称、环回地址、域后缀匹配所做的本地绕过判定。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, WinHttpOpen function. 关于 dwAccessType 各取值的含义:WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY(Windows 8.1 及以后)会使用系统/用户的代理配置自动决定代理,并自动处理故障转移和认证;WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 在 8.1 及以后已不推荐。 ↩ ↩2
-
Microsoft Learn, defaultProxy element (network settings). 关于 system.net/defaultProxy 元素的 enabled 与 useDefaultCredentials 属性、proxy 与 bypasslist 与 module 子元素、元素为空时使用系统的代理配置,以及迁移到 .NET 6 及以后时改用 HttpClient.DefaultProxy 配置。 ↩ ↩2
-
Microsoft Learn, HttpClientHandler.DefaultProxyCredentials Property. 关于该属性用于在 UseProxy 为 true 且 Proxy 为 null、使用系统默认代理时,设置向该默认代理进行认证所用的凭据。 ↩
-
Microsoft Learn, WebProxy.Credentials Property. 关于 Credentials 属性是作为对 HTTP 407 的应答而发给代理的凭据,以及在多数客户端场景中推荐把 UseDefaultCredentials 设为 true 以使用已登录用户的默认凭据。 ↩
-
Microsoft Learn, Work with existing on-premises proxy servers. 关于出站 HTTPS 通信通过向代理发送 CONNECT 请求来建立,成功时返回 HTTP 200,而 407(要求认证)或 502 等响应表示代理没有放行该通信,应当与代理侧团队一起继续排查。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
不要用 using 包裹 HttpClient —— C# 业务应用的 HTTP 通信实务(创建模式・超时设计・重试)
C# 的 HttpClient 如果每次都用 using 创建会导致套接字耗尽,改成 static 又无法跟上 DNS 变更——本文从 Windows 业务应用的实务角度,整理这两大问题的成因,以及通过 PooledConnectionLifetime、IHttpClien...
Windows 名称解析的顺序 ── hosts、DNS 缓存、LLMNR/mDNS 与 DoH
「解析不了名称」「只有部分电脑连不上」,结果取决于是 hosts、DNS 缓存、DNS 服务器还是 LLMNR/mDNS 给出的答案。本文从机制上梳理 Windows 名称解析的顺序与 DoH 改变了什么,并讲解按层排查的步骤。
Time Travel Debugging ── 把长期运行中不复现的缺陷“录下来”再倒回去
一个月才出一次的缺陷,崩溃转储只拍得到结果。本文讲解如何用 WinDbg 的 Time Travel Debugging(TTD) 录制执行并倒回,涵盖 TTD.exe 的录制设计、环形缓冲区、TTD.Calls 查询,以及与转储的分工。
参数为什么会坏掉 ── Windows 命令行参数的规则
在 Windows 上并不存在参数的数组,传给 CreateProcess 的是一条字符串,切分由接收方完成。本文讲解 CommandLineToArgvW・CRT・.NET 的切分规则,以及 .NET 的 ArgumentList 与 C++ 中正确的组装方法。
Windows 打印驱动程序停止提供 ── 业务应用的报表与标签打印如何应对
Microsoft 正在分阶段推进 v3/v4 打印驱动程序的停止提供,从 2026 年 7 月起会优先选择 IPP 类驱动程序。本文梳理 Windows protected print mode 下会消失什么,并用判断表整理业务应用程序的报表、标签打印中依赖点的盘点方法与...
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
故障调查 & 长期运行故障
整理间歇性故障、通信诊断、长期运行崩溃、失败路径测试基础的主题页面。
常见问题
汇总了咨询这一主题时常见的问题。
- 浏览器能连通,为什么只有业务应用过不了企业内网代理?
- 浏览器读取的是 WinINET 的按用户代理配置,但业务应用未必读同一份。以 Windows 服务运行的应用,或以其他账户运行的应用,引用的是该账户能看到的配置、WinHTTP 的计算机配置或环境变量。请先确定运行账户,再用 netsh winhttp show proxy 和用户配置两边检查该账户能看到的代理配置。如果能在同一台计算机、同一个账户下用 curl.exe 等工具复现,就可以判断这不是应用自身的问题,而是配置体系之间的错位。
- 已经设置了 netsh winhttp set proxy,为什么应用的通信没有变化?
- netsh winhttp 设置的是 WinHTTP 的计算机默认值,不影响读取 WinINET 的浏览器和交互式应用,也不影响优先读取环境变量的 .NET(Core 及以后)HttpClient。而且 netsh winhttp set proxy 是静态配置,不处理 PAC 自动配置、自动检测和代理认证。需要先确认目标应用用的是哪套 HTTP 栈、从哪套配置体系解析代理。
- .NET 应用读取哪套代理配置?
- .NET Framework 默认使用当前运行账户的 Internet 选项(相当于 WinINET)配置,可以用 app.config 的 system.net/defaultProxy 元素覆盖。.NET(Core 及以后)的 HttpClient 先读 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 等环境变量,未定义时才回退到 Windows 的用户代理配置。两种情况下,只要用 HttpClientHandler.Proxy 显式指定,它的优先级都最高。也就是说,Framework 与 Core 及以后的默认解析顺序不同,迁移时需要重新确认代理行为。
- 返回 407 Proxy Authentication Required 时应该检查什么?
- 407 是代理本身在要求认证的信号,与目标服务器的认证错误(401)是两回事。先用 Proxy-Authenticate 标头确认代理要求的认证方案(Negotiate、NTLM、Basic),在 .NET 中则用 HttpClientHandler.DefaultProxyCredentials 或 WebProxy.UseDefaultCredentials 传入凭据。以服务账户运行的应用里,“默认凭据”会变成该服务账户的凭据,因此典型现象是交互式用户能通过、一改成服务就返回 407。也请一并在代理侧的日志中确认它被认证成了谁。
- TLS 检查型代理导致证书错误,可以关闭证书验证吗?
- 不建议关闭。TLS 检查型代理会先把通信解密,再把用自己的 CA 重新签名的证书呈现给客户端,如果该 CA 证书不在受信任的根中,就会出现验证错误。正确的处理是把企业内部 CA 证书分发到 Windows 的证书存储(通常是本地计算机的“受信任的根证书颁发机构”)。在代码里关闭验证,会导致应用在外部网络中使用时无法检测中间人攻击,作为漏洞长期留存。