简体   繁体   English

Blazor 请求被 PHP 上的 CORS 策略阻止

[英]Blazor Request blocked by CORS policy on PHP API

I am setting up a PHP API and a web-page based on client-side Blazor.我正在设置一个 PHP API 和一个基于客户端 Blazor 的网页。 But for some reason CORS is triggered and my login process or any requests to my PHP pages result in CORS errors.但由于某种原因,CORS 被触发,我的登录过程或对我的 PHP 页面的任何请求都会导致 CORS 错误。

I started out testing my PHP API with a C# console app and the Blazor app, I tried using without any database access to test the functionality.我开始使用 C# 控制台应用程序和 Z98AD8B3C99B3CA16F1F7FA84EE6 来测试我的 PHP API 控制台应用程序和 Z98AD8B3C99B3CA16F1F7FA84EE6 访问数据库功能。 The Blazor is right now running with Preview 9. The PHP version is 5.3.8. Blazor 现在正在运行 Preview 9。PHP 版本是 5.3.8。 I could in theory update it, but several other active projects are running on it and I do not have any test environment.理论上我可以更新它,但是其他几个活动项目正在它上面运行,我没有任何测试环境。 MySQL version 5.5.24. MySQL 版本 5.5.24。

First I figured it might have been because I was running it on my local machine, so I pushed it to the website where the PHP and MySQL is also running.首先我想这可能是因为我在本地机器上运行它,所以我将它推送到 PHP 和 MySQL 也在运行的网站。 Still I run into this CORS error.我仍然遇到这个 CORS 错误。

I am still just testing this, so I have tried setting it to allow any origin.我仍然只是在测试这个,所以我尝试将它设置为允许任何来源。 I have not had any experience with CORS before this.在此之前,我对 CORS 没有任何经验。 Pretty sure I ought to be able to add PHP code in each file I access that should allow CORS, but since it should all be on the same website, I figure CORS should not even be relevant?很确定我应该能够在我访问的每个文件中添加 PHP 代码,应该允许 CORS,但由于它应该都在同一个网站上,我认为 CORS 甚至不应该相关?

PHP Code: PHP 代码:

function cors() {

// Allow from any origin
if (isset($_SERVER['HTTP_ORIGIN'])) {
    // Decide if the origin in $_SERVER['HTTP_ORIGIN'] is one
    // you want to allow, and if so:
    header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Max-Age: 86400');    // cache for 1 day
}

// Access-Control headers are received during OPTIONS requests
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {

    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
        // may also be using PUT, PATCH, HEAD etc
        header("Access-Control-Allow-Methods: GET, POST, OPTIONS");         

    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
        header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");

    exit(0);
}

echo "You have CORS!";
}
cors();

C# code using the injected HttpClient: C#代码使用注入的HttpClient:

var resp = await Http.GetStringAsync(link);

The error I get is:我得到的错误是:

Access to fetch at 'https://titsam.dk/ntbusit/busitapi/requestLoginToken.php' from origin 'https://www.titsam.dk' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

The response I hoped to get was that the link I use return a token for the login as it does for my API.我希望得到的响应是我使用的链接返回一个登录令牌,就像它对我的 API 一样。

Is it because its running client side maybe and this triggers CORS?是不是因为它正在运行的客户端可能会触发 CORS? But that does not seem to explain why I cannot make it allow all.但这似乎并不能解释为什么我不能让它允许所有人。

Update: My C# code in OnInitializedAsync:更新:我在 OnInitializedAsync 中的 C# 代码:

link = API_RequestLoginTokenEndPoint;

Http.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", "basic:testuser:testpass");

var requestMessage = new HttpRequestMessage(HttpMethod.Get, link);

requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
{
    credentials = "include"
};

var response = await Http.SendAsync(requestMessage);
var responseStatusCode = response.StatusCode;
var responseBody = await response.Content.ReadAsStringAsync();

output = responseBody + " " + responseStatusCode;

