asp net core 2.1中如何使用jwt(从原理到精通)

为什么使用 Jwt

最近,移动开发的劲头越来越足,学校搞的各种比赛都需要用手机 APP 来撑场面,所以,作为写后端的,很有必要改进一下以往的基于 Session 的身份认证方式了,理由如下:

  • 移动端经常要保持长时间(1 到 2 星期)在线,但是 Session 却不好在服务端保存这么久,虽然可以持久化到数据库,但是还是挺费资源
  • 移动端往往不是使用的网页技术,所以藏在 Cookie 里面的 SessionId 不是很方便的传递给服务端
  • 服务端暴露给客户端的接口往往是 RESTful 风格的,这是一种无状态的 API 风格,所以身份认证的方式最好也是无状态的才好

所以我选择了使用 Jwt (Json Web Token) 这个技术。Jwt 是一种无状态的分布式的身份验证方式,与 Session 相反,Jwt 将用户信息存放在 Token 的 payload 字段保存在客户端,通过 RSA 加密的方式,保证数据不会被篡改,验证数据有效性。

下面是一个使用 Jwt 的系统的身份验证流程:

可以看出,用户的信息保存在 Token 中,而 Token 分布在用户的设备中,所以服务端不再需要在内存中保存用户信息了
身份认证的 Token 传递时以一种相当简单的格式保存在 header 中,方便客户端对其进行操作

原理

jwt对所有语言都是通用的,只要知道秘钥,另一一种语言有可以对jwt的有效性进行判断;

jwt的组成;Header部分Base64转化.Payload部分Base64转化.使用HS256方式根据秘钥对前面两部分进行加密后再Base64转化,其中使用的hs256加密是header部分指定的,也可以通过官网的查看,如下图:

原理就这么简单,那究竟用怎样使用C#来实现呢,又怎么确定它的正确性呢?,请继续

使用C#实现

我们定义一个今天方法,其中需要使用到Microsoft.IdentityModel.Tokens.dll,asp.net core 2.1再带,如果其他版本,没有自带,需要nuget 一下这个类库

/// <summary>
 /// 创建jwttoken,源码自定义
 /// </summary>
 /// <param name="payLoad"></param>
 /// <param name="header"></param>
 /// <returns></returns>
 public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null)
 {
  if (header == null)
  {
  header = new Dictionary<string, object>(new List<KeyValuePair<string, object>>() {
   new KeyValuePair<string, object>("alg", "HS256"),
   new KeyValuePair<string, object>("typ", "JWT")
  });
  }
  //添加jwt可用时间(应该必须要的)
  var now = DateTime.UtcNow;
  payLoad["nbf"] = ToUnixEpochDate( now);//可用时间起始
  payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));//可用时间结束

  var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));
  var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad));

  var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
  var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload))));

  var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature);
  return encodedJwt;
 }
 public static long ToUnixEpochDate(DateTime date) =>  (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);

该方法很简单,只需要传入header键值对和payLoad键值对,然后根据原理进行Base64转换和hs256加密,接下来我们来使用一个测试类对其进行测试,代码如下:

[TestMethod]
 public void TokenValidateTest()
 {
  Dictionary<string, object> payLoad = new Dictionary<string, object>();
  payLoad.Add("sub", "rober");
  payLoad.Add("jti", "09e572c7-62d0-4198-9cce-0915d7493806");
  payLoad.Add("nbf", null);
  payLoad.Add("exp", null);
  payLoad.Add("iss", "roberIssuer");
  payLoad.Add("aud", "roberAudience");
  payLoad.Add("age", 30);

  var encodeJwt = TokenContext.CreateToken(payLoad, 30);

  var result = TokenContext.Validate(encodeJwt, (load) => { return true; });
  Assert.IsTrue(result);
 }

先不管后面的验证,我们先看看其中生成的encodeJwt的值:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ.7Is2KYHAtSr5fW2gPU1jGeHPzz2ULCZJGcWb40LSYyw

