英文:
Can't load API from localhost from Angular Client with C# Backend hosted on a standalone unit
问题
I am running an Angular Client (v14) with a .Net 6 WebAPI. 运行一个使用 Angular 客户端(版本14)和 .Net 6 WebAPI 的项目。
These are in separate .Net projects running on a Raspberry Pi. 这些项目是在树莓派上运行的不同 .Net 项目中。
It is a standalone kiosk, so the front and backend run on the same box. 这是一个独立的信息亭,前端和后端在同一台设备上运行。
I want to be able to access the front end from a PC on the same network, via the browser. 我希望能够通过浏览器从同一网络上的计算机访问前端。
When I remote to the address of the Raspberry Pi, I can see the loading screen of the Angular App, but it can't resolve localhost, as it looks to the PC's localhost for the backend, not the Kiosk. 当我远程连接到树莓派的地址时,我可以看到 Angular 应用的加载屏幕,但它无法解析 localhost,因为它将后端的请求定向到了PC的 localhost,而不是信息亭的 localhost。
I also want to be able to access the API remotely to control the functions of the unit, this might be via Postman or a third-party application. 但我还希望能够远程访问 API 以控制该单位的功能,可能是通过 Postman 或第三方应用程序。但这可能是一个单独的问题,我以后可以解决。
I need the angular application to rewrite the localhost to the current IP address. 我需要 Angular 应用程序将 localhost 重写为当前的 IP 地址。
I've struggled to find examples of how this is done and things have got quite convoluted along the way. 我苦苦寻找如何实现这一点的示例,而且事情已经变得相当复杂。
Below are sections of the configuration, I wonder if someone can point me in the right direction or point me to an example of how I can make this work? 以下是配置的部分,我想知道是否有人能指导我正确的方向,或者为我提供一个示例,以便我可以使其工作?
launchSettings.json
{
"profiles": {
"Kiosk_ClientApp": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://0.0.0.0:4200"
}
}
}
nginx
server {
listen 443 ssl;
listen [::]:443 ssl;
include snippets/self-signed.conf;
include snippets/ssl-params.conf;
server_name kiosk;
location / {
proxy_pass https://localhost:4200;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_cookie_path / "/; SameSite = None' secure";
proxy_read_timeout 18000s;
proxy_send_timeout 18000s;
}
location /api {
proxy_pass https://localhost:4901;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
proxy.conf.json
{
"/api": {
"target": "https://0.0.0.0:4901",
"secure": true,
"changeOrigin": true,
"logLevel": "debug"
}
}
environment.ts
export const environment = {
production: false,
urlAddress: "https://localhost:4901"
};
environment.prod.ts
export const environment = {
production: true,
urlAddress: "https://localhost:4901"
};
angular.json
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"Kiosk": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"progress": false,
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/assets"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css",
"node_modules/sweetalert2/src/sweetalert2.scss",
"node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css",
"node_modules/datatables.net-dt/css/jquery.dataTables.css"
],
"scripts": [
"./node_modules/jquery/dist/jquery.min.js",
"./node_modules/popper.js/dist/umd/popper.min.js",
"./node_modules/bootstrap/dist/js/bootstrap.min.js",
"./node_modules/chart.js/dist/Chart.js",
"node_modules/jquery/dist/jquery.js",
"node_modules/datatables.net/js/jquery.dataTables.js",
"node_modules/datatables.net-bs4/js/dataTables.bootstrap4.min.js"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "Kiosk:build",
"proxyConfig": "src/proxy.conf.json",
"ssl": true,
"sslCert": "ssl/server.crt",
"sslKey": "ssl/server.key"
<details>
<summary>英文:</summary>
I am running an Angular Client (v14) with a .Net 6 WebAPI. These are in separate .Net projects running on a Raspberry Pi. It is a standalone kiosk, so the front and backend run on the same box.
I want to be able to access the front end from a PC on the same network, via the browser. When I remote to the address of the Raspberry Pi, I can see the loading screen of the Angular App, but it can't resolve localhost, as it looks to the PC's localhost for the backend, not the Kiosk.
I also want to be able to access the API remotely to control the functions of the unit, this might be via Postman or a third party application. But this maybe a separate issue, which I can solve later.
I need the angular application to rewrite the localhost to the current IP address. I've struggled to find example of how this is done and things have got quite convoluted along the way. Below are sections of the configuration, I wonder if someone can point me in the right direction or point me to an example of how I can make this work?
launchSettings.json
{
"profiles": {
"Kiosk_ClientApp": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://0.0.0.0:4200"
}
}
}
nginx
server {
listen 443 ssl;
listen [::]:443 ssl;
include snippets/self-signed.conf;
include snippets/ssl-params.conf;
server_name kiosk;
location / {
proxy_pass https://localhost:4200;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_cookie_path / "/; SameSite = None' secure";
proxy_read_timeout 18000s;
proxy_send_timeout 18000s;
}
location /api {
proxy_pass https://localhost:4901;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
proxy.conf.json
{
"/api": {
"target": "https://0.0.0.0:4901",
"secure": true,
"changeOrigin": true,
"logLevel": "debug"
}
}
environment.ts
export const environment = {
production: false,
urlAddress: "https://localhost:4901"
};
environment.prod.ts
export const environment = {
production: true,
urlAddress: "https://localhost:4901"
};
angular.json
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"Kiosk": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"progress": false,
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/assets"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css",
"node_modules/sweetalert2/src/sweetalert2.scss",
"node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css",
"node_modules/datatables.net-dt/css/jquery.dataTables.css"
],
"scripts": [
"./node_modules/jquery/dist/jquery.min.js",
"./node_modules/popper.js/dist/umd/popper.min.js",
"./node_modules/bootstrap/dist/js/bootstrap.min.js",
"./node_modules/chart.js/dist/Chart.js",
"node_modules/jquery/dist/jquery.js",
"node_modules/datatables.net/js/jquery.dataTables.js",
"node_modules/datatables.net-bs4/js/dataTables.bootstrap4.min.js"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "Kiosk:build",
"proxyConfig": "src/proxy.conf.json",
"ssl": true,
"sslCert": "ssl/server.crt",
"sslKey": "ssl/server.key"
},
"configurations": {
"production": {
"browserTarget": "Kiosk:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "Kiosk:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.css"
],
"scripts": [],
"assets": [
"src/assets"
]
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist-server",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.server.json",
"sourceMap": true,
"optimization": false
},
"configurations": {
"dev": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true
},
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true
}
},
"defaultConfiguration": ""
}
}
},
"Kiosk-e2e": {
"root": "e2e/",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "Kiosk:serve"
}
}
}
}
},
"cli": {
"analytics": false
}
}
Example Post:
saveRecipe(recipe: IRecipe): Observable<number> {
this.convertRecipeToMetric(recipe);
return this.http
.post<number>(this.createCompleteRoute("api/Recipe/AddRecipe", this.envUrl.urlAddress),
recipe,
{ withCredentials: true }).pipe(map((s: number) => { return s; })
);
}
private createCompleteRoute = (route: string, envAddress: string) => {
return `${envAddress}/${route}`;
};
package.json - script section:
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.config.json",
"build": "ng build",
"build:ssr": "ng run Kiosk:server:dev",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
C# Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(
"AllowMyOrigins",
builder =>
builder
.AllowCredentials()
.WithOrigins("https://localhost:4200")
.SetIsOriginAllowed(host => true)
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod());
});
services.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
});
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseCors("AllowMyOrigins");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
"default",
"{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer("start");
}
});
}
The IP address could be anything as there would be many of these units, placed anywhere, so I need the IP address dynamic, not static.
</details>
# 答案1
**得分**: 1
如果您正在使用nginx作为入口代理,则前端代码中不应使用绝对路径。
如果您使用相对路径从JavaScript中调用API(即"/api/something"),则浏览器将访问您用于加载前端的服务器(可能是一个访问nginx代理的服务器URL)。
<details>
<summary>英文:</summary>
If you're using nginx as an ingress proxy then you shouldn't use absolute paths in your front-end code.
If you use relative paths to call the API (i.e. "/api/something") from javascript then the browser will hit whatever server you're using to load the FE (which presumably is a server URL that hits the nginx proxy?)
</details>
# 答案2
**得分**: -1
问题似乎是Angular客户端无法向运行在本地主机上的单独C#后端API发出请求。这可能是由于CORS(跨域资源共享)限制引起的,这些限制阻止网页向不同于提供页面的域的域发出请求。
要解决此问题,您需要配置C#后端以允许来自Angular客户端域的跨域请求。一种方法是通过向C#后端添加CORS中间件来实现这一点。以下是如何使用Microsoft.AspNetCore.Cors包执行此操作的示例:
1. 使用NuGet安装Microsoft.AspNetCore.Cors包。
2. 在C#后端的Startup.cs文件中,在ConfigureServices方法中添加以下代码以配置CORS:
```csharp
services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
这将允许任何来源发出请求到您的API,并允许任何HTTP方法和标头。
- 在Startup.cs的Configure方法中,添加以下代码以启用CORS:
app.UseCors();
这将为应用程序中的所有端点启用CORS。
通过这些更改,您的C#后端现在应该允许来自Angular客户端域的请求。请注意,应谨慎允许任何来源发出请求到您的API,因为这可能存在安全风险。通常建议限制允许发出请求到您的API的域。
英文:
The issue appears to be that the Angular client is unable to make requests to an API hosted on a separate C# backend that is running on localhost. This is likely due to CORS restrictions, which prevent web pages from making requests to a different domain than the one that served the page.
To fix this issue, you will need to configure the C# backend to allow cross-origin requests from the Angular client's domain. One way to do this is by adding CORS middleware to the C# backend. Here's an example of how to do this using the Microsoft.AspNetCore.Cors package:
Install the Microsoft.AspNetCore.Cors package using NuGet.
In your C# backend's Startup.cs file, add the following code to the ConfigureServices method to configure CORS:
services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
This will allow any origin to make requests to your API and allow any HTTP method and headers.
In the Configure method of Startup.cs, add the following code to enable CORS:
app.UseCors();
This will enable CORS for all endpoints in your application.
With these changes, your C# backend should now allow requests from the Angular client's domain. Note that you should be cautious about allowing any origin to make requests to your API, as this can pose a security risk. It's generally a good idea to limit the origins that are allowed to make requests to your API.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论