{ "cells": [ { "cell_type": "markdown", "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" } }, "source": [ "# HttpClient 使用代理功能" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "实际开发中,HttpClient 通过代理访问目标服务器是常见的需求。\n", "\n", "本文将全面介绍如何在 .NET 中配置 HttpClient 使用代理(Proxy)功能,包括基础使用方式、代码示例、以及与依赖注入结合的最佳实践。\n", "\n", "> 注意:运行代码之前,先开启`Fiddler Classic`及其代理功能,充当代理服务器。" ] }, { "cell_type": "markdown", "metadata": { "dotnet_interactive": { "language": "csharp" }, "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "source": [ "## 初始化" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 开启`Fiddler Classic`及其代理功能,充当代理服务器" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![代理服务器](./Assets/HttpClient-代理服务器设置.jpg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 导入初始终化笔记文件,并且执行一次" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "data": { "text/markdown": [ "## 初始化\n", "这是全局共用文件,包括Nuget包引用、全局类库引用、全局文件引用、全局命名空间引用、全局变量、全局方法、全局类定义等功能。\n", "\n", "在业务笔记中引用,执行其它单元格之前先执行一次。" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
Installed Packages
" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n", "配置文件根目录:c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.Core\n", "启动WebApi项目...\n", "程序[c:\\Users\\ruyu\\Desktop\\HttpClientStudy\\Docs\\Publish\\HttpClientStudy.WebApp\\HttpClientStudy.WebApp.exe]已在新的命令行窗口执行。如果未出现新命令行窗口,可能是程序错误造成窗口闪现!\n", "已启动WebApi项目,保持窗口打开状态!\n", "初始化完成!\n" ] } ], "source": [ "#!import \"./Ini.ipynb\"\n", "\n", "//共享变量\n", "var fiddlerProxyAddress = \"127.0.0.1:8888\";\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🧩 什么是代理?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "代理(Proxy)是一种中间服务器,用于转发客户端请求到目标服务器。它常用于以下目的:\n", "\n", "- 访问受限资源:企业内网中,通过代理服务器访问外部资源;\n", "- 提高安全性和隐私保护:代理可隐藏真实 IP 地址,保护目标服务器的隐私和数据安全;\n", "- 提高性能:代理可使用请求缓存和负载均衡等,减少目标服务器的压力,提高性能;\n", "- 方便调试、测试\n", " - 代理服务器可记录请求和响应信息,方便调试和测试;\n", " - `Fiddler Classic`等软件,默认是抓不到 HttpClient 的请求的,需要将其设置为代理服务器,才能抓取到 HttpClient 的请求;\n", "\n", "在 .NET HttpClient 中,可以通过多种方式来设置代理服务器。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🛠️ 设置 HttpClient 代理" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ✅ 基本方式使用(无用户名密码)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "响应状态:OK\r\n" ] } ], "source": [ "{ \n", " // 设置 SocketsHttpHandler 使用代理\n", " var handler = new SocketsHttpHandler()\n", " {\n", " UseProxy = true,\n", " Proxy = new WebProxy(fiddlerProxyAddress),\n", " };\n", "\n", " // 创建 HttpClient,并且请求\n", " using (var client = new HttpClient(handler))\n", " {\n", " var response = await client.GetAsync(\"https://www.baidu.com\");\n", " \n", " Console.WriteLine($\"响应状态:{response.StatusCode}\");\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "执行上面的单元格,应该在fiddler classic 中,抓到请求包:可以查看和管理详细信息.\n", "![Fillder 抓包](./Assets/HttpClient-代理-抓包.jpg)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ✅ 带用户名和密码的代理" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "响应状态:OK\r\n" ] } ], "source": [ "{ \n", " // 设置 SocketsHttpHandler 使用代理\n", " var handler = new SocketsHttpHandler()\n", " {\n", " UseProxy = true,\n", " Proxy = new WebProxy(fiddlerProxyAddress)\n", " {\n", " //正式项目:机密数据一定要脱敏处理或者使用环境变量、机密管理器等手段\n", " //因为Fiddler代理服务器,没有用户凭据要求,所以此处随意填写的。需要的话,真实填写正确的用户凭据。\n", " Credentials = new NetworkCredential(\"username\", \"password\"),\n", " },\n", " };\n", "\n", " // 创建 HttpClient,并且请求\n", " using (var client = new HttpClient(handler))\n", " {\n", " var response = await client.GetAsync(\"https://www.baidu.com\");\n", " Console.WriteLine($\"响应状态:{response.StatusCode}\");\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "source": [ "### 📦 在IoC和工厂中使用 Proxy [推荐方式]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "在 ASP.NET Core 或基于 IServiceCollection 的项目中,可以通过 UseSocketsHttpHandler 扩展方法,统一管理代理服务器配置。\n", "\n", "还可以根据客户端的命名不同,进行不同的代理服务器配置!" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "正常请求,响应内容为: Pong\r\n" ] } ], "source": [ "//IoC或工厂中设置代理\n", "{\n", " //IoC\n", " var services = new ServiceCollection();\n", "\n", " //默认命名客户端\n", " services\n", " .AddHttpClient(string.Empty)\n", " .ConfigureHttpClient(client => \n", " {\n", " client.BaseAddress = new Uri(webApiBaseUrl);\n", " client.Timeout = TimeSpan.FromSeconds(10);\n", " })\n", "\n", " //配置代理服务器\n", " .UseSocketsHttpHandler(handlerBuilder =>\n", " {\n", " handlerBuilder.Configure((handler,s) => \n", " {\n", " handler.Proxy = new WebProxy(fiddlerProxyAddress);\n", " }); \n", " });\n", "\n", " //发送请求\n", " var factory = services.BuildServiceProvider().GetRequiredService();\n", "\n", " //正常请求\n", " var defaultClient = factory.CreateClient();\n", " var defaultContent = await defaultClient.GetStringAsync(\"api/hello/ping\");\n", " Console.WriteLine($\"正常请求,响应内容为: {defaultContent}\");\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 🔄 动态切换代理服务器\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "要根据请求的不同(请求地址、请求方法、请求头、请求参数等),动态选择使用一同的代理服务器,可以使用Pipeline中间件,来管理。" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LoggerDelegatingHandler -> SendAsync -> Before\n", "ProxySelectorLastHandler -> SendAsync -> Before\n", "ProxySelectorLastHandler -> SendAsync -> After\n", "LoggerDelegatingHandler -> SendAsync -> After\n", "OK\n", "---------------------------------------------------\n", "ProxySelectorLastHandler -> SendAsync -> Before\n", "ProxySelectorLastHandler -> SendAsync -> After\n", "OK\n" ] } ], "source": [ "///\n", "/// 代理服务选择器中间件\n", "/// 注意:此中间件会短路,必须设置为最后一个中间件\n", "///\n", "public class ProxySelectorLastHandler : DelegatingHandler\n", "{\n", " /// \n", " /// 拦截请求,并动态设置代理\n", " /// 注意:会短路其它中间件,要放最后\n", " /// \n", " protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct)\n", " {\n", " Console.WriteLine(\"ProxySelectorLastHandler -> SendAsync -> Before\");\n", " //动态选择示例\n", " var proxy = request.RequestUri.Host switch\n", " {\n", " string url when url.Contains(\"baidu\") => new WebProxy(\"127.0.0.1:8888\"),\n", " string url when url.Contains(\"qq\") => new WebProxy(\"127.0.0.1:8888\"),\n", " _ => null\n", " };\n", "\n", " InnerHandler = new SocketsHttpHandler\n", " {\n", " Proxy = proxy,\n", " UseProxy = proxy != null\n", " };\n", "\n", "\n", " //请求\n", " HttpResponseMessage response = await base.SendAsync(request, ct);\n", "\n", " Console.WriteLine(\"ProxySelectorLastHandler -> SendAsync -> After\");\n", "\n", " return response;\n", " }\n", "}\n", "\n", "//日志中间件(管道类)\n", "public class LoggerDelegatingHandler : DelegatingHandler\n", "{\n", " protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)\n", " {\n", " Console.WriteLine(\"LoggerDelegatingHandler -> Send -> Before\");\n", "\n", " HttpResponseMessage response = base.Send(request, cancellationToken);\n", "\n", " Console.WriteLine(\"LoggerDelegatingHandler -> Send -> After\");\n", "\n", " return response;\n", " }\n", "\n", " protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n", " {\n", " Console.WriteLine(\"LoggerDelegatingHandler -> SendAsync -> Before\");\n", "\n", " HttpResponseMessage response = await base.SendAsync(request, cancellationToken);\n", "\n", " Console.WriteLine(\"LoggerDelegatingHandler -> SendAsync -> After\");\n", "\n", " return response;\n", " }\n", "}\n", "\n", "//使用:ProxySelectorLastHandler必须设置为最后一个中间件\n", "{\n", " var handlerLink = new LoggerDelegatingHandler()\n", " {\n", " InnerHandler = new ProxySelectorLastHandler(),\n", " };\n", "\n", " var myClient = new HttpClient(handlerLink);\n", " var response = await myClient.GetAsync(\"https://www.qq.com\");\n", "\n", " Console.WriteLine(response.StatusCode);\n", " Console.WriteLine(\"---------------------------------------------------\");\n", "}\n", "\n", "\n", "// ProxySelectorLastHandler 不是最后一个的话,其它中间件无效(被短路)\n", "{\n", " var handlerLink = new ProxySelectorLastHandler()\n", " {\n", " InnerHandler = new LoggerDelegatingHandler (),\n", " };\n", "\n", " var myClient = new HttpClient(handlerLink);\n", " var response = await myClient.GetAsync(\"https://www.qq.com\");\n", "\n", " Console.WriteLine(response.StatusCode);\n", "}\n", "\n", "//注意看输出:后面的没有日志中间件的任何输出" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🔐 HTTPS 代理信任问题" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "当使用 HTTPS 代理时,可能会遇到 SSL/TLS 证书不被信任的问题,尤其是在测试环境中使用自签名证书。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ✅解决方法一:忽略证书验证(⚠️ 注意:仅用于开发环境)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "OK\r\n" ] } ], "source": [ "var handler = new HttpClientHandler\n", "{\n", " Proxy = new WebProxy(fiddlerProxyAddress),\n", " UseProxy = true,\n", " ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true\n", "};\n", "\n", "using (var client = new HttpClient(handler))\n", "{ \n", " var response = await client.GetAsync(\"https://www.baidu.com\");\n", " Console.WriteLine(response.StatusCode);\n", "};\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ✅ 解决方法二:手动添加根证书信任" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "polyglot_notebook": { "kernelName": "csharp" }, "vscode": { "languageId": "polyglot-notebook" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "OK\r\n" ] } ], "source": [ "using System.Security;\n", "using System.Security.Cryptography;\n", "using System.Security.Cryptography.X509Certificates;\n", "\n", "var file = Environment.CurrentDirectory + \"\\\\Assets\\\\FiddlerRoot.cer\";\n", "//Console.WriteLine(file);\n", "var rootCert = System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadCertificateFromFile(file);\n", "\n", "var handler = new HttpClientHandler\n", "{\n", " Proxy = new WebProxy(fiddlerProxyAddress),\n", " UseProxy = true,\n", " ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>\n", " {\n", " //return true;\n", " return chain.ChainElements.Any(x => x.Certificate.Thumbprint == cert.Thumbprint); // 验证证书链包含指定根证书\n", " }\n", "};\n", "\n", "using (var client = new HttpClient(handler))\n", "{ \n", " var response = await client.GetAsync(\"https://www.baidu.com\");\n", " Console.WriteLine(response.StatusCode);\n", "};" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 📌 总结" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "通过本文,你应该掌握了以下内容:\n", "\n", "+ 如何在 HttpClient 中直接设置代理\n", "+ 如何在依赖注入系统中配置全局代理\n", "+ 如何通过环境变量设置代理\n", "+ 如何验证代理是否生效\n", "+ 实际使用中的注意事项\n", "+ 动态选择及证书信任" ] } ], "metadata": { "kernelspec": { "display_name": ".NET (C#)", "language": "C#", "name": ".net-csharp" }, "language_info": { "name": "python" }, "polyglot_notebook": { "kernelInfo": { "defaultKernelName": "csharp", "items": [ { "aliases": [], "name": "csharp" } ] } } }, "nbformat": 4, "nbformat_minor": 2 }