【发布时间】:2021-05-11 15:34:30
【问题描述】:
我正在尝试在我的 Swagger 规范和 SwaggerUI 中包含安全标头信息,因为我希望第 3 方系统能够轻松使用我的 API,以及能够使用 Swagger CodeGen 和 NSwag 等 CodeGen 工具来生成客户端库来自它。
我正在使用 Swashbuckle / SwaggerGen 在运行时自动生成文档。 我的 API 使用不记名令牌身份验证,我希望我的 SwaggerDocs 反映我的身份验证方案。我还可以在我的 SwaggerDocs 中看到所需的安全标头信息,并且客户端能够在尝试 API 调用时通过 SwaggerUI 测试身份验证。
但是,我无法成功地将我的安全标头信息显示在我自己的解决方案中。
这是我目前在调试期间通过本地 SwaggerUI 查看 SwaggerDocs 时看到的内容:
请注意,生成的 OAS3 标记在通过https://editor.swagger.io/ 运行时会正确呈现
这是由 Swashbuckle / SwaggerGen 生成的实际标记:
{
"openapi": "3.0.1",
"info": {
"title": "MyDemo Host API v1",
"version": "v1"
},
"paths": {
"/api/services/app/Tenant/CreateTenant": {
"post": {
"tags": [
"Tenant"
],
"operationId": "ApiServicesAppTenantCreatetenantPost",
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/CreateTenantInput"
}
]
}
},
"application/json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/CreateTenantInput"
}
]
}
},
"text/json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/CreateTenantInput"
}
]
}
},
"application/*+json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/CreateTenantInput"
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/SwaggerDocResponseWrapper"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/SwaggerDocResponseWrapper"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/SwaggerDocResponseWrapper"
}
}
}
}
},
"security": [
{
"bearer": []
}
]
}
}
},
"components": {
"schemas": {
"CreateTenantInput": {
"required": [
"adminEmailAddress",
"name",
"tenancyName"
],
"type": "object",
"properties": {
"tenancyName": {
"maxLength": 64,
"minLength": 0,
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]{1,}$",
"type": "string"
},
"name": {
"maxLength": 128,
"minLength": 0,
"type": "string"
},
"adminEmailAddress": {
"maxLength": 256,
"minLength": 0,
"type": "string",
"format": "email"
},
"adminPassword": {
"maxLength": 128,
"minLength": 0,
"type": "string",
"nullable": true
},
"connectionString": {
"maxLength": 1024,
"type": "string",
"nullable": true
},
"shouldChangePasswordOnNextLogin": {
"type": "boolean"
},
"sendActivationEmail": {
"type": "boolean"
},
"editionId": {
"type": "integer",
"format": "int32",
"nullable": true
},
"isActive": {
"type": "boolean"
},
"subscriptionEndDateUtc": {
"type": "string",
"format": "date-time",
"nullable": true
},
"isInTrialPeriod": {
"type": "boolean"
},
"onSellingPartnerId": {
"type": "integer",
"format": "int32",
"nullable": true
},
"onSellingPartner": {
"allOf": [
{
"$ref": "#/components/schemas/OnSellingPartnerDto"
}
],
"nullable": true
},
"contactPersonFirstName": {
"maxLength": 32,
"type": "string",
"nullable": true
},
"contactPersonLastName": {
"maxLength": 32,
"type": "string",
"nullable": true
},
"contactNumber": {
"maxLength": 24,
"type": "string",
"nullable": true
},
"contactEmail": {
"maxLength": 256,
"type": "string",
"nullable": true
},
"taxNumber": {
"maxLength": 24,
"type": "string",
"nullable": true
},
"registeredName": {
"maxLength": 256,
"type": "string",
"nullable": true
},
"tenantBillingAddress": {
"allOf": [
{
"$ref": "#/components/schemas/TenantBillingAddressInput"
}
],
"nullable": true
}
},
"additionalProperties": false
},
"ValidationError": {
"type": "object",
"properties": {
"message": {
"type": "string",
"nullable": true
},
"members": {
"type": "array",
"items": {
"type": "string"
},
"nullable": true
}
},
"additionalProperties": false
},
"SwaggerDocResponseWrapper": {
"type": "object",
"properties": {
"result": {
"type": "string",
"nullable": true
},
"targetUrl": {
"type": "string",
"nullable": true
},
"success": {
"type": "boolean"
},
"error": {
"allOf": [
{
"$ref": "#/components/schemas/ResponseError"
}
],
"nullable": true
},
"unauthorizedRequest": {
"type": "boolean"
},
"__Abp": {
"type": "boolean"
}
},
"additionalProperties": false
},
"OnSellingPartnerDto": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"isActive": {
"type": "boolean"
},
"registeredName": {
"type": "string",
"nullable": true
},
"taxNumber": {
"type": "string",
"nullable": true
},
"contactNumber": {
"type": "string",
"nullable": true
},
"contactPersonFirstName": {
"type": "string",
"nullable": true
},
"contactPersonLastName": {
"type": "string",
"nullable": true
},
"contactEmail": {
"type": "string",
"nullable": true
},
"id": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
},
"TenantBillingAddressInput": {
"type": "object",
"properties": {
"streetAddress": {
"maxLength": 256,
"minLength": 0,
"type": "string",
"nullable": true
},
"region": {
"maxLength": 64,
"minLength": 0,
"type": "string",
"nullable": true
},
"city": {
"maxLength": 64,
"minLength": 0,
"type": "string",
"nullable": true
},
"countryId": {
"type": "integer",
"format": "int32",
"nullable": true
},
"regionCode": {
"maxLength": 6,
"minLength": 0,
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"ResponseError": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string",
"nullable": true
},
"details": {
"type": "string",
"nullable": true
},
"validationErrors": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"nullable": true
}
},
"additionalProperties": false
}
},
"securitySchemes": {
"bearer": {
"type": "http",
"description": "Specify the authorization token.",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"security": [
{}
]
}
环境:
- AspNetZero Core MVC & JQuery v10.3.0 (.Net 5)
- Abp (6.2.0)
- Swashbuckle.AspNetCore (5.6.3) 和 Swashbuckle.AspNetCore.NewtonSoft (6.1.4)
从Startup.cs 中的ConfiguredServices 方法调用以下代码:
public override void InstallServices(IHostEnvironment hostEnvironment, IServiceCollection services, IConfiguration configuration)
{
if (WebConsts.SwaggerUiEnabled)
{
//Swagger - Enable this line and the related lines in Configure method to enable swagger UI
services.AddSwaggerGen(options =>
{
options.SwaggerDoc(ApiNames.HostApiv1, new OpenApiInfo { Title = ApiTitles.HostApiv1, Version = "v1" });
options.SwaggerDoc(ApiNames.PartnerApiv1, new OpenApiInfo { Title = ApiTitles.PartnerApiv1, Version = "v1" });
options.SwaggerDoc(ApiNames.TenantApiv1, new OpenApiInfo { Title = ApiTitles.TenantApiv1, Version = "v1" });
OpenApiSecurityScheme securityDefinition = new OpenApiSecurityScheme()
{
Name = "Bearer",
BearerFormat = "JWT",
Scheme = "bearer",
Description = "Specify the authorization token.",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
};
OpenApiSecurityRequirement securityRequirements = new OpenApiSecurityRequirement()
{
{securityDefinition, new string[] { }},
};
options.AddSecurityDefinition("bearer", securityDefinition);
// Make sure swagger UI requires a Bearer token to be specified
options.AddSecurityRequirement(securityRequirements);
options.DocInclusionPredicate((docName, apiDesc) =>
{
if (!apiDesc.ActionDescriptor.IsControllerAction())
{
return false;
}
apiDesc.TryGetMethodInfo(out MethodInfo methodInfo);
var actionDocs = methodInfo.GetCustomAttributes<SwaggerDocAttribute>()
.SelectMany(a => a.IncludeInDocuments);
var controllerDocs = methodInfo.DeclaringType.GetCustomAttributes<SwaggerDocAttribute>()
.SelectMany(a => a.IncludeInDocuments);
switch (docName)
{
case ApiNames.HostApiv1:
return apiDesc.GroupName == null ||
actionDocs.Contains(ApiNames.HostApiv1) ||
controllerDocs.Contains(ApiNames.HostApiv1);
case ApiNames.PartnerApiv1:
return apiDesc.GroupName == null ||
actionDocs.Contains(ApiNames.PartnerApiv1) ||
controllerDocs.Contains(ApiNames.PartnerApiv1);
case ApiNames.TenantApiv1:
return apiDesc.GroupName == null ||
actionDocs.Contains(ApiNames.TenantApiv1) ||
controllerDocs.Contains(ApiNames.TenantApiv1);
default:
return true;
}
});
options.IgnoreObsoleteActions();
options.IgnoreObsoleteProperties();
options.OrderActionsBy((apiDesc) => $"{apiDesc.RelativePath}");
options.ParameterFilter<SwaggerEnumParameterFilter>();
options.SchemaFilter<SwaggerEnumSchemaFilter>();
options.OperationFilter<SwaggerOperationIdFilter>();
options.OperationFilter<SwaggerOperationFilter>();
options.CustomDefaultSchemaIdSelector();
options.UseAllOfToExtendReferenceSchemas();
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
}).AddSwaggerGenNewtonsoftSupport();
}
}
更多背景信息:
我将我的 API 文档生成到单独的 SwaggerDocs 中,用于我的 API 的不同类型的消费者(内部/主机应用程序;合作伙伴应用程序;普通租户/客户端应用程序)。 TI 使用SwaggerDocsAttribute 装饰我的 ApplicationServices(在运行时由 ABP/AspNetZero 动态提供类似 REST 的服务),用于描述应将其包含在其文档中的一个或多个 Swagger 文档。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class SwaggerDocAttribute: Attribute
{
public SwaggerDocAttribute(params string[] includeInDocuments)
{
IncludeInDocuments = includeInDocuments;
}
public string[] IncludeInDocuments { get; }
}
示例用法:
[SwaggerDoc(ApiNames.HostApiv1, ApiNames.PartnerApiv1, ApiNames.TenantApiv1)]
public class SystemStatusAppService: MyDemoAppServiceBase, ISystemStatusAppService
{
[ProducesResponseType(200, Type = typeof(SwaggerDocResponseWrapper))]
public async Task Ping()
{
//Do nothing - will return status code 200
}
[AbpAuthorize()]
[ProducesResponseType(200, Type = typeof(SwaggerDocResponseWrapper))]
public async Task PingWithAuth()
{
//Do nothing - will return status code 200
}
}
【问题讨论】:
-
其实刮一下我之前的评论,生成的spec确实有一些错误,你粘贴到editor.swagger.io就可以看到。具体来说,安全方案名称不匹配 -
components/securitySchemes部分中的方案命名为jwt_auth,但操作引用security: - bearer: []。两个地方的名称必须相同(“jwt_auth”或“bearer”)。我认为将options.AddSecurityDefinition("jwt_auth", securityDefinition);中的名称更改为"bearer"应该可以解决问题。 -
谢谢 - 我要试试。对此仍然很陌生,找到最新且准确的文档一直是一项挑战!
-
@Helen 我已经进行了建议的更改,是的,OpenApi 规范现在似乎已正确生成,并且在 editor.swagger.io 上现在可以正确呈现......我能够看到“授权”按钮和提示。但是,这仍然没有在我的本地实现上正确呈现。
标签: asp.net-core-mvc swagger swagger-ui aspnetboilerplate swashbuckle.aspnetcore