Blazor Webassembly 应用程序中的 HttpClient 不会在请求中包含 cookies

10

我有一个使用Blazor WebAssembly开发的应用程序,并且有一个用户服务,旨在调用API以检索用户的详细信息。该服务如下所示:

public class UserDataService : IUserDataService
{
    public readonly HttpClient _HttpClient;

    public UserDataService(HttpClient httpClientDI)
    {
        _HttpClient = httpClientDI;
    }

    public async Task<User> GetUserInfo()
    {
        try
        {
            return await _HttpClient.GetFromJsonAsync<User>("api/users/MyUserInfo");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            throw;
        }
    }
}

该API专门设计用于从客户端请求中读取加密cookie。此cookie包含用户的电子邮件地址,并由用户信息服务用于检索更详细的用户信息。

[HttpGet("MyUserInfo")]
public User MyUserInfo()
{
    var myCookie = HttpContext.Request.Cookies.FirstOrDefault(c => c.Key == "MyCookie");

    var userMask = JsonConvert.DeserializeObject<AuthUserMask>(Protector.Unprotect(myCookie.Value));

    var user = UserService.Find(userMask.Email).FirstOrDefault();

    return user;
}

当我运行 Web 应用程序时,我能够验证浏览器中存在 cookie,但是当应用程序向 API 发送请求时,该 cookie 并未被包含在内。实际上,请求根本不包含来自客户端的任何 cookie。

enter image description here

我完全不了解 Blazor,并且不确定是否存在任何约定来处理这种情况,但目前我只是试图使这个新的 Web 应用程序与我们现有的服务配合工作。有没有办法确保 cookies 被包含在内?我可能做错了什么?

感谢您提前的帮助。

编辑:

以下是创建 cookie 的代码。它是验证用户已通过身份验证的较大方法的一部分,但这是相关部分:

{
    var userJson = JsonConvert.SerializeObject(new AuthUserMask()
    {
        Email = user.Email,
        isActive = user.IsActive
    });

    var protectedContents = Protector.Protect(userJson);

    HttpContext.Response.Cookies.Append("MyCookie", protectedContents, new CookieOptions()
    {
        SameSite = SameSiteMode.None,
        Secure = true,
        Path = "/",
        Expires = DateTime.Now.AddMinutes(60)
    });

    HttpContext.Response.Redirect(returnUrl);
}

编辑 2

在 UserDataService 中尝试了以下操作,以查看会发生什么:

public async Task<User> GetUserInfo()
{
    try
    {
        _HttpClient.DefaultRequestHeaders.Add("Test", "ABC123");
        return await _HttpClient.GetFromJsonAsync<User>("api/users/MyUserInfo");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}

不幸的是,结果是一样的——当它到达API时,RequestCookieCollection完全为空。


1
你能否同时包含写入或配置cookie的代码?某些设置可能会影响浏览器正确地将其包含在请求中的能力。 - Matt Hensley
1
看起来将您的路径设置为“/”可能是问题所在。查看文档,似乎只有在请求发出域的根目录时才会包括您的Cookie。您能否在CookieOptions中删除该属性并查看问题是否仍然存在?https://learn.microsoft.com/en-us/dotnet/api/system.web.httpcookie.path?view=netframework-4.8 - Matt Hensley
1
如果我理解正确,您正在尝试将此cookie发送到服务器,这种情况下它不应该是Response而是Request吗? 您可以尝试添加httpclient.DefaultRequestHeaders.Add("MyCookie", "SomeCokkieContent"); - Mihaimyh
1
情节越来越复杂了。唯一吸引我注意的是Secure = true。当设置为true时,UI和API必须使用HTTPS进行所有通信。您也可以再次检查一下。如果这不起作用,我已经点赞以引起一些关注。祝你好运。 - Matt Hensley
1
我有点困惑,你是在 POST 到服务器时尝试在标头中包含某些内容,还是在查找响应标头中的某些内容发送到服务器?如果你的目标是将“_HttpClient.DefaultRequestHeaders.Add("Test", "ABC123");”发送到服务器,那么它应该跟随一个 POST 请求而不是 GET。 - Mihaimyh
显示剩余6条评论
4个回答

4

在 Program.cs 文件中使用 Blazor .NET 6 样式,需要以下代码:

builder.Services
    .AddTransient<CookieHandler>()
    .AddScoped(sp => sp
        .GetRequiredService<IHttpClientFactory>()
        .CreateClient("API"))
    .AddHttpClient("API", client => client.BaseAddress = new Uri(apiAddress)).AddHttpMessageHandler<CookieHandler>();

然后您需要像@murat_yuceer描述的处理程序:

namespace Client.Extensions
{
    public class CookieHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

            return await base.SendAsync(request, cancellationToken);
        }
    }
}

您不需要(也不应该)指定cookie。正确的cookie将为您发送,只需在消息中添加BrowserRequestCredentials.Include