第一部分和第二部分,并不是加密,只是Base64转换,我们可以通过其他语言轻松转换回来,如下使用javascript进行转,window.atob(base64加密) window.btoa(base64解密)

var header=JSON.parse(window.atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))

如下图:

我再对payLoa进行转换回来, var payLoad=JSON.parse(window.atob('eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ')) ,如下图:

所以,从这里可以看出来,Base64并不是属于加密,只是简单转换,因此,不能在payLoad中存放重要内容,比如密码等

使用aspnetcore 中自带的类生成jwt

aspnet core中自带了一个jwt帮助类,其实原理一样,对上面做了封装,丰富了一个内容,我们继续使用一个静态方法,如下

/// <summary>
 /// 创建jwtToken,采用微软内部方法,默认使用HS256加密,如果需要其他加密方式,请更改源码
 /// 返回的结果和CreateToken一样
 /// </summary>
 /// <param name="payLoad"></param>
 /// <param name="expiresMinute">有效分钟</param>
 /// <returns></returns>
 public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)
 {

  var now = DateTime.UtcNow;

  // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
  // You can add other claims here, if you want:
  var claims = new List<Claim>();
  foreach (var key in payLoad.Keys)
  {
  var tempClaim = new Claim(key, payLoad[key]?.ToString());
  claims.Add(tempClaim);
  }

  // Create the JWT and write it to a string
  var jwt = new JwtSecurityToken(
  issuer: null,
  audience: null,
  claims: claims,
  notBefore: now,
  expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),
  signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));
  var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
  return encodedJwt;
 }

它效果和上面一模一样,如果使用同样的header 、payload、秘钥,生成的jwt肯定一样,这里就不演示了,感兴趣的可以自行尝试;

aspnetcore中如何使用自定义jwt验证

上面讲了那么多,只是为了大家更好的理解如何使用jwt进行验证,那是jwt是如何进行验证的呢?,如果一个http请求过来,一般jwt携带在http请求头部的Authorization中;先不看如何获取,先看看他是如何验证的,我们再定义个静态方法,如下:

/// <summary>
 /// 验证身份 验证签名的有效性,
 /// </summary>
 /// <param name="encodeJwt"></param>
 /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值, </param>
 /// 例如:payLoad["aud"]?.ToString() == "roberAuddience";
 /// 例如:验证是否过期 等
 /// <returns></returns>
 public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)
 {
  var success = true;
  var jwtArr = encodeJwt.Split('.');
  var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));
  var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));

  var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
  //首先验证签名是否正确(必须的)
  success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
  if (!success)
  {
  return success;//签名不正确直接返回
  }
  //其次验证是否在有效期内(也应该必须)
  var now = ToUnixEpochDate(DateTime.UtcNow);
  success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));

  //再其次 进行自定义的验证
  success = success && validatePayLoad(payLoad);

  return success;
 }

其中validatePayLoad 参数是一个自定义的验证的Fun,执行该Fun方法时会把解密后的payload作为参数传入进去

我们验证通过分为两部分,

第一,必须的(自认为的) 

  • jwt签名是否正确,请看以上代码实现 
  • jwt是否在可以时间内,请看以上代码实现

第二,自定义的(各复杂的,原理就是获取payLoad 的某个值,然后对这个值进行各种判读--等于,大于,包含,)  

  • 该jwt是不是进入黑名单
  • aud==‘roberAudience'

我们来通过一个测试类验证

