如何使用System.Net.Http.HttpClient进行基本身份验证?

77
我正在尝试在c# .net core中实现一个REST客户端,需要先进行基本身份验证,然后在后续请求中使用Bearer令牌。当我尝试结合FormUrlEncodedContent对象进行基本身份验证并进行client.PostAsync时,我遇到了异常:
System.InvalidOperationException occurred in System.Net.Http.dll: 'Misused header name. Make sure request headers are used with HttpRequestMessage, response headers with HttpResponseMessage, and content headers with HttpContent objects.'
//setup reusable http client
HttpClient client = new HttpClient();
Uri baseUri = new Uri(url);
client.BaseAddress = baseUri;
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.ConnectionClose = true;

//Post body content
var values = new List<KeyValuePair<string,string>>();
values.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));

var content = new FormUrlEncodedContent(values);

//Basic Authentication
var authenticationString = $"{clientId}:{clientSecret}";
var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(authenticationString));
content.Headers.Add("Authorization", $"Basic {base64EncodedAuthenticationString}");

//make the request
var task = client.PostAsync("/oauth2/token",content);
var response = task.Result;
response.EnsureSuccessStatusCode();
string responseBody = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(responseBody);
Exception has occurred: CLR/System.InvalidOperationException
An unhandled exception of type 'System.InvalidOperationException' occurred in System.Net.Http.dll: 'Misused header name. Make sure request headers are used with HttpRequestMessage, response headers with HttpResponseMessage, and content headers with HttpContent objects.'
   at System.Net.Http.Headers.HttpHeaders.GetHeaderDescriptor(String name)
   at System.Net.Http.Headers.HttpHeaders.Add(String name, String value)

1
这会不会在整个应用程序中应用相同的授权?我需要在每个请求中控制授权/标头。 - ScArcher2
2
您不需要对标题的“Basic”部分进行编码。 - Robert McKee
是的,那是一个愚蠢的错误,但那只会导致身份验证错误。这不是我目前要解决的问题。 - ScArcher2
可能的解决方案:https://dev59.com/QGMk5IYBdhLWcg3wtQFd - Neil Moss
7个回答

100

看起来你不能使用 PostAsync 并且无法访问头部进行身份验证的操作。我不得不使用 HttpRequestMessage 和 SendAsync。

//setup reusable http client
HttpClient client = new HttpClient();
Uri baseUri = new Uri(url);
client.BaseAddress = baseUri;
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.ConnectionClose = true;

//Post body content
var values = new List<KeyValuePair<string, string>>();
values.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
var content = new FormUrlEncodedContent(values);

var authenticationString = $"{clientId}:{clientSecret}";
var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(authenticationString));

var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/oauth2/token");
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString);
requestMessage.Content = content;

//make the request
var response = await client.SendAsync(requestMessage);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseBody);

32

直接从调用代码中显式创建HttpClients并不是一个好的实践。 请使用简化许多事情的HttpClientFactory

但是,如果您想使用基本身份验证,只需创建一个HttpRequestMessage并添加以下标头:

var request = new HttpRequestMessage(HttpMethod.Post, getPath)
{
    Content = new FormUrlEncodedContent(values)
};
request.Headers.Authorization = new BasicAuthenticationHeaderValue("username", "password");
// other settings

如果你决定使用推荐的IHttpClientFactory,那么它甚至更简单:

serviceCollection.AddHttpClient(c =>
{
   c.BaseAddress = new Uri("your base url");
   c.SetBasicAuthentication("username", "password");
})

9
感谢指出HttpClientFactory,并使用BasicAuthenticationHeaderValue()而不是重新发明将Convert.ToBase64String转换成字符串的方法。 - Jpsy
19
BasicAuthenticationHeaderValue 只在 ASP.NET Identity 中可用。 我找不到任何关于它的文档。 - Luke Vo
4
这段话的意思是,要使用System.Net.Http.Headers中提供的AuthenticationHeaderValue类来设置HTTP请求头部的授权信息。可以通过以下方式实现:request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(encoding.GetBytes("username" + ":" + "password")));其中,需要将用户名和密码以字符串的形式传入,并使用Base64编码后再进行设置。 - EvilDr
3
不同的是,AuthenticationHeaderValue不同于BasicAuthenticationHeaderValue。尽管你给出的解决方案可行,但你仍需要自己处理令牌值。 - Luke Vo
17
SetBasicAuthentication 是从哪里来的? - David Gardiner
显示剩余2条评论

30

不要对整个身份验证字符串进行编码 - 编码“用户名:密码”表达式,并将结果附加到“Basic”前缀。

var authenticationString = $"{clientId}:{clientSecret}";
var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.ASCIIEncoding.UTF8.GetBytes(authenticationString));
content.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString);

还要考虑仅使用ASCII编码 - 除非您在头文件中添加charset声明,否则服务器可能无法理解UTF8。

维基百科似乎已经涵盖得很全面了。


