HttpClient 在 Blazor Webassembly 应用程序中不包含带有请求的 cookie

HttpClient doesn't include cookies with requests in Blazor Webassembly app

我有一个 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。

我是 Blazor 的新手,我不确定这种情况是否存在任何约定,但目前我只是想让这个新的网络应用程序与我们现有的服务一起工作.有没有办法确保包含 cookie?我可能做错了什么?

在此先感谢您的帮助。

编辑

这是创建 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;
    }
}

不幸的是,结果是一样的 - RequestCookieCollection 在到达 API.

时完全是空的

这是我在测试 Blazor WebAssembly AspNet 托管应用程序中所做的:

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 上,我在请求 headers 中查找密钥,如果存在,我将尝试获取值:

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 请求的值 headers。

如果您需要创建 cookie,则必须使用 JsInterop,更多详细信息请参见 How do I create a cookie client side using blazor

基于@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 "";
    }
}

我还修改了服务端的内容,以在 headers 中查找 cookie 而不是 collection 中的 cookie。现在,而不是这个...

var myCookie = HttpContext.Request.Cookies.FirstOrDefault(c => c.Key == "MyCookie");

...我已经这样做了:

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

诚然,我不知道这如何符合 Blazor 应用程序中此类事物的约定,但它似乎足以满足我们的目的。再次感谢大家的帮助。

添加这个

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

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

在 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()
);

现在一切都应该按预期工作了。