[TestMethod]
  public void TokenCustomerValidateTest()
  {
   Dictionary<string, object> payLoad = new Dictionary<string, object>();
   payLoad.Add("sub", "rober");
   payLoad.Add("jti", Guid.NewGuid().ToString());
   payLoad.Add("nbf", null);
   payLoad.Add("exp", null);
   payLoad.Add("iss", "roberIssuer");
   payLoad.Add("aud", "roberAudience");
   payLoad.Add("age", 30);

   var encodeJwt = TokenContext.CreateToken(payLoad, 30);

   var result = TokenContext.Validate(encodeJwt, (load) => {
    var success = true;
    //验证是否包含aud 并等于 roberAudience
    success = success&& load["aud"]?.ToString() == "roberAudience";

    //验证age>20等
    int.TryParse(load["age"].ToString(), out int age);
    Assert.IsTrue(age > 30);
    //其他验证 jwt的标识 jti是否加入黑名单等

    return success;
   });
   Assert.IsTrue(result);
  }

如上面,我们可以把jwt中的payload解析出来,然后进行各种复杂的想要的验证;

其实,aspnet core中的基于角色,用户、策略,自定义策略的验证就相当这里的自定义验证,一下章将详细说明和对比,这里暂时不讲解

看完上面,是不是觉得jwt很简单就,主要就两部

  • 创建jwt;
  • 验证jwt;

完整代码如下:

/// <summary>
 /// Token上下文,负责token的创建和验证
 /// </summary>
 public class TokenContext
 {
  /// <summary>
  /// 秘钥,可以从配置文件中获取
  /// </summary>
  public static string securityKey = "GQDstclechengroberbojPOXOYg5MbeJ1XT0uFiwDVvVBrk";

  /// <summary>
  /// 创建jwttoken,源码自定义
  /// </summary>
  /// <param name="payLoad"></param>
  /// <param name="header"></param>
  /// <returns></returns>
  public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null)
  {
   if (header == null)
   {
    header = new Dictionary<string, object>(new List<KeyValuePair<string, object>>() {
     new KeyValuePair<string, object>("alg", "HS256"),
     new KeyValuePair<string, object>("typ", "JWT")
    });
   }
   //添加jwt可用时间(应该必须要的)
   var now = DateTime.UtcNow;
   payLoad["nbf"] = ToUnixEpochDate( now);//可用时间起始
   payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));//可用时间结束

   var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));
   var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad));

   var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
   var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload))));

   var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature);
   return encodedJwt;
  }

  /// <summary>
  /// 创建jwtToken,采用微软内部方法,默认使用HS256加密,如果需要其他加密方式,请更改源码
  /// 返回的结果和CreateToken一样
  /// </summary>
  /// <param name="payLoad"></param>
  /// <param name="expiresMinute">有效分钟</param>
  /// <returns></returns>
  public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)
  {

   var now = DateTime.UtcNow;

   // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
   // You can add other claims here, if you want:
   var claims = new List<Claim>();
   foreach (var key in payLoad.Keys)
   {
    var tempClaim = new Claim(key, payLoad[key]?.ToString());
    claims.Add(tempClaim);
   }

   // Create the JWT and write it to a string
   var jwt = new JwtSecurityToken(
    issuer: null,
    audience: null,
    claims: claims,
    notBefore: now,
    expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),
    signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));
   var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
   return encodedJwt;
  }

  /// <summary>
  /// 验证身份 验证签名的有效性,
  /// </summary>
  /// <param name="encodeJwt"></param>
  /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值, </param>
  /// 例如:payLoad["aud"]?.ToString() == "roberAuddience";
  /// 例如:验证是否过期 等
  /// <returns></returns>
  public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)
  {
   var success = true;
   var jwtArr = encodeJwt.Split('.');
   var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));
   var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));

   var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
   //首先验证签名是否正确(必须的)
   success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
   if (!success)
   {
    return success;//签名不正确直接返回
   }
   //其次验证是否在有效期内(也应该必须)
   var now = ToUnixEpochDate(DateTime.UtcNow);
   success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));

   //再其次 进行自定义的验证
   success = success && validatePayLoad(payLoad);

   return success;
  }
  /// <summary>
  /// 获取jwt中的payLoad
  /// </summary>
  /// <param name="encodeJwt"></param>
  /// <returns></returns>
  public static Dictionary<string ,object> GetPayLoad(string encodeJwt)
  {
   var jwtArr = encodeJwt.Split('.');
   var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));
   return payLoad;
  }
  public static long ToUnixEpochDate(DateTime date) =>
   (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
 }

