一、定义接口
为什么要定义接口而不直接使用静态类,因为接口可以注入缓存对象,这样就能从缓存中读取指定的请求头
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Test.Common
{
/// <summary>
/// HTTP请求接口
/// </summary>
public interface IHttpClientUtils: IApplicationService
{
/// <summary>
/// 发送HTTP请求,注意Get请求第4个参数才是缓存key,因此第3个参数请设置为null
/// </summary>
/// <param name="method">HttpMethod.Get、HttpMethod.Post、HttpMethod.Put、HttpMethod.Delete</param>
/// <param name="url">完整地址</param>
/// <param name="model">请求体对象</param>
/// <param name="cacheKey">鉴权请求头缓存key</param>
/// <param name="headers">请求头</param>
/// <returns></returns>
Task<T> SendAsync<T>(HttpMethod method, string url, object model = null, string cacheKey = "", Dictionary<string, string> headers = null);
/// <summary>
/// 发送HTTP表单请求
/// </summary>
/// <param name="method">HttpMethod.Post、HttpMethod.Put</param>
/// <param name="url">完整地址</param>
/// <param name="model">请求体对象</param>
/// <param name="cacheKey">鉴权请求头缓存key</param>
/// <param name="headers">请求头</param>
/// <returns></returns>
Task<T> SendFormAsync<T>(HttpMethod method, string url, object model, string cacheKey = "", Dictionary<string, string> headers = null);
}
}
二、实现接口
1、支持https
2、支持从缓存读取鉴权请求头
3、支持对象携带文件上传
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RestSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using Volo.Abp.Json;
namespace Test.Common
{
/// <summary>
/// HTTP请求实现
/// </summary>
public class HttpClientUtils: ApplicationService, IHttpClientUtils
{
private readonly ILogger<HttpClientUtils> _logger;
private readonly IDistributedCache<Dictionary<string, string>> _cache;
public HttpClientUtils(
ILogger<HttpClientUtils> logger,
IDistributedCache<Dictionary<string, string>> cache)
{
_cache = cache;
_logger = logger;
}
/// <summary>
/// 发送HTTP请求
/// </summary>
/// <param name="method">HttpMethod.Get、HttpMethod.Post、HttpMethod.Put、HttpMethod.Delete</param>
/// <param name="url">完整地址</param>
/// <param name="model">请求体对象</param>
/// <param name="cacheKey">鉴权请求头缓存key</param>
/// <param name="headers">请求头</param>
/// <returns></returns>
public async Task<T> SendAsync<T>(HttpMethod method, string url, object model = null, string cacheKey = "", Dictionary<string, string> headers = null)
{
_logger.LogInformation($"SendAsync {method.Method} url = {url}, cacheKey = {cacheKey}, data = {JsonConvert.SerializeObject(model)}");
try
{
using (var client = new HttpClient())
{
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
url = $"http://{url}";
}
using (var request = new HttpRequestMessage(method, url))
{
if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
{
SetHttps();
}
var sc = new StreamContent(new MemoryStream());
if (model != null)
{
var jsonStr = JsonConvert.SerializeObject(model);
var bytes = Encoding.UTF8.GetBytes(jsonStr);
sc = new StreamContent(new MemoryStream(bytes));
}
HeaderHandler(request, sc.Headers, "application/json", headers, cacheKey);
request.Content = sc;
var rsp = await client.SendAsync(request);
return ResponseHandler<T>(rsp);
}
}
}
catch (Exception e)
{
_logger.LogError($"SendAsync {method.Method} 请求:{url}, 错误消息:{e.Message}, 请求参数:{JsonConvert.SerializeObject(model)}");
throw new Exception($"请求HTTP服务{new Uri(url).AbsolutePath}异常:{e.Message}");
}
}
/// <summary>
/// 发送HTTP表单请求
/// </summary>
/// <param name="method">HttpMethod.Post、HttpMethod.Put</param>
/// <param name="url">完整地址</param>
/// <param name="model">请求体对象</param>
/// <param name="cacheKey">鉴权请求头缓存key</param>
/// <param name="headers">请求头</param>
/// <returns></returns>
public async Task<T> SendFormAsync<T>(HttpMethod method, string url, object model, string cacheKey = "", Dictionary<string, string> headers = null)
{
_logger.LogInformation($"SendFormAsync {method.Method} url = {url}, cacheKey = {cacheKey}, data = {JsonConvert.SerializeObject(model)}");
try
{
var client = new RestClient();
var request = new RestRequest(url, Method.POST);
if (method == HttpMethod.Put)
{
request.Method = Method.PUT;
}
SetHeader(request, cacheKey, headers);
request.AlwaysMultipartFormData = true;
var props = model.GetType().GetProperties();
foreach (var item in props)
{
var k = item.Name;
var v = item.GetValue(model);
if (v == null)
{
_logger.LogInformation($"SendFormAsync {method.Method} url = {url} {k} value is null");
continue;
}
if (v.GetType().Name == "Dictionary`2") // 文件只支持字典类型
{
if (v is Dictionary<string, byte[]>)
{
var files = v as Dictionary<string, byte[]>;
foreach (var obj in files) // 多个文件只能一个个添加,不能集合到List中
{
string ext = Path.GetExtension(obj.Key);
ext = ext.RemovePreFix(".");
_logger.LogInformation($"SendFormAsync file key = {k}, name = {obj.Key}, ext = {ext}, size = {obj.Value.Length}");
request.AddFileBytes(k, obj.Value, obj.Key, $"image/{ext}");
}
}
}
else
{
request.AddParameter(k, v);
}
}
var response = await client.ExecuteAsync(request);
if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created)
{
_logger.LogError($"SendFormAsync {method.Method} {url} 响应失败: {response.Content}");
throw new Exception($"服务器响应失败:status = {response.StatusCode}");
}
_logger.LogInformation($"SendFormAsync {method.Method} {url} Response: {response.Content}");
return JsonConvert.DeserializeObject<T>(response.Content, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
}
catch (Exception e)
{
_logger.LogError($"SendFormAsync {method.Method} 请求:{url}, 错误消息:{e.Message}, 请求参数:{JsonConvert.SerializeObject(model)}");
throw new Exception($"请求HTTP服务{new Uri(url).AbsolutePath}异常:{e.Message}");
}
}
/// <summary>
/// 设置请求头
/// </summary>
/// <param name="request"></param>
/// <param name="cacheKey"></param>
private void SetHeader(RestRequest request, string cacheKey, Dictionary<string, string> headers)
{
// 鉴权请求头
var cacheHeaders = _cache.Get(cacheKey);
if (cacheHeaders != null)
{
foreach (var dic in cacheHeaders)
{
_logger.LogInformation($"SetHeader cache header: {dic.Key} = {dic.Value}");
if (dic.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase))
{
request.AddHeader("Authorization", dic.Value);
}
else
{
request.AddHeader(dic.Key, dic.Value);
}
}
}
// 默认请求头
if (headers != null && headers.Count > 0)
{
foreach (var dic in headers)
{
request.AddHeader(dic.Key, dic.Value);
}
}
}
/// <summary>
/// 处理请求头
/// </summary>
/// <param name="request">new HttpRequestMessage</param>
/// <param name="contentHeader">new MultipartFormDataContent</param>
/// <param name="contentType">multipart/form-data</param>
/// <param name="headers">Dictionary<string, string></param>
/// <param name="cacheKey">authorization</param>
private void HeaderHandler(HttpRequestMessage request, HttpContentHeaders contentHeader, string contentType, Dictionary<string, string> headers, string cacheKey)
{
// 添加默认请求头
if (headers != null && headers.ContainsKey("Content-Type"))
{
contentType = headers["Content-Type"];
}
if (string.IsNullOrEmpty(contentType))
{
contentType = "application/json"; // 默认application/json
}
_logger.LogInformation($"HeaderHandler {request.Method.Method} {request.RequestUri.AbsolutePath} default contentType header: {contentType}");
if (contentType.IndexOf("multipart/form-data") != -1)
{
contentHeader.Remove("Content-Type"); // MediaTypeHeaderValue不支持解析boundary,所以先删除再Add
contentHeader.TryAddWithoutValidation("Content-Type", contentType);
}
else
{
contentHeader.ContentType = new MediaTypeHeaderValue(contentType);
}
// 添加鉴权请求头
var cacheHeaders = _cache.Get(cacheKey);
if (cacheHeaders != null)
{
foreach (var dic in cacheHeaders)
{
_logger.LogInformation($"HeaderHandler {request.Method.Method} {request.RequestUri.AbsolutePath} cache header: {dic.Key} = {dic.Value}");
if (dic.Key.Equals("authorization", StringComparison.OrdinalIgnoreCase))
{
request.Headers.Authorization = new AuthenticationHeaderValue(dic.Value);
// request.SetBearerToken(dic.Value); // 这个方法里加了前缀Bearer,针对接口不同
}
else
{
contentHeader.Add(dic.Key, dic.Value); // cookie、token ...
}
}
}
// 添加参数请求头
if (headers != null && headers.Count > 0)
{
foreach (var dic in headers)
{
if (contentHeader.ContentType.MediaType == dic.Value)
{
continue;
}
contentHeader.Add(dic.Key, dic.Value);
}
}
}
/// <summary>
/// 处理HTTP响应
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="rsp"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private T ResponseHandler<T>(HttpResponseMessage rsp)
{
if (rsp.StatusCode != HttpStatusCode.OK && rsp.StatusCode != HttpStatusCode.Created)
{
_logger.LogError($"ResponseHandler {rsp.RequestMessage.Method.Method} {rsp.RequestMessage.RequestUri.AbsoluteUri} 响应失败: {rsp.Content.ReadAsStringAsync().Result}");
throw new Exception($"服务器响应失败:status = {rsp.StatusCode}");
}
if (rsp is T)
{
return (T)(object)rsp; // 返回HttpResponseMessage,用于获取响应头
}
else
{
var json = rsp.Content.ReadAsStringAsync().Result;
_logger.LogInformation($"ResponseHandler {rsp.RequestMessage.Method.Method} {rsp.RequestMessage.RequestUri.AbsoluteUri} 响应:{json}");
return JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
// return rsp.Content.ReadFromJsonAsync<T>().Result; // 会检查int null类型报错
}
}
/// <summary>
/// 设置HTTPS
/// </summary>
private void SetHttps()
{
// set remote certificate Validation auto pass
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(RemoteCertificateValidate);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
// FIX:修复不同.Net版对一些SecurityProtocolType枚举支持情况不一致导致编译失败等问题,这里统一使用数值
//ServicePointManager.SecurityProtocol = (SecurityProtocolType)48 | (SecurityProtocolType)3072 | (SecurityProtocolType)768 | (SecurityProtocolType)192;
}
/// <summary>
/// 远程证书验证
/// </summary>
/// <param name="sender"></param>
/// <param name="cert"></param>
/// <param name="chain"></param>
/// <param name="error"></param>
/// <returns>验证是否通过,始终通过</returns>
private bool RemoteCertificateValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
{
return true;
}
}
}
三、遇到问题
1、无法添加请求头content-type
var s = new HttpRequestMessage()
s.Headers.Add(“Content-Type”, “”)
报错:Misused header name, ‘Content-Type’. Make sure request headers are used with HttpRequestMessage,
var b = new HttpContentHeader()
b.Add(“Content-Type”, “”)
报错:Cannot add value because header ‘Content-Type’ does not support multiple values.
var b = new HttpContentHeader()
b.ContentType = new MediaTypeHeaderValue(“multipart/form-data; boundary=----8db27a5c21ea4f8”);
报错:The format of value ‘multipart/form-data; boundary=----8db27a5c21ea4f8’ is invalid
报错:‘multipart/form-data; ----8db27a507065123’ is invalid
报错:‘multipart/form-data; boundary=–8db27ca4cfafb57’ is invalid
1、解决方案:
请看方法:HeaderHandler