谢谢你注意到这个问题!不过这不是问题所在。 - ScArcher2
1
@Neil Moss 我相信你的代码仍在使用UTF8编码。请将 ASCIIEncoding.UTF8 更改为 Encoding.ASCII。(这些静态编码实例被所有编码子类继承的方式,可能会产生一些奇怪的问题。) - pettys

7
具体问题是下面这行代码(以下)
content.Headers.Add("Authorization", $"Basic {base64EncodedAuthenticationString}");

这种方法失败是因为HttpContent.Headers (System.Net.Http.Headers.HttpContentHeaders)仅适用于特定于内容的头,例如Content-TypeContent-Length等。

您已经说明您不能使用DefaultRequestHeaders,因为您只需要它来处理单个请求 - 但您也不能在PostAsync中使用它 - 只能在SendAsync中使用,前提是您自己构造HttpRequestMessage根据您自己的答案和@NeilMoss的答案 - 但以后您可以使用扩展方法。

但为了其他读者的利益,另一种选择是添加一个基于现有的PostAsync的新扩展方法,这实际上非常简单(仅需3行!):

public Task<HttpResponseMessage> PostAsync( this HttpClient httpClient, Uri requestUri, HttpContent content, String basicUserName, String basicPassword, String? challengeCharSet = null, CancellationToken cancellationToken = default )
{
    if( basicUserName.IndexOf(':') > -1 ) throw new ArgumentException( message: "RFC 7617 states that usernames cannot contain colons.", paramName: nameof(basicUserName) );

    HttpRequestMessage httpRequestMessage = new HttpRequestMessage( HttpMethod.Post, requestUri );
    httpRequestMessage.Content = content;

    //

    Encoding encoding = Encoding.ASCII;
    if( challengeCharSet != null )
    {
        try
        {
            encoding = Encoding.GetEncoding( challengeCharSet );
        }
        catch
        {
            encoding = Encoding.ASCII;
        }
    }

    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue(
        scheme   : "Basic",
        parameter: Convert.ToBase64String( encoding.GetBytes( userName + ":" + password ) )
    );

    return SendAsync( httpRequestMessage, cancellationToken );
}

用法:

HttpClient httpClient = ...

using( HttpResponseMessage response = await httpClient.PostAsync( uri, content, basicUserName: "AzureDiamond", basicPassword: "hunter2" ).ConfigureAwait(false) )
{
    // ...
}

6

使用 .NET 6,我使用 HttpClient.DefaultRequestHeaders.Authorization 属性来设置 Authorization 头。

// This example will send a signing request to the RightSignature API
var api = "https://api.rightsignature.com/public/v2/sending_requests";

// requestJson is the serialized JSON request body
var contentData = new StringContent(requestJson, Encoding.UTF8, "application/json");

// Instantiate client (for testing), use Microsoft's guidelines in production
var client = new HttpClient();

// Use basic auth, the token has already been converted to base64
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", tokenB64);

try
{
    var response = await client.PostAsync(api, contentData);
}
...

祝你好运!


5

我有一个需要补充的问题,就是我在使用基本认证端点时遇到了困难。如果您将Json作为StringContent添加,则它会添加charset = utf-8,这经常导致返回BadRequest 400。

以下代码可以解决这个问题: 参考: https://dzone.com/articles/httpclient-how-to-remove-charset-from-content-type

using (var client = new HttpClient())
using (var content = new StringContent(ParseJSON(data), Encoding.Default, "application/json"))
{  
   //Remove UTF-8 Charset causing BadRequest 400
   content.Headers.ContentType.CharSet = "";

   var clientId = "client";
   var clientSecret = "secret";
   var authenticationString = $"{clientId}:{clientSecret}";
   var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.ASCIIEncoding.UTF8.GetBytes(authenticationString));
   client.DefaultRequestHeaders.TryAddWithoutValidation(authHeader, authorization);

   var response = await client.PostAsync(url, content);
   return response;
}

4

我已经通过使用以下代码解决了这个问题,这也符合我的目的。为Get / Post添加的代码,这将有助于您。此外,我还添加了另一个Header key。因此,可以传递额外的数据到header中。希望这能解决您的问题。

class Program 
{
    private static readonly string Username = "test";
    private static readonly string Password = "test@123";
    
    static void Main(string[] args) 
    {
        var response = Login();
    }
    
    public static async Task Login() 
    {
        var anotherKey ="test";
        HttpClient httpClient = new HttpClient
        {
            BaseAddress = new Uri("https://google.com/")
        };

        httpClient.DefaultRequestHeaders.Add($"Authorization", $"Basic {Base64Encode($"{Username}:{Password}")}");
        httpClient.DefaultRequestHeaders.Add($"anotherKey", $"{anotherKey}");
        HttpResponseMessage httpResponseMessage = await httpClient.GetAsync("user/123").ConfigureAwait(false);
        // For Get Method
        var response= await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
        // For Post Method
        User user = new User (1,"ABC");
        HttpResponseMessage httpResponseMessage = await httpClient.PostAsJsonAsync("/post", user).ConfigureAwait(false);
        UserDetail userDetail  = await httpResponseMessage.Content.ReadAsAsync<UserDetail>().ConfigureAwait(false);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接