以上就是jwt的基本内容,它确实很简单,不要被aspnet core中的各种写法给搞晕了,只要是jwt相关的验证都是基于上面这些东西

下一章节将讲述:

  • 在aspnet core中,自定义jwt管道验证;
  • 在aspnet core中,自定义策略验证CommonAuthorizeHandler : AuthorizationHandler<CommonAuthorize>
  • 自定义jwt逻辑验证和原生的角色,用户,策略,等进行对比

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • asp net core2.1如何使用jwt从原理到精通(二)

    在aspnet core中,自定义jwt管道验证 有了上一节的内容作为基础,那这点也是非常容易的,关键点在中间件,只是把上一级在测试类中的自定义验证放到中间件中来即可, 不过需要注意:中间件 的位置很重要,只有它后面的管道才会收到影响: 那我们先建一个自定义中间件类:(中间件的详细内容这里就不讲了,大家可以参考官网和其他博文) /// <summary> /// 自定义授权中间件 /// </summary> public class JwtCustomerAuthorizeMid

  • .net core webapi jwt 更为清爽的认证详解

    我的方式非主流,控制却可以更加灵活,喜欢的朋友,不妨花一点时间学习一下 jwt认证分为两部分,第一部分是加密解密,第二部分是灵活的应用于中间件,我的处理方式是将获取token放到api的一个具体的controller中,将发放token与验证分离,token的失效时间,发证者,使用者等信息存放到config中. 1.配置: 在appsettings.json中增加配置 "Jwt": { "Issuer": "issuer",//随意定义 &quo

  • asp.net core集成JWT的步骤记录

    [什么是JWT] JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案. JWT的官网地址:https://jwt.io/ 通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限. JWT中包含了身份认证必须的参数以及用户自定义的参数,JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名. [什么时候应该使用JSON Web令牌?] 授权:这是使用JWT的最常见方案.一旦用户登录

  • Asp.Net Core基于JWT认证的数据接口网关实例代码

    前言 近日,应一位朋友的邀请写了个Asp.Net Core基于JWT认证的数据接口网关Demo.朋友自己开了个公司,接到的一个升级项目,客户要求用Aps.Net Core做数据网关服务且基于JWT认证实现对前后端分离的数据服务支持,于是想到我一直做.Net开发,问我是否对.Net Core有所了解?能不能做个简单Demo出来看看?我说,分道扬镳之后我不是调用别人的接口就是提供接口给别人调用,于是便有了以下示例代码. 示例要求能演示获取Token及如何使用该Token访问数据资源,在Demo中实现

  • asp net core 2.1中如何使用jwt(从原理到精通)

    为什么使用 Jwt 最近,移动开发的劲头越来越足,学校搞的各种比赛都需要用手机 APP 来撑场面,所以,作为写后端的,很有必要改进一下以往的基于 Session 的身份认证方式了,理由如下: 移动端经常要保持长时间(1 到 2 星期)在线,但是 Session 却不好在服务端保存这么久,虽然可以持久化到数据库,但是还是挺费资源 移动端往往不是使用的网页技术,所以藏在 Cookie 里面的 SessionId 不是很方便的传递给服务端 服务端暴露给客户端的接口往往是 RESTful 风格的,这是一

  • ASP.NET Core 2.0中Razor页面禁用防伪令牌验证

    在这篇短文中,我将向您介绍如何ASP.NET Core Razor页面中禁用防伪令牌验证. Razor页面是ASP.NET Core 2.0中增加的一个页面控制器框架,用于构建动态的.数据驱动的网站:支持跨平台开发,可以部署到Windows,Unix和Mac操作系统. 跨站点请求伪造(也称为XSRF或CSRF)是对Web托管应用程序的攻击,因为恶意网站可能会影响客户端浏览器和浏览器信任网站之间的交互.这种攻击是完全有可能的,因为Web浏览器会自动在每一个请求中发送某些身份验证令牌到请求网站.这种

  • ASP.NET Core 2.2中的Endpoint路由详解

    Endpoint路由 在ASP.NET Core 2.2中,新增了一种路由,叫做 Endpoint (终结点)路由.本文将以往的路由系统称为 传统路由 . 本文通过源码的方式介绍传统路由和 Endpoint 路由部分核心功能和实现方法,具体功能上的差异见 官方文档 . 在升级到ASP.NET Core 2.2后,会自动启用 Endpoint 路由.如果要恢复以往的实现逻辑,需要加入以下代码: services.AddMvc(options => options.EnableEndpointRou

  • ASP.NET Core 应用程序中的静态文件中间件的实现

    在这篇文章中,我将向大家介绍,如何使用中间件组件来处理静态文件.这篇文章中,我们讨论下面几个问题: 在ASP.NET Core中,我们需要把静态文件存放在哪里? 在ASP.NET Core中 wwwroot文件夹是啥? 怎样在ASP.NET Core应用程序中,配置静态文件中间件? UseFileServer中间件的作用是什么? 最重要的特性之一就是;几乎所有的web应用程序都应该具备直接从文件系统存取静态文件的能力.ASP.NET Core能够直接从客户端获取应用程序的静态文件,比如:HTML

  • 在 asp.net core 的中间件中返回具体的页面的实现方法

    前言 在 asp.net core 中,存在着中间件这一概念,在中间件中,我们可以比过滤器更早的介入到 http 请求管道,从而实现对每一次的 http 请求.响应做切面处理,从而实现一些特殊的功能 在使用中间件时,我们经常实现的是鉴权.请求日志记录.全局异常处理等等这种非业务性的需求,而如果你有在 asp.net core 中使用过 swashbuckle(swagger).health check.mini profiler 等等这样的组件的话,你会发现,这些第三方的组件往往都提供了页面,允

  • 详解如何在ASP.NET Core Web API中以三种方式返回数据

    在 ASP.NET Core 中有三种返回 数据 和 HTTP状态码 的方式,最简单的就是直接返回指定的类型实例,如下代码所示: [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random()

  • 在ASP.Net Core应用程序中使用Bootstrap4

    笔者的前端文件如下 笔者增加Bootstrap 4 和 FontAwersome(字体图标),因为Bootsrap 4已经不再包含图标了. ASp.Net Core 中,通常在 _Layout.cshtml 文件设置全局css.js文件, 其中有两个标签 <environment include="Development"> </environment> <environment exclude="Development"> &l

  • 如何在ASP.NET Core类库项目中读取配置文件详解

    前言 最近有朋友问如何在.net core类库中读取配置文件,当时一下蒙了,这个提的多好,我居然不知道,于是这两天了解了相关内容才有此篇文章的出现,正常来讲我们在应用程序目录下有个appsettings.json文件对于相关配置都会放在这个json文件中,但是要是我建立一个类库项目,对于一些配置比如密钥或者其他需要硬编码的数据放在JSON文件中,在.net core之前配置文件为web.config并且有相关的类来读取节点上的数据,现如今在.net core中为json文件,那么我们该如何做?本

  • asp.net core 3.0中使用swagger的方法与问题

    Intro# 上次更新了 asp.net core 3.0 简单的记录了一下 swagger 的使用,那个项目的 api 比较简单,都是匿名接口不涉及到认证以及 api 版本控制,最近把另外一个 api 项目升级到了 3.0,还是遇到了一些问题,这里单独写一篇文章介绍,避免踩坑. Swagger 基本使用# swagger 服务注册: services.AddSwaggerGen(option => { option.SwaggerDoc("sparktodo", new Ope

随机推荐