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

Posted

技术标签:

【中文标题】HttpClient 在 Blazor Webassembly 应用程序中不包含带有请求的 cookie【英文标题】:HttpClient doesn't include cookies with requests in Blazor Webassembly app 【发布时间】:2020-09-10 14:31:37 【问题描述】:

我有一个带有用户服务的 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 包含用户的电子邮件地址,用户信息服务使用此 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 完全陌生,我不确定是否存在适用于此类场景的任何约定,但目前我只是想让这个新的 Web 应用程序与我们现有的服务一起使用。有没有办法确保包含 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 时是完全空的。

【问题讨论】:

能否也包括写入或配置cookie的代码?某些设置可能会影响浏览器将其正确包含在您的请求中的能力。 看起来将您的路径设置为“/”可能是问题所在。查看文档,如果请求是针对已发布域的根目录,这听起来只会包含您的 cookie。您介意在 CookieOptions 中删除该属性并查看问题是否仍然存在吗? docs.microsoft.com/en-us/dotnet/api/… 如果我没看错,您正试图将此 cookie 发送到服务器,在这种情况下,它不应该是 Request 而不是 Response?可以试试加httpclient.DefaultRequestHeaders.Add("MyCookie", "SomeCokkieContent"); 情节变厚了。唯一引起我注意的是 Secure = true。当设置为 true 时,UI 和 API 都必须使用 HTTPS 进行所有通信。你也可以仔细检查一下。如果这不起作用,我赞成提请注意这个问题。祝你好运。 我在这里有点困惑,当您向服务器发布内容时,您试图在标头中包含某些内容,或者您​​正在寻找服务器在响应标头中发送的内容? _HttpClient.DefaultRequestHeaders.Add("Test", "ABC123"); 后面应该跟一个 POST 请求,而不是 GET,如果您的范围是将它发送到服务器。 【参考方案1】:

这是我在测试 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 我正在查看密钥的请求标头,如果存在,我正在尝试获取值:

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,更多细节在这里How do I create a cookie client side using blazor。

【讨论】:

我实际上已经在我的代码中尝试过了,它确实有效,至少我能够在标题中包含 cookie 值。话虽如此,HttpContext.Request.Cookies 集合仍然是空的。我也尝试过使用Http.DefaultRequestHeaders.Add("Set-Cookie", $"myCookie=someValue"),但这仍然没有填充集合。我不知道如果我仍然可以从标头中读取 cookie 值有多重要,但这仍然令人费解。【参考方案2】:

基于@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(); 这个答案的一个问题是它永远不会与 HttpOnly 设置为 true 的 cookie 一起工作。 JSRuntime 将无法读取这些值。【参考方案3】:

添加这个

public class CookieHandler : DelegatingHandler

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

        return await base.SendAsync(request, cancellationToken);
    

【讨论】:

请确保添加一些关于代码在做什么以及为什么要使用它的信息【参考方案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 允许凭据。

使用 Program.cs 中应该已有的 .net 6 语法:

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

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

【讨论】:

以上是关于HttpClient 在 Blazor Webassembly 应用程序中不包含带有请求的 cookie的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Blazor WASM 客户端中访问 Httpclient 标头

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

尽管 Fiddler 显示收到了正确的响应,但 Blazor WASM 上的 HttpClient.SendAsync 返回空 HttpResponseMessage

ServiceStack:添加 Blazor 支持?

如何在服务器端 Blazor 组件中访问实体框架 DbContext 实体

Blazor解决Blazor WebAssembly跨域访问后台服务问题