192 lines
7.3 KiB
C#
192 lines
7.3 KiB
C#
using Atomx.Common.Models;
|
|
using Atomx.Utils.Json;
|
|
using Microsoft.AspNetCore.Http;
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace Atomx.Admin.Client.Services
|
|
{
|
|
public class HttpService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly IHttpContextAccessor? _httpContextAccessor;
|
|
private readonly ILogger<HttpService> _logger;
|
|
|
|
public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null, ILogger<HttpService>? logger = null)
|
|
{
|
|
_httpClient = httpClient;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<HttpService>.Instance;
|
|
}
|
|
|
|
public async Task<T> Get<T>(string url)
|
|
{
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
AttachCookieIfServer(request);
|
|
var response = await _httpClient.SendAsync(request);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
return content.FromJson<T>();
|
|
}
|
|
else
|
|
{
|
|
await LogNonSuccessAsync(url, response);
|
|
ThrowForStatus(response.StatusCode, url);
|
|
throw new Exception($"Error: {response.StatusCode}");
|
|
}
|
|
}
|
|
|
|
public async Task<T> Post<T>(string url, object data)
|
|
{
|
|
var json = data.ToJson();
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
|
{
|
|
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
|
};
|
|
AttachCookieIfServer(request);
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
|
return responseContent.FromJson<T>();
|
|
}
|
|
else
|
|
{
|
|
await LogNonSuccessAsync(url, response);
|
|
ThrowForStatus(response.StatusCode, url);
|
|
throw new Exception($"Error: {response.StatusCode}");
|
|
}
|
|
}
|
|
|
|
public async Task<ApiResult<PagingList<T>>> GetPagingList<T>(string url, object data, int page, int size = 20)
|
|
{
|
|
try
|
|
{
|
|
if (page < 1)
|
|
{
|
|
page = 1;
|
|
}
|
|
url = $"{url}?page={page}&size={size}";
|
|
var json = data.ToJson();
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
|
{
|
|
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
|
//Content = JsonContent.Create(data)
|
|
};
|
|
AttachCookieIfServer(request);
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
return content.FromJson<ApiResult<PagingList<T>>>();
|
|
}
|
|
else
|
|
{
|
|
await LogNonSuccessAsync(url, response);
|
|
// 明确在 401/403 场景抛出授权异常以便上层 UI/组件做特殊处理
|
|
ThrowForStatus(response.StatusCode, url);
|
|
throw new Exception($"Error: {response.StatusCode}");
|
|
}
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "HttpRequestException while calling {Url}", url);
|
|
Console.Error.WriteLine($"[{DateTime.UtcNow:o}] HttpRequestException Url:{url} Error:{ex.Message}");
|
|
throw new Exception($"api {url} service failure");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 如果在 Server 环境并且 IHttpContextAccessor 可用,则把浏览器请求的 Cookie 转发到后端请求中
|
|
/// </summary>
|
|
private void AttachCookieIfServer(HttpRequestMessage request)
|
|
{
|
|
try
|
|
{
|
|
if (!OperatingSystem.IsBrowser())
|
|
{
|
|
var ctx = _httpContextAccessor?.HttpContext;
|
|
if (ctx != null && ctx.Request.Headers.TryGetValue("Cookie", out var cookie) && !string.IsNullOrEmpty(cookie))
|
|
{
|
|
// 覆盖或添加 Cookie header
|
|
if (request.Headers.Contains("Cookie"))
|
|
{
|
|
request.Headers.Remove("Cookie");
|
|
}
|
|
request.Headers.Add("Cookie", (string)cookie);
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 忽略任何转发异常,保持健壮性
|
|
}
|
|
}
|
|
|
|
private async Task LogNonSuccessAsync(string url, HttpResponseMessage response)
|
|
{
|
|
try
|
|
{
|
|
var status = response.StatusCode;
|
|
var reason = response.ReasonPhrase;
|
|
|
|
string userId = "unknown";
|
|
string ip = "unknown";
|
|
|
|
var ctx = _httpContextAccessor?.HttpContext;
|
|
if (ctx != null)
|
|
{
|
|
userId = ctx.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
|
?? ctx.User?.FindFirst("sub")?.Value
|
|
?? "unknown";
|
|
|
|
if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff))
|
|
{
|
|
ip = xff.ToString().Split(',')[0].Trim();
|
|
}
|
|
else
|
|
{
|
|
ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
|
}
|
|
}
|
|
|
|
// 结构化日志记录
|
|
_logger.LogWarning("UserId:{UserId} Url:{Url} Ip:{Ip} HttpStatus:{StatusCode} Reason:{ReasonPhrase}", userId, url, ip, (int)status, reason);
|
|
|
|
// 控制台输出一份,便于本地/容器查看
|
|
var consoleMsg = new
|
|
{
|
|
Timestamp = DateTime.UtcNow.ToString("o"),
|
|
Level = "Warning",
|
|
UserId = userId,
|
|
Url = url,
|
|
Ip = ip,
|
|
Status = (int)status,
|
|
Reason = reason
|
|
};
|
|
Console.WriteLine(JsonSerializer.Serialize(consoleMsg));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 日志失败不能影响主流程
|
|
_logger.LogError(ex, "Failed to log non-success response for {Url}", url);
|
|
}
|
|
}
|
|
|
|
private void ThrowForStatus(HttpStatusCode statusCode, string url)
|
|
{
|
|
if (statusCode == HttpStatusCode.Unauthorized || statusCode == HttpStatusCode.Forbidden)
|
|
{
|
|
// 抛出明确的授权异常,便于上层按需处理(例如提示登陆、重定向或显示权限不足)
|
|
throw new UnauthorizedAccessException($"Error: {statusCode} when calling {url}");
|
|
}
|
|
}
|
|
}
|
|
}
|