在您拥有API的服务器端,您需要设置允许凭据的CORS。

使用.NET 6语法,您应该已经在Program.cs中拥有:

app.UseCors(x => x.
  .AllowAnyHeader()
  .AllowAnyMethod()
  .AllowAnyOrigin()
);

但你还需要添加AllowCredentials()

如果你添加了AllowCredentials,你会得到以下运行时错误:

System.InvalidOperationException: 'The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. Configure the CORS policy by listing individual origins if credentials needs to be supported.'

因此,您需要指定允许的来源,或者像这样使用通配符:

app.UseCors(x => x
    .AllowAnyHeader()
    .AllowAnyMethod()
    //.AllowAnyOrigin()
    .SetIsOriginAllowed(origin => true)
    .AllowCredentials()
);

现在一切都应该正常工作了。


2
这是我在一个测试的Blazor WebAssembly AspNet Hosted应用程序中所做的事情: < p > 在 < code > FetchData.razor 中实现了以下内容:
@page "/fetchdata"
@using BlazorApp3.Shared
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        Http.DefaultRequestHeaders.Add("key", "someValue");
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }

}

注意:Http.DefaultRequestHeaders.Add("key", "someValue");

在服务器端,WeatherForecastController 中我正在查找请求头中的键,如果存在则尝试获取其值:

using BlazorApp3.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

namespace BlazorApp3.Server.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        // The Web API will only accept tokens 1) for users, and 2) having the access_as_user scope for this API
        private static readonly string[] scopeRequiredByApi = new string[] { "user_impersonation" };

        private static readonly string[] Summaries = new[]
                {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            if (HttpContext.Request.Headers.ContainsKey("key"))
            {
                var success = HttpContext.Request.Headers.TryGetValue("key", out var headervalue);

                if (success)
                {
                    _logger.LogInformation(headervalue.ToString());
                }
            }

            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

调试截图

我可以获取HTTP请求头中的值。

如果您需要创建Cookie,必须使用JsInterop,更多详细信息请参见如何在Blazor中客户端创建Cookie


1
我已经在我的代码中尝试过这个方法,它确实可以工作,至少我能够将cookie值包含在头部中。话虽如此,HttpContext.Request.Cookies集合仍然为空。我还尝试使用Http.DefaultRequestHeaders.Add("Set-Cookie", $"myCookie={someValue}"),但仍无法填充集合。我不知道如果我仍然可以从头部读取cookie值会有多大影响,但这仍然令人困惑。 - Dumas.DED

1
添加这个。
public class CookieHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

        return await base.SendAsync(request, cancellationToken);
    }
}

2
请确保添加一些关于代码正在做什么以及为什么使用它的信息。 - voidraen

1

基于@Mihaimyh的一些见解,我能够使用自定义委托处理程序在用户数据服务上使其工作。注册方式如下:

builder.Services.AddHttpClient<IUserDataService, UserDataService>(client => client.BaseAddress = new Uri("https://localhost:44336/"))
                .AddHttpMessageHandler<CustomDelegatingHandler>();

内部使用JSInterop来运行Javascript函数以检索cookie,然后将其附加到使用SendAsync()方法的所有传出请求中:

public class CustomDelegatingHandler : DelegatingHandler
{
    private IJSRuntime JSRuntime;

    public CustomDelegatingHandler(IJSRuntime jSRuntime) : base()
    {
        JSRuntime = jSRuntime;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var cookie = await JSRuntime.InvokeAsync<string>("blazorExtensions.GetCookie", new[] { "MyCookie" });
        Debug.WriteLine($"My cookie: {cookie}");
        request.Headers.Add("MyCookie", $"{cookie}");
        return await base.SendAsync(request, cancellationToken);
    }
}

Javascript函数长这样(几乎完全摘自W3Schools):
window.blazorExtensions = { 
    GetCookie: function (cname) {
        var name = cname + "=";
        var decodedCookie = decodeURIComponent(document.cookie);
        var ca = decodedCookie.split(';');
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1);
            }
            if (c.indexOf(name) == 0) {
                return c.substring(name.length, c.length);
            }
        }
        return "";
    }
}

我还在服务端进行了修改,现在会在头部寻找cookie而不是cookie集合。现在,代替之前的...
var myCookie = HttpContext.Request.Cookies.FirstOrDefault(c => c.Key == "MyCookie");

...我已经做到了这一点:

HttpContext.Request.Headers.TryGetValue("MyCookie", out var myCookie);

我承认我不知道这与Blazor应用程序中的此类事物的惯例如何相符,但对于我们的目的而言,它似乎运行良好。再次感谢大家的帮助。


很明显,但请记得注册您的CustomDelegatingHandler builder.Services.AddScoped<CustomDelegatingHandler>(); - Rocklands.Cave
这个答案的一个问题是它永远无法使用已设置为true的HttpOnly cookie。JSRuntime将无法读取这些值。 - The Thirsty Ape

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