翼度科技»论坛 编程开发 .net 查看内容

.NET9 - Swagger平替Scalar详解(四)

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
书接上回,上一章介绍了Swagger代替品Scalar,在使用中遇到不少问题,今天单独分享一下之前Swagger中常用的功能如何在Scalar中使用。
下面我们将围绕文档版本说明、接口分类、接口描述、参数描述、枚举类型、文件上传、JWT认证等方面详细讲解。

01、版本说明

我们先来看看默认添加后是什么样子的。
  1. public static void Main(string[] args)
  2. {
  3.     var builder = WebApplication.CreateBuilder(args);
  4.     builder.Services.AddControllers();
  5.     builder.Services.AddOpenApi();
  6.     var app = builder.Build();
  7.     app.MapScalarApiReference();
  8.     app.MapOpenApi();
  9.     app.UseAuthorization();
  10.     app.MapControllers();
  11.     app.Run();
  12. }
复制代码
效果如下:

我们可以直接修改builder.Services.AddOpenApi()这行代码,修改这块描述,代码如下:
  1. builder.Services.AddOpenApi(options =>
  2. {
  3.     options.AddDocumentTransformer((document, context, cancellationToken) =>
  4.     {
  5.         document.Info = new()
  6.         {
  7.             Title = "订单微服务",
  8.             Version = "v1",
  9.             Description = "订单相关接口"
  10.         };
  11.         return Task.CompletedTask;
  12.     });
  13. });
复制代码
我们再来看看效果。

02、接口分类

通过上图可以看到菜单左侧排列着所有接口,现在我们可以通过Tags特性对接口进行分类,如下图我们把增删改查4个方法分为幂等接口和非幂等接口两类,如下图:

然后我们看看效果,如下图:

03、接口描述

之前使用Swagger我们都是通过生成的注释XML来生成相关接口描述,现在则是通过编码的方式设置元数据来生成相关描述。
可以通过EndpointSummary设置接口摘要,摘要不设置默认为接口url,通过EndpointDescription设置接口描述,代码如下:
  1. //获取
  2. [HttpGet(Name = "")]
  3. [Tags("幂等接口")]
  4. [EndpointDescription("获取订单列表")]
  5. public IEnumerable<Order> Get()
  6. {
  7.     return null;
  8. }
  9. //删除
  10. [HttpDelete(Name = "{id}")]
  11. [Tags("幂等接口")]
  12. [EndpointSummary("删除订单")]
  13. [EndpointDescription("根据订单id,删除相应订单")]
  14. public bool Delete(string id)
  15. {
  16.     return true;
  17. }
复制代码
运行效果如下:

04、参数描述

同样可以通过Description特性来设置参数的描述,并且此特性可以直接作用于接口中参数之前,同时也支持作用于属性上,可以看看下面示例代码。
  1. public class Order
  2. {
  3.     [property: Description("创建日期")]
  4.     public DateOnly Date { get; set; }
  5.     [property: Required]
  6.     [property: DefaultValue(120)]
  7.     [property: Description("订单价格")]
  8.     public int Price { get; set; }
  9.     [property: Description("订单折扣价格")]
  10.     public int PriceF => (int)(Price * 0.5556);
  11.     [property: Description("商品名称")]
  12.     public string? Name { get; set; }
  13. }
  14. [HttpPut(Name = "{id}")]
  15. [Tags("非幂等接口")]
  16. public bool Put([Description("订单Id")] string id, Order order)
  17. {
  18.     return true;
  19. }
复制代码
效果如下图:

从上图可以发现除了描述还有默认值、必填项、可空等字样,这些是通过其他元数据设置的,对于属性还有以下元数据可以进行设置。

05、枚举类型

对于枚举类型,我们正常关注两个东西,其一为枚举项以int类型展示还是以字符串展示,其二为枚举项显示描述信息。
关于第一点比较简单只要对枚举类型使用JsonStringEnumConverter即可,代码如下:
  1. [JsonConverter(typeof(JsonStringEnumConverter<OrderStatus>))]
  2. public enum OrderStatus
  3. {
  4.     [Description("等待处理")]
  5.     Pending = 1,
  6.     [Description("处理中")]
  7.     Processing = 2,
  8.     [Description("已发货")]
  9.     Shipped = 3,
  10.     [Description("已送达")]
  11.     Delivered = 4,
  12. }
复制代码
效果如下:

通过上图也可以引发关于第二点的需求,如何对每个枚举项添加描述信息。
要达到这个目标需要做两件事,其一给每个枚举项通过Description添加元数据定义,其二我们要修改文档的数据结构Schema。
修改builder.Services.AddOpenApi(),通过AddSchemaTransformer方法修改文档数据结构,代码如下:
  1. options.AddSchemaTransformer((schema, context, cancellationToken) =>
  2. {
  3.     //找出枚举类型
  4.     if (context.JsonTypeInfo.Type.BaseType == typeof(Enum))
  5.     {
  6.         var list = new List<IOpenApiAny>();
  7.         //获取枚举项
  8.         foreach (var enumValue in schema.Enum.OfType<OpenApiString>())
  9.         {
  10.             //把枚举项转为枚举类型
  11.             if (Enum.TryParse(context.JsonTypeInfo.Type, enumValue.Value, out var result))
  12.             {
  13.                 //通过枚举扩展方法获取枚举描述
  14.                 var description = ((Enum)result).ToDescription();
  15.                 //重新组织枚举值展示结构
  16.                 list.Add(new OpenApiString($"{enumValue.Value} - {description}"));
  17.             }
  18.             else
  19.             {
  20.                 list.Add(enumValue);
  21.             }
  22.         }
  23.         schema.Enum = list;
  24.     }
  25.     return Task.CompletedTask;
  26. });
复制代码
我们再来看看结果。

但是这也带来了一个问题,就是参数的默认值也是添加描述的格式,显然这样的数据格式作为参数肯定是错误的,因此我们需要自己注意,如下图。目前我也没有发现更好的方式即可以把每项枚举描述加上,又不影响参数默认值,有解决方案的希望可以不吝赐教。

06、文件上传

下面我们来看看文件上传怎么用,直接上代码:
  1. [HttpPost("upload/image")]
  2. [EndpointDescription("图片上传接口")]
  3. [DisableRequestSizeLimit]
  4. public bool UploadImgageAsync(IFormFile file)
  5. {
  6.     return true;
  7. }
复制代码
然后我们测试一下效果。

首先我们可以看到请求示例中相关信息,这个相当于告诉我们后面要怎么选择文件上传,我们继续点击Test Request。
首先请求体需要选择multipart/form-data,上图请求示例中已经给出提示。

然后设置key为file,上图请求示例中已经给出提示,然后点击File上传图片,最后点击Send即可。

07、JWT认证

最后我们来看看如何使用JWT认证,
首先我们需要注入AddAuthentication及AddJwtBearer,具体代码如下:
  1. public class JwtSettingOption
  2. {
  3.     //这个字符数量有要求,不能随便写,否则会报错
  4.     public static string Secret { get; set; } = "123456789qwertyuiopasdfghjklzxcb";
  5.     public static string Issuer { get; set; } = "asdfghjkkl";
  6.     public static string Audience { get; set; } = "zxcvbnm";
  7.     public static int Expires { get; set; } = 120;
  8.     public static string RefreshAudience { get; set; } = "zxcvbnm.2024.refresh";
  9.     public static int RefreshExpires { get; set; } = 10080;
  10. }
  11. builder.Services.AddAuthentication(options =>
  12. {
  13.     options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  14.     options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  15. }).AddJwtBearer(options =>
  16. {
  17.     //取出私钥
  18.     var secretByte = Encoding.UTF8.GetBytes(JwtSettingOption.Secret);
  19.     options.TokenValidationParameters = new TokenValidationParameters()
  20.     {
  21.         //验证发布者
  22.         ValidateIssuer = true,
  23.         ValidIssuer = JwtSettingOption.Issuer,
  24.         //验证接收者
  25.         ValidateAudience = true,
  26.         ValidAudiences = new List<string> { JwtSettingOption.Audience, JwtSettingOption.Audience },
  27.         //验证是否过期
  28.         ValidateLifetime = true,
  29.         //验证私钥
  30.         IssuerSigningKey = new SymmetricSecurityKey(secretByte),
  31.         ClockSkew = TimeSpan.FromHours(1), //过期时间容错值,解决服务器端时间不同步问题(秒)
  32.         RequireExpirationTime = true,
  33.     };
  34. });