Update 2: It finally works.更新 2:它终于起作用了。 The C# code I linked is the solution Agua From Mars suggested and it solved the problem to use SendAsync with a HttpRequestMessage and adding the Fetch property include credentials to it.我链接的 C# 代码是 Agua From Mars 建议的解决方案,它解决了将 SendAsync 与 HttpRequestMessage 一起使用并添加 Fetch 属性包括凭据的问题。 Another alternative was to add this line to the startup:另一种选择是将这一行添加到启动中:

WebAssemblyHttpMessageHandler.DefaultCredentials = FetchCredentialsOption.Include;

Then I could keep doing what I did to begin with, using GetStringAsync as it becomes the default.然后我可以继续做我开始做的事情,使用 GetStringAsync 因为它成为默认值。 await Http.GetStringAsync(API_RequestLoginTokenEndPoint);等待 Http.GetStringAsync(API_RequestLoginTokenEndPoint);

So all the solutions Agua From Mars suggested worked.所以来自火星的 Agua 建议的所有解决方案都有效。 But I encountered a browser problem, where it kept the CORS issue in the cache somehow even after it had gotten solved, so it seemed like nothing had changed.但是我遇到了一个浏览器问题,它以某种方式将 CORS 问题保留在缓存中,即使它已经解决了,所以似乎什么都没有改变。 Some code changes would show a different result, but I guess the CORS part was kept alive.一些代码更改会显示不同的结果,但我猜 CORS 部分保持活动状态。 With Chrome it helped opening a new pane or window.借助 Chrome,它有助于打开一个新窗格或 window。 In my Opera browser this was not enough, I had to close all panes with the site open to ensure it would clear the cache and then opening a new window or pane with the site works in Opera as well.在我的 Opera 浏览器中,这还不够,我必须在站点打开的情况下关闭所有窗格以确保它会清除缓存,然后打开一个新的 window 或窗格,该站点也可以在 Opera 中运行。 I had already in both browsers trying to use ctrl-F5 and Shift-F5 to get them to clear the cache.我已经在两个浏览器中尝试使用 ctrl-F5 和 Shift-F5 让它们清除缓存。 This did not change anything.这并没有改变什么。

I hope this will help others avoid spending 2-3 days on an issue like this.我希望这将帮助其他人避免在这样的问题上花费 2-3 天。

update 3.1-preview3更新 3.1-preview3

In 3.1-preview3, we cannot use the fetch option per message, the options is global在 3.1-preview3 中,我们不能对每条消息使用 fetch 选项,这些选项是全局的

WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;

WebAssemblyHttpMessageHandler has been removed. WebAssemblyHttpMessageHandler已被删除。 The HttpMessageHanlder used is WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler from WebAssembly.Net.Http but don't include WebAssembly.Net.Http in your depencies or the application will failled to launch. The HttpMessageHanlder used is WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler from WebAssembly.Net.Http but don't include WebAssembly.Net.Http in your depencies or the application will failled to launch.

If you want to use the HttpClientFactory you can implement like that:如果你想使用HttpClientFactory你可以这样实现:

public class CustomDelegationHandler : DelegatingHandler
{
    private readonly IUserStore _userStore;
    private readonly HttpMessageHandler _innerHanler;
    private readonly MethodInfo _method;

   public CustomDelegationHandler(IUserStore userStore, HttpMessageHandler innerHanler)
   {
       _userStore = userStore ?? throw new ArgumentNullException(nameof(userStore));
       _innerHanler = innerHanler ?? throw new ArgumentNullException(nameof(innerHanler));
       var type = innerHanler.GetType();
       _method = type.GetMethod("SendAsync", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod) ?? throw new InvalidOperationException("Cannot get SendAsync method");
       WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
   }
   protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   {
        request.Headers.Authorization = new AuthenticationHeaderValue(_userStore.AuthenticationScheme, _userStore.AccessToken);            
        return _method.Invoke(_innerHanler, new object[] { request, cancellationToken }) as Task<HttpResponseMessage>;
   }
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient(p =>
    {
        var wasmHttpMessageHandlerType =  Assembly.Load("WebAssembly.Net.Http")
                        .GetType("WebAssembly.Net.Http.HttpClient.WasmHttpMessageHandler");
        var constructor = wasmHttpMessageHandlerType.GetConstructor(Array.Empty<Type>());
        return constructor.Invoke(Array.Empty<object>()) as HttpMessageHandler;
    })
    .AddTransient<CustomDelegationHandler>()
    .AddHttpClient("MyApiHttpClientName")
    .AddHttpMessageHandler<CustonDelegationHandler>();
}

