如何通过 Swashbuckle 使 SwaggerUI 在 traefik 代理后面的 docker 容器中工作?
How to make SwaggerUI via Swashbuckle work in a docker container behind a traefik proxy?
我有一个简单的 .NET 核心/.NET 5 API returns 一些数据通过 OData 和一些数据通过“传统”端点,尽管所有数据都在继承 ODataController 的同一控制器中。
当我从 Visual Studio 2019 年开始使用 IIS Express 并点击 http:///dev-machine/swagger 时,我得到了 API 的预期 SwaggerUI 输出。
如果我将它部署到 docker 容器,我仍然可以到达我的 API 端点和 /swagger/v1/swagger.json 以及位于 /swagger 的 SwaggerUI .
一旦我把那个东西放在 traefik 路由器后面,我就无法再访问 /swagger,但是 /swagger/v1/swagger。json 仍然有效。
我在这里不知所措,不知道从哪里开始 - 所以:如果有人能指出正确的方向,我将不胜感激!
我的docker-compose.yaml项目:
version: '3'
services:
traefik_msstockentries:
restart: always
image: "traefik"
container_name: "traefik_msstockentries"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "9016:80"
- "8016:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
default:
ipv4_address: 192.168.239.254
msstockentries:
restart: always
container_name: "msstockentries"
image: "msstockentries:dev"
environment:
- 'ASPNETCORE_ENVIRONMENT=Development'
- 'TZ=Europe/Berlin'
labels:
- "traefik.enable=true"
- "traefik.http.routers.msstockentries.rule=Host(`msstockentries`)"
- "traefik.http.routers.msstockentries.entrypoints=web"
networks:
- default
depends_on:
- traefik_msstockentries
networks:
default:
driver: bridge
ipam:
config:
- subnet: 192.168.239.0/24
我的 Dockerfile(非常简单...)
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM base AS final
WORKDIR .
COPY /publish/MS.StockEntries .
ENTRYPOINT ["dotnet", "MS.StockEntries.dll"]
我的Startup.cs
public class Startup
{
public IConfiguration Configuration { get; }
public static readonly LoggerFactory loggerFactory = new LoggerFactory(new[]
{
new DebugLoggerProvider()
});
private readonly ILogger _logger = loggerFactory.CreateLogger("StartupLogger");
public Startup(IWebHostEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
this.Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MSStockEntriesConfig>(Configuration.GetSection("MSStockEntriesConfig"));
MSStockEntriesConfig config = new MSStockEntriesConfig();
Configuration.GetSection("MSStockEntriesConfig").Bind(config);
// EdgeBlood Datenbank-Context
services.AddDbContext<EdgeBloodDbContext>(optionsBuilder =>
{
optionsBuilder
//.UseLoggerFactory(loggerFactory)
.UseOracle(config.xxx, oracleOptions =>
{
oracleOptions.UseOracleSQLCompatibility("11");
})
//.EnableSensitiveDataLogging()
;
});
services.AddControllers();
services.AddOData();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "MS.StockEntries",
Version = "v1",
Description = "Get items on stock",
Contact = new OpenApiContact
{
Name = "Team xxx",
Email = "xxx@yyy.com"
}
});
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
// Use method name as operationId
c.CustomOperationIds(apiDesc =>
{
return apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null;
});
});
SetOutputFormatters(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers(); endpoints.Expand().Count().MaxTop(null).SkipToken().OrderBy().Filter();
// Set OData-Route
endpoints.MapODataRoute("OData", "OData", GetEdmModel());
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Activate Swagger
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "MS.StockEntries v1");
});
// Http-Redirection
app.UseHttpsRedirection();
}
/// <summary>
/// Create EdmModel for OData
/// </summary>
/// <returns>EdmModel für OData</returns>
private IEdmModel GetEdmModel()
{
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<StockEntry>("StockEntries");
return odataBuilder.GetEdmModel();
}
/// <summary>
/// Use swagger with ODataControllers
/// </summary>
/// <param name="services"></param>
private static void SetOutputFormatters(IServiceCollection services)
{
services.AddMvcCore(options =>
{
IEnumerable<ODataOutputFormatter> outputFormatters =
options.OutputFormatters.OfType<ODataOutputFormatter>()
.Where(formatter => formatter.SupportedMediaTypes.Count == 0);
IEnumerable<ODataInputFormatter> inputFormatters =
options.InputFormatters.OfType<ODataInputFormatter>()
.Where(formatter => formatter.SupportedMediaTypes.Count == 0);
foreach (var outputFormatter in outputFormatters)
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
foreach (var inputFormatter in inputFormatters)
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
});
}
}
我的 traefik 配置:
http:
routers:
router-msstockentriesping:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/ping`)"
service: service-msstockentries
middlewares:
- "msstockentriesping-stripprefix"
- "msstockentries-header"
- "msstockentriesping-addendpoint"
router-msstockentriesswagger:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/index.html`)"
service: service-msstockentries
middlewares:
- "msstockentriesswagger-stripprefix"
- "msstockentries-header"
- "msstockentriesswagger-addendpoint"
router-msstockentriesswaggerdesc:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1/swagger.json`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerdesc-stripprefix"
- "msstockentries-header"
- "msstockentriesswaggerdesc-addendpoint"
router-msstockentries:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries`)"
service: service-msstockentries
middlewares:
- "msstockentries-stripprefix"
- "msstockentries-header"
- "msstockentries-addendpoint"
middlewares:
msstockentries-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries"
msstockentriesping-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries/ping"
msstockentriesswagger-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries/swagger/index.html"
msstockentriesswaggerdesc-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries/swagger/v1/swagger.json"
msstockentries-header:
headers:
customRequestHeaders:
Host: "msstockentries"
msstockentriesping-addendpoint:
addPrefix:
prefix: "/StockEntries/Ping"
msstockentries-addendpoint:
addPrefix:
prefix: "/OData/StockEntries"
msstockentriesswagger-addendpoint:
addPrefix:
prefix: "/swagger/index.html"
msstockentriesswaggerdesc-addendpoint:
addPrefix:
prefix: "/swagger/v1/swagger.json"
services:
service-msstockentries:
loadBalancer:
passHostHeader: true
servers:
- url: 'http://zzz:9016'
终于找到我的错误了...
我最终为 SwaggerUI 所需的文件定义了明确的 Traefik 路由器和相关规则,并详细说明了 SwaggerEndpoint(在 app.UseSwaggerUI())。现在一切正常。
traefik配置的相关部分:
http:
routers:
router-msstockentriesswagger:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger`)"
service: service-msstockentries
middlewares:
- "msstockentriesswagger-addendpoint"
router-msstockentriesswaggercss:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui.css`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggercss-addendpoint"
router-msstockentriesswaggerbundle:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-bundle.js`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerbundle-addendpoint"
router-msstockentriesswaggerpreset:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-standalone-preset.js`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerpreset-addendpoint"
router-msstockentriesswaggerdesc:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerv1-addendpoint"
middlewares:
msstockentriesswagger-addendpoint:
addPrefix:
prefix: "/swagger/index.html"
msstockentriesswaggercss-addendpoint:
addPrefix:
prefix: "/swagger/swagger-ui.css"
msstockentriesswaggerbundle-addendpoint:
addPrefix:
prefix: "/swagger/swagger-ui-bundle.js"
msstockentriesswaggerpreset-addendpoint:
addPrefix:
prefix: "/swagger/swagger-ui-standalone-preset.js"
msstockentriesswaggerv1-addendpoint:
addPrefix:
prefix: "/swagger/v1/swagger.json"
Startup.cs的相关部分:
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "swagger";
c.SwaggerEndpoint($"{misApiServiceConfig.SwaggerUiEndpoint}", $"{misApiServiceConfig.SwaggerUiServicename} {misApiServiceConfig.SwaggerUIServiceversion}");
});
要使用的 SwaggerUIEndpoit 可以在 appsettings.json 中设置。
我有一个简单的 .NET 核心/.NET 5 API returns 一些数据通过 OData 和一些数据通过“传统”端点,尽管所有数据都在继承 ODataController 的同一控制器中。
当我从 Visual Studio 2019 年开始使用 IIS Express 并点击 http:///dev-machine/swagger 时,我得到了 API 的预期 SwaggerUI 输出。
如果我将它部署到 docker 容器,我仍然可以到达我的 API 端点和 /swagger/v1/swagger.json 以及位于 /swagger 的 SwaggerUI .
一旦我把那个东西放在 traefik 路由器后面,我就无法再访问 /swagger,但是 /swagger/v1/swagger。json 仍然有效。
我在这里不知所措,不知道从哪里开始 - 所以:如果有人能指出正确的方向,我将不胜感激!
我的docker-compose.yaml项目:
version: '3'
services:
traefik_msstockentries:
restart: always
image: "traefik"
container_name: "traefik_msstockentries"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "9016:80"
- "8016:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
default:
ipv4_address: 192.168.239.254
msstockentries:
restart: always
container_name: "msstockentries"
image: "msstockentries:dev"
environment:
- 'ASPNETCORE_ENVIRONMENT=Development'
- 'TZ=Europe/Berlin'
labels:
- "traefik.enable=true"
- "traefik.http.routers.msstockentries.rule=Host(`msstockentries`)"
- "traefik.http.routers.msstockentries.entrypoints=web"
networks:
- default
depends_on:
- traefik_msstockentries
networks:
default:
driver: bridge
ipam:
config:
- subnet: 192.168.239.0/24
我的 Dockerfile(非常简单...)
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM base AS final
WORKDIR .
COPY /publish/MS.StockEntries .
ENTRYPOINT ["dotnet", "MS.StockEntries.dll"]
我的Startup.cs
public class Startup
{
public IConfiguration Configuration { get; }
public static readonly LoggerFactory loggerFactory = new LoggerFactory(new[]
{
new DebugLoggerProvider()
});
private readonly ILogger _logger = loggerFactory.CreateLogger("StartupLogger");
public Startup(IWebHostEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
this.Configuration = builder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MSStockEntriesConfig>(Configuration.GetSection("MSStockEntriesConfig"));
MSStockEntriesConfig config = new MSStockEntriesConfig();
Configuration.GetSection("MSStockEntriesConfig").Bind(config);
// EdgeBlood Datenbank-Context
services.AddDbContext<EdgeBloodDbContext>(optionsBuilder =>
{
optionsBuilder
//.UseLoggerFactory(loggerFactory)
.UseOracle(config.xxx, oracleOptions =>
{
oracleOptions.UseOracleSQLCompatibility("11");
})
//.EnableSensitiveDataLogging()
;
});
services.AddControllers();
services.AddOData();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "MS.StockEntries",
Version = "v1",
Description = "Get items on stock",
Contact = new OpenApiContact
{
Name = "Team xxx",
Email = "xxx@yyy.com"
}
});
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
// Use method name as operationId
c.CustomOperationIds(apiDesc =>
{
return apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null;
});
});
SetOutputFormatters(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers(); endpoints.Expand().Count().MaxTop(null).SkipToken().OrderBy().Filter();
// Set OData-Route
endpoints.MapODataRoute("OData", "OData", GetEdmModel());
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Activate Swagger
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "MS.StockEntries v1");
});
// Http-Redirection
app.UseHttpsRedirection();
}
/// <summary>
/// Create EdmModel for OData
/// </summary>
/// <returns>EdmModel für OData</returns>
private IEdmModel GetEdmModel()
{
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<StockEntry>("StockEntries");
return odataBuilder.GetEdmModel();
}
/// <summary>
/// Use swagger with ODataControllers
/// </summary>
/// <param name="services"></param>
private static void SetOutputFormatters(IServiceCollection services)
{
services.AddMvcCore(options =>
{
IEnumerable<ODataOutputFormatter> outputFormatters =
options.OutputFormatters.OfType<ODataOutputFormatter>()
.Where(formatter => formatter.SupportedMediaTypes.Count == 0);
IEnumerable<ODataInputFormatter> inputFormatters =
options.InputFormatters.OfType<ODataInputFormatter>()
.Where(formatter => formatter.SupportedMediaTypes.Count == 0);
foreach (var outputFormatter in outputFormatters)
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
foreach (var inputFormatter in inputFormatters)
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
});
}
}
我的 traefik 配置:
http:
routers:
router-msstockentriesping:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/ping`)"
service: service-msstockentries
middlewares:
- "msstockentriesping-stripprefix"
- "msstockentries-header"
- "msstockentriesping-addendpoint"
router-msstockentriesswagger:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/index.html`)"
service: service-msstockentries
middlewares:
- "msstockentriesswagger-stripprefix"
- "msstockentries-header"
- "msstockentriesswagger-addendpoint"
router-msstockentriesswaggerdesc:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1/swagger.json`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerdesc-stripprefix"
- "msstockentries-header"
- "msstockentriesswaggerdesc-addendpoint"
router-msstockentries:
rule: "Host(`xxx.yyy.com`)&&Path(`/msstockentries`)"
service: service-msstockentries
middlewares:
- "msstockentries-stripprefix"
- "msstockentries-header"
- "msstockentries-addendpoint"
middlewares:
msstockentries-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries"
msstockentriesping-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries/ping"
msstockentriesswagger-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries/swagger/index.html"
msstockentriesswaggerdesc-stripprefix:
stripPrefix:
prefixes:
- "/msstockentries/swagger/v1/swagger.json"
msstockentries-header:
headers:
customRequestHeaders:
Host: "msstockentries"
msstockentriesping-addendpoint:
addPrefix:
prefix: "/StockEntries/Ping"
msstockentries-addendpoint:
addPrefix:
prefix: "/OData/StockEntries"
msstockentriesswagger-addendpoint:
addPrefix:
prefix: "/swagger/index.html"
msstockentriesswaggerdesc-addendpoint:
addPrefix:
prefix: "/swagger/v1/swagger.json"
services:
service-msstockentries:
loadBalancer:
passHostHeader: true
servers:
- url: 'http://zzz:9016'
终于找到我的错误了...
我最终为 SwaggerUI 所需的文件定义了明确的 Traefik 路由器和相关规则,并详细说明了 SwaggerEndpoint(在 app.UseSwaggerUI())。现在一切正常。
traefik配置的相关部分:
http:
routers:
router-msstockentriesswagger:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger`)"
service: service-msstockentries
middlewares:
- "msstockentriesswagger-addendpoint"
router-msstockentriesswaggercss:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui.css`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggercss-addendpoint"
router-msstockentriesswaggerbundle:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-bundle.js`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerbundle-addendpoint"
router-msstockentriesswaggerpreset:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger-ui-standalone-preset.js`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerpreset-addendpoint"
router-msstockentriesswaggerdesc:
rule: "HostRegexp(`xxx.yyy.com`)&&Path(`/msstockentries/swagger/v1`)"
service: service-msstockentries
middlewares:
- "msstockentriesswaggerv1-addendpoint"
middlewares:
msstockentriesswagger-addendpoint:
addPrefix:
prefix: "/swagger/index.html"
msstockentriesswaggercss-addendpoint:
addPrefix:
prefix: "/swagger/swagger-ui.css"
msstockentriesswaggerbundle-addendpoint:
addPrefix:
prefix: "/swagger/swagger-ui-bundle.js"
msstockentriesswaggerpreset-addendpoint:
addPrefix:
prefix: "/swagger/swagger-ui-standalone-preset.js"
msstockentriesswaggerv1-addendpoint:
addPrefix:
prefix: "/swagger/v1/swagger.json"
Startup.cs的相关部分:
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "swagger";
c.SwaggerEndpoint($"{misApiServiceConfig.SwaggerUiEndpoint}", $"{misApiServiceConfig.SwaggerUiServicename} {misApiServiceConfig.SwaggerUIServiceversion}");
});
要使用的 SwaggerUIEndpoit 可以在 appsettings.json 中设置。