刚参加工作时,写个API接口,还要写API文档,再使用PostMan测试接口,写文档的时间比写接口还要折腾。后来接触Swagger,API文档的工作得到了很大的改善,不但可以自动构建交互式API说明文档,还能直接调试API接口。今天记录下Core项目下使用Swagger,最新版的Swagger已经完美支持Open Api规范及JWT Token授权访问等。使用Swagger的好处总结如下:
- 使用 Swagger 生成精美的API接口文档
- 使用 Swagger 调试JWT授权接口
- 使用 Swagger 生成各个类库中视图模型的描述
二、配置Swagger服务
1、引用Nuget包
新建一个.NET5 Web Api 项目,打开Nuget安装管理器,搜索Swashbuckle.AspNetCore,安装即可
2、配置服务
我们打开Startup.cs文件,来对Swagger配置进行一些必要的配置,在ConfigureServices方法我们添加一下Swagger配置:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//配置swagger服务
services.AddSwaggerService();
}
自定义的扩展方法AddSwaggerService:
using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.Filters; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; namespace CommonCore.Helpers { /// <summary> /// Swagger 启动服务 /// </summary> static partial class Extention { public static void AddSwaggerService(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services));
//自定义的API名称 var apiName = AppsettingsMap.ApiName; services.AddSwaggerGen(c => {
//自定义的API版本 string version = AppsettingsMap.Version;
//swagger文档 文档一 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module1_Name)) {
//文档一 标识名称 c.SwaggerDoc(AppsettingsMap.ProjectModule_Module1_Name, new OpenApiInfo { Version = version, Title = $"{apiName}—{RuntimeInformation.FrameworkDescription}", Description = $"{ AppsettingsMap.ProjectModule_Module1_Desc }" }); }
//swagger文档 文档二 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module2_Name)) { c.SwaggerDoc(AppsettingsMap.ProjectModule_Module2_Name, new OpenApiInfo { Version = version, Title = $"{apiName}—{RuntimeInformation.FrameworkDescription}", Description = $"{ AppsettingsMap.ProjectModule_Module2_Desc }" }); }
//swagger文档 文档三 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module3_Name)) { c.SwaggerDoc(AppsettingsMap.ProjectModule_Module3_Name, new OpenApiInfo { Version = version, Title = $"{apiName}—{RuntimeInformation.FrameworkDescription}", Description = $"{ AppsettingsMap.ProjectModule_Module3_Desc }" }); }
//swagger文档 文档四 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module4_Name)) { c.SwaggerDoc(AppsettingsMap.ProjectModule_Module4_Name, new OpenApiInfo { Version = version, Title = $"{apiName}—{RuntimeInformation.FrameworkDescription}", Description = $"{ AppsettingsMap.ProjectModule_Module4_Desc }" }); } c.OrderActionsBy(o => o.RelativePath); //解决方法重载导致文档报错 c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); //启用注释nuget包 c.EnableAnnotations(); // 开启加权小锁 c.OperationFilter<AddResponseHeadersFilter>(); c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>(); // 在header中添加token,传递到后台 c.OperationFilter<SecurityRequirementsOperationFilter>(); // ids4和jwt切换 if ((bool)AppsettingsMap.Service_Ids4_Enabled) { c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, Description = $"备注:关于IDS4认证服务,请查阅{ AppsettingsMap.Service_Ids4_AuthorizationUrl }/doc/apidoc", Flows = new OpenApiOAuthFlows { //Implicit = new OpenApiOAuthFlow //{ // AuthorizationUrl = new Uri($"{AppsettingsMap.Service_Ids4_AuthorizationUrl}/connect/authorize"), // Scopes = new Dictionary<string, string> { // { // ApiName,AppsettingsMap.Service_Ids4_Scopes // } // } //}, ClientCredentials = new OpenApiOAuthFlow { TokenUrl = new Uri($"{AppsettingsMap.Service_Ids4_AuthorizationUrl}/connect/token"), Scopes = new Dictionary<string, string> { { AppsettingsMap.ApiName,AppsettingsMap.Service_Ids4_ClientId } } } } }); } else { // Jwt Bearer 认证 c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Name = "Authorization",//jwt默认的参数名称 In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) Type = SecuritySchemeType.ApiKey, Description = "Authorization:Bearer {your JWT token},注意两者之间是一个空格", }); } //使用新的swagger注释,则不需要配置XML 就不需要下面的代码了 // 为 Swagger JSON and UI设置xml文档注释路径 // 获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径) var basePath = AppContext.BaseDirectory; var xmls = Directory.GetFiles(basePath, "*.xml"); xmls.ForEach(aXml => { c.IncludeXmlComments(aXml); }); }); } } }
这里设置了四个文档,可以理解为四个模块,如果接口全部展示在一个文档中,接口太多会显得太乱,所以根据业务模块或者其他依据将接口分列在不同模块中,下面配置swagger相关的中间件,具体效果等下看截图:
然后我们在Configure方法里添加swagger中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// 启用Swagger展示中间件
app.UseSwaggerUIEx();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
swagger中间件扩展方法代码如下:
using Microsoft.AspNetCore.Builder; using System; namespace CommonCore.Helpers { public static partial class Extention { public static IApplicationBuilder UseSwaggerUIEx(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); app.UseSwagger(); app.UseSwaggerUI(c => { string version = AppsettingsMap.Version;
//对应上边的swagger文档一 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module1_Name)) { c.SwaggerEndpoint($"/swagger/{AppsettingsMap.ProjectModule_Module1_Name}/swagger.json", $"{AppsettingsMap.ProjectModule_Module1_Desc}"); }
//对应上边的swagger文档二 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module2_Name)) { c.SwaggerEndpoint($"/swagger/{AppsettingsMap.ProjectModule_Module2_Name}/swagger.json", $"{AppsettingsMap.ProjectModule_Module2_Desc}"); }
//对应上边的swagger文档三 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module3_Name)) { c.SwaggerEndpoint($"/swagger/{AppsettingsMap.ProjectModule_Module3_Name}/swagger.json", $"{AppsettingsMap.ProjectModule_Module3_Desc}"); }
//对应上边swagger文档四 if (!string.IsNullOrWhiteSpace(AppsettingsMap.ProjectModule_Module4_Name)) { c.SwaggerEndpoint($"/swagger/{AppsettingsMap.ProjectModule_Module4_Name}/swagger.json", $"{AppsettingsMap.ProjectModule_Module4_Desc}"); } //是否启用ids4 if ((bool)AppsettingsMap.Service_Ids4_Enabled) { c.OAuthClientId($"{AppsettingsMap.Service_Ids4_ClientId}"); } //路径配置,设置为空,表示直接在根域名访问该文件 c.RoutePrefix = string.Empty; }); return app; } } }
到了这里还不能算结束,为啥呢?正常来说应该是结束了,但是呢我们上边对swagger文档分列了四份,不同的接口展示在不同的文档中,怎么做到的呢?其实很简单在接口上加上文档的标识即可,比方说我们将TestController中方法显示在文档二中,那么将文档二的标识AppsettingsMap.ProjectModule_Module2_Name(我这里是封装的,其实就是读取配置文件)加到控制器上,如下:代码中的“crm_other”就是AppsettingsMap.ProjectModule_Module2_Name的文案,
using CommonCore.Helpers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Serilog; using System; using System.Collections.Generic; namespace CrmRedevelop.Controllers.CrmOther { [ApiExplorerSettings(GroupName = "crm_other")] public class TestController : BaseApiController { private readonly ILogger _logger; public TestController(ILogger logger) { _logger = logger; } [HttpGet] public ContentResult Get([FromServices] ILogger logger) { List<string> testList = null; if (testList.Count > 0) logger.Information("利用特性FromServices注入服务"); var result = ApiResponseHelper.GetSuccess(); return result; } [HttpGet("{id}")] public string Get(int id) { _logger.Information("构造函数注入"); return "value"; } [HttpPost] public ContentResult Post([FromBody] string value) { var logger = HttpContext.RequestServices.GetRequiredService<ILogger>(); logger.Information("GetRequiredService来检索服务"); return ApiResponseHelper.GetSuccess("hello"); } [HttpGet] public ContentResult Unix() { return ApiResponseHelper.GetSuccess(DateTime.Now.Unix13TimeStamp()); } } }
这样swagger基本配置就完成了,看下效果:
3、忽略注释警告
F5运行项目后发现有很多警告:
原因是是swagger把一些 action 方法都通过xml文件配置了,如果没有加入相应的注释就会警告。如果不想每一个方法都加注释,可以如下配置:选择项目,右键属性,选择生成菜单,加入;1591
4、为接口添加注释(XML配置)
右键项目名称=>属性=>生成,勾选“输出”下面的“xml文档文件”,系统会默认生成一个xml。
此时项目中会生成一个 Core.Api.xml文件,修改该文件属性,选择较新则复制。上边swagger扩展方法AddSwaggerService中有下面代码,能够加载这些xml配置:
// 为 Swagger JSON and UI设置xml文档注释路径 // 获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径) var basePath = AppContext.BaseDirectory; var xmls = Directory.GetFiles(basePath, "*.xml"); xmls.ForEach(aXml => { c.IncludeXmlComments(aXml); });
然后action方法上加入注释:我们常见的就是使用<summary>相关来配置注释,如下面:
/// <summary> /// 获取数据 /// </summary> /// <returns></returns> [HttpGet] public IEnumerable<WeatherForecast> Get() { 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(); }
看下效果:
5、为Model 添加注释(XML配置)
新建一个model,加入字段注释
public class StudentModel
{
/// <summary>
/// 标识
/// </summary>
public int ID { get; set; }
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }
}
为model所在的项目也要配置xml,类似于上述第三步
//注入model 的xml
var xmlModelPath = Path.Combine(basePath, "xx.Model.xml");//这个就是Model层的xml文件名 c.IncludeXmlComments(xmlModelPath);
控制器中引用:
/// <summary>
/// 插入学生信息
/// </summary>
/// <param name="studentModel">model实体类参数</param>
/// <returns></returns>
[HttpPost]
public bool Insert([FromQuery] StudentModel studentModel)
{
return true;
}
效果如下:
6、新的注释方式(非XML方式)
上边4和5介绍了通过xml配置注释,现在呢,我们使用swagger提供的方式来配置注释:先看下接口:
[HttpGet]
[SwaggerOperation(Summary = "获取信息列表", Description = "这里可以是参数或者返回数据说明")]
[SwaggerResponse((int)HttpStatusCode.OK, "返回string数组", typeof(List<string>))]
public ContentResult Get([FromServices] ILogger logger)
{
List<string> testList = new List<string> { "apple", "cat" };
logger.Information("利用特性FromServices注入服务");
return ApiResponseHelper.GetSuccess(testList);
}
看下效果:
上边返回的是字符串数组,不需要特殊的说明,如果返回的是对象数组呢?需要对对象中的每个字段做个说明,应该怎么做呢?再来个实例:对上边的接口做个改造,返回对象数组:
[HttpGet]
[SwaggerOperation(Summary = "获取信息列表", Description = "这里可以是参数或者返回数据说明")]
[SwaggerResponse((int)HttpStatusCode.OK, "返回对象数组", typeof(List<ChannelResult>))]
public ContentResult Get([FromServices] ILogger logger)
{
logger.Information("利用特性FromServices注入服务");
List<ChannelResult> testList = new List<ChannelResult>();
testList.Add(new ChannelResult
{
CodeId=1,
Name="1",
Source="1",
Type="1",
Status=1,
Remark="1"
});
return ApiResponseHelper.GetSuccess(testList);
}
再来看下ChannelResult Model:
using Swashbuckle.AspNetCore.Annotations;
namespace CrmMode
{
public class ChannelResult
{
[SwaggerSchema("序号")]
public int CodeId { get; set; }
[SwaggerSchema("渠道名称")]
public string Name { get; set; }
[SwaggerSchema("渠道来源")]
public string Source { get; set; }
[SwaggerSchema("渠道来源类别")]
public string Type { get; set; }
[SwaggerSchema("状态")]
public int Status { get; set; }
[SwaggerSchema("备注")]
public string Remark { get; set; }
}
}
接下来看下效果:
这个方式呢,就不需要配置XML了,可以省去一些代码各有优缺点吧,根据自己情况使用。
7、隐藏接口
如果不想显示某些接口,直接在controller 上,或者action 上,增加特性可隐藏接口
[ApiExplorerSettings(IgnoreApi = true)]
8、Swagger启用API文档的JWT授权
目前很多网站都使用了JWT(JSON WEB TOKEN)来作为账户系统的认证授权,JWT以它的简单、高效、分布式优势很快成为了网站的流行验证方式。接下来我们为Swagger添加JWT授权认证,依旧打开Startup.cs文件,修改上面ConfigureServices方法中的代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Core.Api", Version = "v1" });
#region 加载xml 如果使用新的注释方式,则不需要配置XML了
// 为 Swagger JSON and UI设置xml文档注释路径
//获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径)
var basePath = AppContext.BaseDirectory;
var xmls = Directory.GetFiles(basePath, "*.xml");
foreach (var aXml in xmls)
{
c.IncludeXmlComments(aXml);
}
#endregion
#region jwt认证
// 开启加权小锁
c.OperationFilter<AddResponseHeadersFilter>();
c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
// 在header中添加token,传递到后台
c.OperationFilter<SecurityRequirementsOperationFilter>();
// Jwt Bearer 认证
c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Name = "Authorization",//jwt默认的参数名称
In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
Type = SecuritySchemeType.ApiKey,
Description = "Authorization:Bearer {your JWT token},注意两者之间是一个空格",
});
#endregion
});
}
预览一下授权设置,发现右侧多了一个Authorize绿色的带锁按钮,这个按钮点开后就可以设置我们的JWT Token信息了,格式是:Bearer 你的Token字符串,注意Bearer于Token之间有个空格。设置好Token后,你请求任意的API接口时,Swagger会自动附带Token到请求的Header中。
既然swagger中支持了jwt认证,那么我们在startup.cs中启用jwt认证:如下
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); #region 开启jwt认证 var symmetricKeyAsBase64 = "!@#123"; var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); var signingKey = new SymmetricSecurityKey(keyByteArray); services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, ValidateIssuer = true, ValidIssuer = "asd",//发行人 ValidateAudience = true, ValidAudience = "fdf",//订阅人 ValidateLifetime = true, ClockSkew = TimeSpan.Zero,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲 RequireExpirationTime = true, }; }); #endregion #region 配置swagger文档 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Core.Api", Version = "v1" }); #region 加载xml // 为 Swagger JSON and UI设置xml文档注释路径 //获取应用程序所在目录(绝对,不受工作目录影响,建议采用此方法获取路径) var basePath = AppContext.BaseDirectory; var xmls = Directory.GetFiles(basePath, "*.xml"); foreach (var aXml in xmls) { c.IncludeXmlComments(aXml); } #endregion #region jwt认证 // 开启加权小锁 c.OperationFilter<AddResponseHeadersFilter>(); c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>(); // 在header中添加token,传递到后台 c.OperationFilter<SecurityRequirementsOperationFilter>(); // Jwt Bearer 认证 c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Name = "Authorization",//jwt默认的参数名称 In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) Type = SecuritySchemeType.ApiKey, Description = "Authorization:Bearer {your JWT token},注意两者之间是一个空格", }); #endregion }); #endregion }
更详细关于jwt认证的这里就不细讲了
9、默认直接访问swagger
有时候打开webapi项目,希望直接打开swagger文档,有两种方法:
- 方法一:设置launchSettings.json文件中的launchUrl属性,指定为swagger,如下:
- 方法二我们可以通过RoutePrefix 属性设置。
app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Core.Api v1"); c.RoutePrefix = ""; //路径配置,设置为空,表示直接在根域名访问swagger文件 });
建议使用方法二。
三、Swagger异常汇总
1、问题一
如果出现如上图那样的错误,请打开swagger/v1/swagger.json查看具体原因。 常见的原因是action上没有加入HTTP请求协议,比如[HttpPost]
2、问题二
这是因为接口json文档定义和调用不是一个,请仔细对比下定义和调用的swagger名称是否一致。比如定义的时候名称为“v1”:
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Core.Api", Version = "v1" });
调用的时候也要保证一致:
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Core.Api v1"));
3、问题三
问题分析:出现该问题的原因是路由重载导致的,请确保不要存在多个一样的路由。路由重载是指出现了多个地址一样的接口(controller一样,action一样)。看了下代码,果然,同一个controller中写了重载的action。尽管返回类型和参数不一样,但是路由是一样的,所以swagger抛出了异常。
解决方法:
- 方法一:避免出现路由重载的情况
- 方法二:对swagger做策略,允许出现路由重载,但是只能默认取一个。
services.AddSwaggerGen(c => { string version = AppsettingsMap.Version; c.SwaggerDoc(AppsettingsMap.ProjectModule_CrmProof, new OpenApiInfo { Version = version, Title = $"{apiName}—{RuntimeInformation.FrameworkDescription}", Description = $"aa模块" }); c.OrderActionsBy(o => o.RelativePath); //解决方法重载导致文档报错 c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); ......... });
虽然解决了swagger报错的情况,但是重载的方法是无法同时显示的。
4、问题四
(1)问题描述
大前提:JWT验证。swagger请求报401错误,如下图:
(2)方法查找
第一种情况:看到上边的截图,容易想到的是没有填入Bearer空格{token},这是最简单的情况。当然我遇见的不是这种情况。这里就不赘述了。
第二种情况:查找jwttoken生成的方法和验证jwttoken的方法是不是不对应,比如是不是生成jwttoken的时候没有填写Issuer发行人,而验证的时候却要验证Issuer。这种问题归属于jwt生成和验证不统一的问题,我仔细比对过了,没问题。也不再赘述。
第三种情况:监控swagger的请求,看看有没有携带Bearer空格{token}到后台接口,看下面截图:
发现并没有发现authorization:xx。明白了,swagger虽然集成了JWT,但是没有将token传到后台接口,为什么呢?
(3)问题解决
上边找到了原因。这里解决问题:扩展方法AddSecurityDefinition的第一个参数文案请填写“oauth2”。这样就可以了。。。
services.AddSwaggerGen(c => { ......
......
// Jwt Bearer 认证 c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { Name = "Authorization",//jwt默认的参数名称 In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) Type = SecuritySchemeType.ApiKey, Description = "Authorization:Bearer {JwtToken},注意两者之间是一个空格", }); });
为什么一定要是“oauth2”?