3.0 -> 3.1-preview2 3.0 -> 3.1-preview2

On Blazor client side your need to tell to the Fetch API to send credentials (cookies and authorization header).在 Blazor 客户端上,您需要告诉Fetch API以发送凭据(cookie 和授权标头)。

It's describe in the Blazor doc Cross-origin resource sharing (CORS)它在Blazor doc 跨域资源共享 (CORS)中进行了描述

        requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
        { 
            credentials = FetchCredentialsOption.Include
        };

ex:前任:

@using System.Net.Http
@using System.Net.Http.Headers
@inject HttpClient Http

@code {
    private async Task PostRequest()
    {
        Http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", "{OAUTH TOKEN}");

        var requestMessage = new HttpRequestMessage()
        {
            Method = new HttpMethod("POST"),
            RequestUri = new Uri("https://localhost:10000/api/TodoItems"),
            Content = 
                new StringContent(
                    @"{""name"":""A New Todo Item"",""isComplete"":false}")
        };

        requestMessage.Content.Headers.ContentType = 
            new System.Net.Http.Headers.MediaTypeHeaderValue(
                "application/json");

        requestMessage.Content.Headers.TryAddWithoutValidation(
            "x-custom-header", "value");

        requestMessage.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
        { 
            credentials = FetchCredentialsOption.Include
        };

        var response = await Http.SendAsync(requestMessage);
        var responseStatusCode = response.StatusCode;
        var responseBody = await response.Content.ReadAsStringAsync();
    }
}

You can set up this option globaly with WebAssemblyHttpMessageHandlerOptions.DefaultCredentials static proprerty.您可以使用WebAssemblyHttpMessageHandlerOptions.DefaultCredentials static 属性全局设置此选项。

Or you can implement a DelegatingHandler and set it up in DI with the HttpClientFactory:或者你可以实现一个DelegatingHandler并使用 HttpClientFactory 在 DI 中设置它:

    public class CustomWebAssemblyHttpMessageHandler : WebAssemblyHttpMessageHandler
    {
        internal new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return base.SendAsync(request, cancellationToken);
        }
    }

    public class CustomDelegationHandler : DelegatingHandler
    {
        private readonly CustomWebAssemblyHttpMessageHandler _innerHandler;

        public CustomDelegationHandler(CustomWebAssemblyHttpMessageHandler innerHandler)
        {
            _innerHandler = innerHandler ?? throw new ArgumentNullException(nameof(innerHandler));
        }
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            request.Properties[WebAssemblyHttpMessageHandler.FetchArgs] = new
            {
                credentials = "include"
            };
            return _innerHandler.SendAsync(request, cancellationToken);
        }
    }

In Setup.ConfigureServicesSetup.ConfigureServices

services.AddTransient<CustomWebAssemblyHttpMessageHandler>()
    .AddTransient<WebAssemblyHttpMessageHandler>()
    .AddTransient<CustomDelegationHandler>()
    .AddHttpClient(httpClientName)
    .AddHttpMessageHandler<CustomDelegationHandler>();

Then you can create an HttpClient for your API with IHttpClientFactory.CreateClient(httpClientName)然后您可以使用IHttpClientFactory.CreateClient(httpClientName)为您的 API 创建一个HttpClient

To use the IHttpClientFactory you need to install Microsoft.Extensions.Http package.要使用IHttpClientFactory ,您需要安装Microsoft.Extensions.Http package。

3.0-preview3 => 3.0-preview9 3.0-preview3 => 3.0-preview9

Replace WebAssemblyHttpMessageHandler with BlazorHttpMessageHandlerWebAssemblyHttpMessageHandler替换为BlazorHttpMessageHandler

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM