刚参加工作时,写个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,安装即可

.NET5 引入Swagger

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基本配置就完成了,看下效果:

.NET5 引入Swagger

3、忽略注释警告

F5运行项目后发现有很多警告:

.NET5 引入Swagger

原因是是swagger把一些 action 方法都通过xml文件配置了,如果没有加入相应的注释就会警告。如果不想每一个方法都加注释,可以如下配置:选择项目,右键属性,选择生成菜单,加入;1591

.NET5 引入Swagger

4、为接口添加注释(XML配置)

右键项目名称=>属性=>生成,勾选“输出”下面的“xml文档文件”,系统会默认生成一个xml。

.NET5 引入Swagger

此时项目中会生成一个 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();
}

看下效果:

.NET5 引入Swagger

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;
}

效果如下:

.NET5 引入Swagger

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);
}

看下效果:

.NET5 引入Swagger

上边返回的是字符串数组,不需要特殊的说明,如果返回的是对象数组呢?需要对对象中的每个字段做个说明,应该怎么做呢?再来个实例:对上边的接口做个改造,返回对象数组:

[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; }
    }
}

接下来看下效果:

.NET5 引入Swagger

 这个方式呢,就不需要配置XML了,可以省去一些代码各有优缺点吧,根据自己情况使用。

7、隐藏接口

如果不想显示某些接口,直接在controller 上,或者action 上,增加特性可隐藏接口

[ApiExplorerSettings(IgnoreApi = true)]

.NET5 引入Swagger

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中。

.NET5 引入Swagger

既然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,如下:

.NET5 引入Swagger

  • 方法二我们可以通过RoutePrefix 属性设置。
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Core.Api v1");
    c.RoutePrefix = ""; //路径配置,设置为空,表示直接在根域名访问swagger文件
});

建议使用方法二。

三、Swagger异常汇总

1、问题一

.NET5 引入Swagger

如果出现如上图那样的错误,请打开swagger/v1/swagger.json查看具体原因。 常见的原因是action上没有加入HTTP请求协议,比如[HttpPost]

2、问题二

.NET5 引入Swagger

这是因为接口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、问题三

.NET5 引入Swagger

 

.NET5 引入Swagger

 

问题分析:出现该问题的原因是路由重载导致的,请确保不要存在多个一样的路由。路由重载是指出现了多个地址一样的接口(controller一样,action一样)。看了下代码,果然,同一个controller中写了重载的action。尽管返回类型和参数不一样,但是路由是一样的,所以swagger抛出了异常。

解决方法

  1. 方法一:避免出现路由重载的情况
  2. 方法二:对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错误,如下图:

.NET5 引入Swagger

(2)方法查找

  第一种情况:看到上边的截图,容易想到的是没有填入Bearer空格{token},这是最简单的情况。当然我遇见的不是这种情况。这里就不赘述了。

  第二种情况:查找jwttoken生成的方法和验证jwttoken的方法是不是不对应,比如是不是生成jwttoken的时候没有填写Issuer发行人,而验证的时候却要验证Issuer。这种问题归属于jwt生成和验证不统一的问题,我仔细比对过了,没问题。也不再赘述。

  第三种情况:监控swagger的请求,看看有没有携带Bearer空格{token}到后台接口,看下面截图:

.NET5 引入Swagger

 

   发现并没有发现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”?

相关文章:

  • 2022-12-23
  • 2021-11-08
  • 2022-12-23
  • 2021-11-29
  • 2021-06-28
  • 2021-09-21
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-05-26
  • 2022-12-23
相关资源
相似解决方案