复制代码
然后我们需要继续修改builder.Services.AddOpenApi()这行代码,在里面加上如下代码:

其中BearerSecuritySchemeTransformer实现如下:
  1. public sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
  2. {
  3.     public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
  4.     {
  5.         var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
  6.         if (authenticationSchemes.Any(authScheme => authScheme.Name == JwtBearerDefaults.AuthenticationScheme))
  7.         {
  8.             var requirements = new Dictionary<string, OpenApiSecurityScheme>
  9.             {
  10.                 [JwtBearerDefaults.AuthenticationScheme] = new OpenApiSecurityScheme
  11.                 {
  12.                     Type = SecuritySchemeType.Http,
  13.                     Scheme = JwtBearerDefaults.AuthenticationScheme.ToLower(),
  14.                     In = ParameterLocation.Header,
  15.                     BearerFormat = "Json Web Token"
  16.                 }
  17.             };
  18.             document.Components ??= new OpenApiComponents();
  19.             document.Components.SecuritySchemes = requirements;
  20.             foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
  21.             {
  22.                 operation.Value.Security.Add(new OpenApiSecurityRequirement
  23.                 {
  24.                     [new OpenApiSecurityScheme
  25.                     {
  26.                         Reference = new OpenApiReference
  27.                         {
  28.                             Id = JwtBearerDefaults.AuthenticationScheme,
  29.                             Type = ReferenceType.SecurityScheme
  30.                         }
  31.                     }] = Array.Empty<string>()
  32.                 });
  33.             }
  34.         }
  35.     }
  36. }
复制代码
下面就可以通过[Authorize]开启接口认证,并实现一个登录接口获取token用来测试。
  1. [HttpPost("login")]
  2. [EndpointDescription("登录成功后生成token")]
  3. [AllowAnonymous]
  4. public string  Login()
  5. {
  6.     //登录成功返回一个token
  7.     // 1.定义需要使用到的Claims
  8.     var claims = new[] { new Claim("UserId", "test") };
  9.     // 2.从 appsettings.json 中读取SecretKey
  10.     var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSettingOption.Secret));
  11.     // 3.选择加密算法
  12.     var algorithm = SecurityAlgorithms.HmacSha256;
  13.     // 4.生成Credentials
  14.     var signingCredentials = new SigningCredentials(secretKey, algorithm);
  15.     var now = DateTime.Now;
  16.     var expires = now.AddMinutes(JwtSettingOption.Expires);
  17.     // 5.根据以上,生成token
  18.     var jwtSecurityToken = new JwtSecurityToken(
  19.         JwtSettingOption.Issuer,         //Issuer
  20.         JwtSettingOption.Audience,       //Audience
  21.         claims,                          //Claims,
  22.         now,                             //notBefore
  23.         expires,                         //expires
  24.         signingCredentials               //Credentials
  25.     );
  26.     // 6.将token变为string
  27.     var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
  28.     return token;
  29. }
复制代码
下面我们先用登录接口获取一个token。

我们先用token调用接口,可以发现返回401。

然后我们把上面获取的token放进去,请求成功。

在这个过程中有可能会遇到一种情况:Auth Type后面的下拉框不可选,如下图。

可能因以下原因导致,缺少[builder.Services.AddAuthentication().AddJwtBearer();]或[options.AddDocumentTransformer();]任意一行代码。
:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

来源:https://www.cnblogs.com/hugogoos/p/18571088
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具