PAYATHON 2026

C# Ajax Get Request 返回 401,Python 请求正常

支付小周

结论

401 Unauthorized 说明服务器没有识别到有效的登录会话,并不是 Ajax 请求格式有误。

Python 代码调用的是 session.get(...)。如果这个 session 之前完成过登录、访问过相关页面,或者导入过浏览器 Cookie,发送请求时就会自动带上认证 Cookie。C# 示例只添加了 X-Requested-With,没有复用已经登录的会话。因此,即使复制了 User-Agent、Referer 等请求头,服务器仍会返回 401。

X-Requested-With: XMLHttpRequest 只能表示请求通常由前端脚本发起,不能用来证明用户身份。

正确处理方式

在 C# 中,应长期复用同一个 HttpClient 和 CookieContainer:

  1. 按照服务支持的登录流程完成认证。
  2. 使用 CookieContainer 保存登录响应中的 Cookie。
  3. 使用同一个 HttpClient 访问账户页面和订单历史接口。
  4. 如果接口要求 CSRF Token,需要从登录响应、Cookie 或页面中取得 Token,并按接口要求发送。
  5. 使用开发者工具对比 Python 请求和浏览器请求,重点检查 Cookie、重定向及认证相关请求头,不要只比较普通 Header。

C# 示例

下面的代码演示如何保存和复用 Cookie。登录部分需要根据服务当前支持的认证流程实现,仅补充请求头无法代替登录。

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

public static class EpicOrderHistoryClient
{
    public static async Task<string> GetOrderHistoryAsync()
    {
        var cookies = new CookieContainer();

        using var handler = new HttpClientHandler
        {
            CookieContainer = cookies,
            UseCookies = true,
            AllowAutoRedirect = true,
            AutomaticDecompression =
                DecompressionMethods.GZip |
                DecompressionMethods.Deflate |
                DecompressionMethods.Brotli
        };

        using var client = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(10)
        };

        client.DefaultRequestHeaders.UserAgent.ParseAdd(
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
            "AppleWebKit/537.36 (KHTML, like Gecko) " +
            "Chrome/123.0.0.0 Safari/537.36");

        client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));

        // 必须先通过服务支持的方式完成登录,并继续使用这个 client。
        await AuthenticateAsync(client);

        using var request = new HttpRequestMessage(
            HttpMethod.Get,
            "https://www.epicgames.com/payment/ajaxGetOrderHistory");

        request.Headers.TryAddWithoutValidation(
            "X-Requested-With",
            "XMLHttpRequest");

        request.Headers.Referrer = new Uri(
            "https://www.epicgames.com/account/payment/history");

        using HttpResponseMessage response =
            await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

        string body = await response.Content.ReadAsStringAsync();

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            throw new InvalidOperationException(
                "服务器返回 401:登录会话无效、已过期,或认证 Cookie/Token 未随请求发送。");
        }

        response.EnsureSuccessStatusCode();
        return body;
    }

    private static Task AuthenticateAsync(HttpClient client)
    {
        // 按服务当前支持的认证流程实现。
        // 登录请求和后续接口请求必须使用同一个 HttpClient/CookieContainer。
        throw new NotImplementedException();
    }
}

如果已经通过合规方式取得当前会话的 Cookie,也可以将其加入 CookieContainer。Cookie 的名称和值应以实际登录响应为准:

var cookies = new CookieContainer();

cookies.Add(
    new Uri("https://www.epicgames.com"),
    new Cookie("actual_cookie_name", "actual_cookie_value")
    {
        Secure = true,
        HttpOnly = true,
        Path = "/"
    });

不要将账号 Cookie 写死在源码中,也不要把它记录到日志或提交到公开仓库。Cookie 通常会过期,还可能与设备、地区、CSRF Token 或其他会话状态绑定,所以只复制一个 Cookie 未必能完成认证。

为什么复制 Python Headers 没有效果

Python 请求中真正起作用的很可能不是这些显式设置的 Header,而是 session 内部保存的状态,例如:

Cookie: ...
Authorization: ...
X-CSRF-Token: ...

接口具体采用哪种认证机制,取决于当前实现。常见请求头的作用如下:

  • User-Agent:描述客户端,不能证明用户已经登录。
  • Referer:表示请求来源,不能代替身份认证。
  • X-Requested-With:常用于识别 Ajax 请求,不属于身份凭据。
  • Cache-Control、Pragma:用于控制缓存,与登录权限无关。
  • Host:HttpClient 会根据 URL 自动生成,通常不需要手动设置。
  • Connection、Keep-Alive:由 HTTP 客户端管理,不需要手动复制。

如何确认缺少了什么

可以在发送 Python 请求前检查 Session 中是否已经保存了 Cookie:

print(sesion.cookies.get_dict())

也可以打开浏览器开发者工具的 Network 面板,找到能够成功返回的请求,重点检查以下内容:

  • 请求是否携带认证 Cookie;
  • 发送请求前是否完成过登录或 Token 刷新;
  • 是否包含 CSRF Token;
  • 请求过程中是否发生了 302 或 303 重定向;
  • Python 是否通过代理使用了不同的网络出口;
  • C# 是否为每次请求重新创建客户端,导致 Cookie 丢失。

调试 C# 时,不要一开始就调用 EnsureSuccessStatusCode()。先记录状态码、响应内容、响应头和最终请求地址。服务器有时会在响应体或 WWW-Authenticate 中说明更具体的原因。

注意事项

该地址属于账户支付历史接口,通常需要用户身份认证。接口路径、认证方式和防自动化策略可能随时变化。如果服务提供正式 API,应优先使用正式 API 和受支持的授权流程。

不要伪造、窃取或绕过他人的会话凭据。即使操作自己的账户,也应遵守服务条款,并妥善保护登录 Cookie、访问令牌和支付历史数据。

备注:内容仅供参考。