当请求包含冒号字符时,Twitter 单用户 OAuth 失败

Twitter Single User OAuth fails when request has a colon character

我正在尝试使用 Twitter 的单用户 OAuth 向 Twitter API 发出搜索。我发现了这个有用的库,让我可以这样做:https://gist.github.com/EelcoKoster/326aa7d1b1338806b058ddcc404622b6

(基于此处的原始作品:https://blog.dantup.com/2016/07/simplest-csharp-code-to-post-a-tweet-using-oauth

但是,虽然我可以使用此库发送查询并获得纯文本搜索的结果,但当我尝试查询某个位置时它会阻塞,例如:geocode:26.201461,-98.237987,0.25mi

经过一些测试,似乎是冒号 : 字符导致了问题。从查询中删除它会得到结果(因为 twitter 中没有这样的字符串,所以为空,但成功);将其添加回去会出现错误:无法对您进行身份验证。

我尝试使用参数的编码,但没有任何进展。

我相信这可以做到,因为 API 控制台似乎正在使用相同的授权:https://dev.twitter.com/rest/tools/console

而且我可以进行这样的搜索,所以我的签名肯定有问题。

这是相关代码(我已经硬编码了我的查询以进行测试):

    public Task<string> Search(string search)
    {
        // search = Uri.EscapeDataString(search);
        return SendRequest("search/tweets.json?q=geocode:26.201461,-98.237987,0.25mi", HttpMethod.GET, new Dictionary<string, string>());
    }

    Task<string> SendRequest(string url, HttpMethod httpMethod, Dictionary<string, string> data)
    {
        var fullUrl = TwitterApiBaseUrl + url;
        Random rand = new Random();

        // Timestamps are in seconds since 1/1/1970.
        var timestamp = (int) ((DateTime.UtcNow - epochUtc).TotalSeconds);

        // Add all the OAuth headers and querystring parameters, we'll need to use when constructing the hash.
        var query = url.Split('?');
        if (query.Count() > 1)
        {
            if (data == null) data = new Dictionary<string, string>();
            var pairs = query[1].Split('&');
            foreach (var pair in pairs)
            {
                var keyvalue = pair.Split('=');
                data.Add(keyvalue[0], keyvalue[1]);
            }
        }
        data.Add("oauth_consumer_key", consumerKey);
        data.Add("oauth_signature_method", "HMAC-SHA1");
        data.Add("oauth_timestamp", timestamp.ToString());
        data.Add("oauth_nonce", rand.Next(10000000, 999999999).ToString());
        data.Add("oauth_token", accessToken);
        data.Add("oauth_version", "1.0");

        // Generate the OAuth signature and add it to our payload.
        data.Add("oauth_signature", GenerateSignature(fullUrl, data, httpMethod));

        // Build the OAuth HTTP Header from the data.
        string oAuthHeader = GenerateOAuthHeader(data);

        switch (httpMethod)
        {
            case HttpMethod.GET:
                return SendRequest(fullUrl, oAuthHeader, null, httpMethod);
            case HttpMethod.POST:
                var formData = new FormUrlEncodedContent(data.Where(kvp => !kvp.Key.StartsWith("oauth_")));
                return SendRequest(fullUrl, oAuthHeader, formData, httpMethod);
            default: return null;
        }
    }

    /// <summary>
    /// Generate an OAuth signature from OAuth header values.
    /// </summary>
    string GenerateSignature(string url, Dictionary<string, string> data, HttpMethod httpMethod)
    {
        var sigString = string.Join(
           "&",
           data
              .Union(data)
              .Select(kvp => string.Format("{0}={1}", WebUtility.UrlEncode(kvp.Key), WebUtility.UrlEncode(kvp.Value)))
              .OrderBy(s => s)
        );

        string urlWithoutParameters = url.Split('?')[0];

        var fullSigData = string.Format(
           "{0}&{1}&{2}",
           httpMethod.ToString(),
           Uri.EscapeDataString(urlWithoutParameters),
           Uri.EscapeDataString(sigString.ToString())
        );

        return Convert.ToBase64String(sigHasher.ComputeHash(new ASCIIEncoding().GetBytes(fullSigData.ToString())));
    }

    /// <summary>
    /// Generate the raw OAuth HTML header from the values (including signature).
    /// </summary>
    string GenerateOAuthHeader(Dictionary<string, string> data)
    {
        return "OAuth " + string.Join(
           ",",
           data
              .Where(kvp => kvp.Key.StartsWith("oauth_"))
              .Select(kvp => string.Format("{0}=\"{1}\"", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
              .OrderBy(s => s)
        );
    }

谁能看出我做错了什么?

aha 在发布后不久就发现了解决方案。在我的例子中,问题是查询字符串正在被编码,但显然必须与发出请求的 url 完全匹配。由于我没有对 url 进行编码以匹配,因此出现错误。

我需要修改代码以对原始 URL 中的查询字符串值进行 UrlEncode,以便它们与签名中提交的内容相匹配。

这是一个让我能够继续的快速而肮脏的解决方案:

    Task<string> SendRequest(string url, HttpMethod httpMethod, Dictionary<string, string> data)
    {
        var fullUrl = TwitterApiBaseUrl;

        // ...

        var query = url.Split('?');
        if (query.Count() > 1)
        {
            fullUrl += query[0] + "?";
            if (data == null) data = new Dictionary<string, string>();
            var pairs = query[1].Split('&');
            foreach (var pair in pairs)
            {
                var keyvalue = pair.Split('=');
                data.Add(keyvalue[0], keyvalue[1]);
                fullUrl += keyvalue[0] + "=" + WebUtility.UrlEncode(keyvalue[1]) + "&";
            }

            fullUrl = fullUrl.TrimEnd('&');
        }
        else
        {
            fullUrl += url;
        }

        // ...
    }

希望这对其他人有帮助!