立聪堂德州十三局店 发表于 2024-9-8 04:18:15

ASP.NET Core 8.0 WebApi 从零开始学习JWT登录认证

前言

我一起写后端Api我都是直接裸连的,但是现在为了规范一些,我还是打算上一个JWT功能
相关链接

   ASP.NET Web API 2系列(四):基于JWT的token身份认证方案
    Jwt-dotnet github
Nuget选择

https://i-blog.csdnimg.cn/blog_migrate/21797b781e2b3c2e7be4b9140c042988.png
选好了模板,就进去看看号了,42M的下载量已经很高了,一般来说,只要超过500k的下载量,基本就是一个稳定的代码堆栈了。
进去看看内里写的怎么样
https://i-blog.csdnimg.cn/blog_migrate/310b88635de7ff88d1e56059249c9102.png
可以看到写的还是比力清晰的
https://i-blog.csdnimg.cn/blog_migrate/ce18e66c6938b1747e81b430f22ff511.png
知识增补

JWT不是加密算法

JWT全名 JSON Web Token。Token这部分才是加密得出来的,JWT只是一个网页认证策略。
可逆加密和不可逆加密

无论是可逆加密还是不可逆加密,都需要一个自定义的【Key】来辅助加密。可逆加密就类似于一元二次方程,key就类似于修改方程各项的系数,【Key=125】得到【x^2+2x+5】。我的原文是【1】,得到的密文就是盘算后的效果【8】。所以加密比解密要方便。
不可逆加密就是不能逆向解决的方程,比如高阶方程【x^7- x^3 +5x-3】,正向好算,逆向基本不可解。
所以现实利用的时间,可逆加密一般是解密后利用。因为可逆,所以可以用于复杂的敏感信息。
不可逆加密是判定加密后的的密文是否类似,比如暗码,是不能走漏的,所以我们的暗码只能重置,因为系统也不知道原暗码是什么。
普通Jwt(不推荐)

项目环境



[*]visual studio 2022
[*].net core 8.0
[*]win 10
Nuget

https://i-blog.csdnimg.cn/blog_migrate/3a143eae691a0c80af03b6beaaf64651.png
https://i-blog.csdnimg.cn/blog_migrate/a7925c6c302c4f68b37dfd32d5aa3958.png
最小JWT测试

我们先不管JWT是怎样添加到ASP.NET Core内里的,我们先测试JWT的加密和登录验证的功能。
/// <summary>
/// 加密密钥
/// </summary>
private static readonly string jwtKey = "TokenKey";

public record UserTest(string Name,string Account,string Password);

static void Main(string[] args)
{

    UserTest userTest = new UserTest("小王","admin","1dixa0d");
    Console.WriteLine("原文");
    Console.WriteLine(JsonConvert.SerializeObject(userTest));//{"Name":"小王","Account":"admin","Password":"1dixa0d"}

    var encoder = GetEncoder();
    string jwtToken = encoder.Encode(userTest,jwtKey);
    Console.WriteLine("加密");
    Console.WriteLine(jwtToken);//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJOYW1lIjoi5bCP546LIiwiQWNjb3VudCI6ImFkbWluIiwiUGFzc3dvcmQiOiIxZGl4YTBkIn0.C_tlDhHpkjAkJpRRdnqz6Jn2FmP0qONAI4oNl8Ye9wM

    var decoder =GetDecoder();
    var decodeData =decoder.Decode(jwtToken,jwtKey);
    Console.WriteLine("解密");
    Console.WriteLine(decodeData);//{"Name":"小王","Account":"admin","Password":"1dixa0d"}


    Console.WriteLine("Hello, World!");
}

/// <summary>
/// 获取加密解密
/// </summary>
/// <returns></returns>
public static IJwtEncoder GetEncoder()
{
    IJwtAlgorithm algorithm = new HMACSHA256Algorithm();//加密方式
    IJsonSerializer serializer = new JsonNetSerializer();//序列化Json
    IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();//base64加解密
    IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);//JWT编码
    return encoder;
}

/// <summary>
/// 获取解密密钥
/// </summary>
/// <returns></returns>
public static IJwtDecoder GetDecoder()
{
    IJsonSerializer serializer = new JsonNetSerializer();
    IDateTimeProvider provider = new UtcDateTimeProvider();
    IJwtValidator validator = new JwtValidator(serializer, provider);
    IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
    IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
    IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
    return decoder;
}
https://i-blog.csdnimg.cn/blog_migrate/267e5930edab8338fe22527707e84da1.png
这里的JWT的加密解密算法是可以自己搭配的,选择加密算法,这里我就不展开了。具体可以看官方文档
https://i-blog.csdnimg.cn/blog_migrate/ae9274de4aaa921dd87b4c6fc83087dd.png
在WebApi中简单利用

起首我们之前用到了可逆的加密息争密。那我们就需要用一个判定是否逾期的类
public class UserJwtLogin
{
    /// <summary>
    /// 用户Id,因为数据库的Id是唯一的,不会重复
    /// </summary>
    public long UserId { get; set; }   

    /// <summary>
    /// 过期时间
    /// </summary>
    public DateTime ExpireTime { get; set; }
}
具体的解决方案就在这个文章内里了,我就不照抄了,拾人牙慧。
   ASP.NET Web API 2系列(四):基于JWT的token身份认证方案
我这里是这么写的
新建一个JwtHelper
public static class JwtHelper
{

    private static readonly string JwtKey = "这里填你的密钥";
    /// <summary>
    /// 获取加密解密
    /// </summary>
    /// <returns></returns>
    private static IJwtEncoder GetEncoder()
    {
      IJwtAlgorithm algorithm = new HMACSHA256Algorithm();//加密方式
      IJsonSerializer serializer = new JsonNetSerializer();//序列化Json
      IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();//base64加解密
      IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);//JWT编码
      return encoder;
    }

    /// <summary>
    /// 获取解密密钥
    /// </summary>
    /// <returns></returns>
    private static IJwtDecoder GetDecoder()
    {
      IJsonSerializer serializer = new JsonNetSerializer();
      IDateTimeProvider provider = new UtcDateTimeProvider();
      IJwtValidator validator = new JwtValidator(serializer, provider);
      IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
      IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
      IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
      return decoder;
    }

    /// <summary>
    /// 加密
    /// </summary>
    public static string Encode(object payload)
    {
      var encoder = GetEncoder();
      var token = encoder.Encode(payload, JwtKey);
      return token;
    }

    /// <summary>
    /// 解密
    /// </summary>
    public static T Decode<T>(string token)
    {
      var decoder = GetDecoder();
      var data =decoder.Decode(token,JwtKey);
      var res= JsonConvert.DeserializeObject<T>(data);
      return res;
    }

    /// <summary>
    /// 解密,只返回Json文本
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    public static string Decode(string token)
    {

      var decoder = GetDecoder();
      var data = decoder.Decode(token, JwtKey);
      return data;
    }

}
两个实体类
   public class UserJwtLogin
   {
       /// <summary>
       /// 用户Id,因为数据库的Id是唯一的,不会重复
       /// </summary>
       public long UserId { get; set; }   

       /// <summary>
       /// 过期时间
       /// </summary>
       public DateTime ExpireTime { get; set; }
   }
public class WebMsg
{

    public int Code { get; set; } = 200;

    public bool Success { get; set; } = true;

    public object Data {get; set; }

    public string Msg { get; set; } = "操作成功!";


    public WebMsg()
    {

    }


    public WebMsg(object data)
    {
      Data = data;
    }
}
简单利用

/// <summary>
/// Jwt登录
/// </summary>
/")]

public class LoginController : ControllerBase
{
        //这个是我的Nlog配置,这里不做展开
    private NlogService nlogService;

    public LoginController(NlogService nlogService)
    {
      this.nlogService = nlogService;
    }



    /// <summary>
    /// JWT登录
    /// </summary>
    /// <param name="username">账号</param>
    /// <param name="password">密码</param>
    /// <returns></returns>
   
    public WebMsg Login(string username, string password)
    {

      if (username == null || password == null)
      {
            return new WebMsg()
            {
                Msg = "登录信息为空",
                Success = false,
            };
      }
      if (username == "admin" && password == "123456")
      {
                        //这里是模拟拿到数据库的User表的Id
            var pyload = new UserJwtLogin()
            {
                UserId = 291,
                ExpireTime = DateTime.Now.AddDays(1)
            };
            var token = JwtHelper.Encode(pyload);
            return new WebMsg(token);
      }
      else
      {
            return new WebMsg()
            {
                Msg = "登录失败,账号或者密码错误",
                Success = false,
            };
      }

    }

    /// <summary>
    /// Jwt解密
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
   
    public WebMsg Decode(string token)
    {
      try
      {
            var res = JwtHelper.Decode(token);
            return new WebMsg(res);
      }
      catch (Exception ex)
      {
            nlogService.Error("登录解析失败:" + token);
            nlogService.Error(ex.Message);
            return new WebMsg() {
                Msg = "登录解析失败:" + token,
                Success = false,
            };
      }
    }
}
运行效果

https://i-blog.csdnimg.cn/blog_migrate/364d513e3db2ef300ca7d8cfa27714c6.png
{
"code": 200,
"success": true,
"data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjI5MSwiRXhwaXJlVGltZSI6IjIwMjQtMDMtMTRUMTM6MjE6MzYuNjE3NDk0KzA4OjAwIn0.sxh9sM4gQoCfFfim-MQSsHqDQX3Dji3FZaEu4t06D1s",
"msg": "操作成功!"
}
https://i-blog.csdnimg.cn/blog_migrate/6b9a7b1760bf1d38c6385eeb7aafe7c8.png
{
"code": 200,
"success": true,
"data": "{\"UserId\":291,\"ExpireTime\":\"2024-03-14T13:21:36.617494+08:00\"}",
"msg": "操作成功!"
}
WebApi 授权,博客太老了,尝试失败

这里我们就用简单的授权,复杂的授权可以自己去看微软官方文档
   ASP.NET Core 中的简单授权
https://i-blog.csdnimg.cn/blog_migrate/f4bf3097b997314354971211d900b3dc.png
然后我发现.net core 8.0的认证方式好像变了,我按照博客的写法发现不让我重写了
https://i-blog.csdnimg.cn/blog_migrate/dc591ebf5dd210b55b0e9647e4b3f1bd.png
这个方法是.net framework上面用的,.net core 用不了。我想还是算了,用.net core 的方法好了
https://i-blog.csdnimg.cn/blog_migrate/cb61cce27b5a47ff803ae7fdb25bc1ce.png
https://i-blog.csdnimg.cn/blog_migrate/619a5fff6b2e80a62f778535fd789d3d.png
https://i-blog.csdnimg.cn/blog_migrate/74df7c66cc38081959be5b2c14cecaad.png
WebApi .net core 8.0 最新版Jwt (微软官方集成)

我看了一天的博客,终于解决了JWT认证的题目。
   在没有 ASP.NET CoreIdentity的情况下利用cookie身份验证
    ASP.NET Core 6.0 添加 JWT 认证和授权
    .Net Core WebApi集成JWT实现身份认证
https://i-blog.csdnimg.cn/blog_migrate/ba4e7626210ca721282262ceb04374b4.png
重新新建一个Webapi

这里我就不多说了
   .NET Core webapi 从零开始在IIS上面发布后端接口
我们还是要改一下的,因为之前的我测试的时间,发现Builder内里的代码实在是太多了,这里我们分开一下

using Microsoft.OpenApi.Models;
using System.Reflection;

namespace JtwTestWebApi
{
    public class Program
    {
      public static void Main(string[] args)
      {
            var builder = WebApplication.CreateBuilder(args);
            var MyPolicy = "MyPolicy";
            // Add services to the container.

            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();
            //为了防止配套太多,代码混乱。这里自定义了一个方法
            AddMyService(builder);
            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
               
            }
            app.UseSwagger();
            app.UseSwaggerUI();
            app.UseStatusCodePagesWithRedirects("/swagger/index.html");
            app.UseHttpsRedirection();

            app.UseAuthorization();


            app.MapControllers();

            app.Run();
      }

      /// <summary>
      /// 为了防止代码过于臃肿,将新的配置放在这里写
      /// </summary>
      /// <param name="builder"></param>
      public static void AddMyService(WebApplicationBuilder builder)
      {
            builder.Services.AddCors(options =>
            {
                options.AddPolicy("MyPolicy", policy =>
                {
                  policy.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod();
                });
            });
            builder.Services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new OpenApiInfo
                {
                  Version = "v1",
                  Title = "API标题",
                  Description = $"API描述,v1版本"
                });
                var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                //IncludeXmlComments 第二参数 true 则显示 控制器 注释
                options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename), true);
            });
      }
    }
}

简单Controller

public class WebMsg
{

    public object Data { get; set; }

    public bool Success { get; set; } = true;

    public string Msg { get; set; } = "操作成功!";

    public WebMsg()
    {

    }

    public WebMsg(object data)
    {
      Data = data;
    }
}
/// <summary>
/// 测试控制器
/// </summary>
/")]

public class TestController : ControllerBase
{
    /// <summary>
    /// 测试返回值
    /// </summary>
    /// <returns></returns>
   
    public WebMsg GetTest()
    {
      return new WebMsg("测试返回值");
    }

}
https://i-blog.csdnimg.cn/blog_migrate/661a168662362d19ddc16059ced35a37.png
最简单的Jwt认证

我们知道Jwt其实就是生成和验证两个功能。微软把这个功能集成到一个JwtBearer库内里了。
https://i-blog.csdnimg.cn/blog_migrate/bd54f7e6c53c387e693d20ae627ef6cd.png
为了方便Json打印,添加个Newtonsoft
https://i-blog.csdnimg.cn/blog_migrate/765d6a740713747bb2e8608b1c9b2f96.png
获取JwtConfig

我们在appsetting.json内里添加
"JwtConfig": {
    "SecretKey": "123123123123", // 密钥   可以是guid 也可以是随便一个字符串
    "Issuer": "XiaoWang", // 颁发者
    "Audience": "XiaoWang", // 接收者
    "Expired": 30 // 过期时间(30min)
}
https://i-blog.csdnimg.cn/blog_migrate/947b83451c88186dd44e35ba131c11c2.png
public class JwtConfig
{
   /// <summary>
   /// 密钥
   /// </summary>
   public string SecretKey { get; set; }   

   /// <summary>
   /// 发布者
   /// </summary>
   public string Issuer { get; set; }

   /// <summary>
   /// 接受者
   /// </summary>
   public string Audience { get; set; }

   /// <summary>
   /// 过期时间(min)
   /// </summary>
   public int Expired { get; set; }
}
获取Config
var jwtConfig = new JwtConfig();
builder.Configuration.Bind("JwtConfig",jwtConfig);
https://i-blog.csdnimg.cn/blog_migrate/3c9a4d21366a7aca061cd32948e5d81c.png
https://i-blog.csdnimg.cn/blog_migrate/2cac500048c6135e1c27ec67fca1170d.png
新建JwtHelper类

public class JwtHelper
{

   public JwtConfig JwtConfig { get; set; }
   public JwtHelper()
   {

   }

   /// <summary>
   /// 添加Jwt服务
   /// </summary>
   public void AddJwtService()
   {

   }

   /// <summary>
   /// 返回
   /// </summary>
   /// <returns></returns>
   public string GetJwtToken()
   {
         //简单测试一下
         var token = JsonConvert.SerializeObject(JwtConfig);
         return token;
   }


}
我们应该利用Ioc的方式注入JwtHelper
var jwtConfig = new JwtConfig();
builder.Configuration.Bind("JwtConfig",jwtConfig);
var jwtHelper = new JwtHelper() {    JwtConfig = jwtConfig};//将JwtHelper添加到Services内里builder.Services.AddSingleton<JwtHelper>(jwtHelper); https://i-blog.csdnimg.cn/blog_migrate/42a3b3ae1a42347d9537c26b1bffbae2.png
然后在控制器内里获取Token
/// <summary>
/// 测试控制器
/// </summary>
/")]

public class TestController : ControllerBase
{
    private JwtHelper jwtHelper;

    /// <summary>
    /// 通过Ioc得到Jwt
    /// </summary>
    /// <param name="jwtHelper"></param>
    public TestController(JwtHelper jwtHelper) {
      this.jwtHelper = jwtHelper;
    }
    /// <summary>
    /// 测试返回值
    /// </summary>
    /// <returns></returns>
   
    public WebMsg GetTest()
    {
      return new WebMsg("测试返回值");
    }


    /// <summary>
    /// 获取JwtToken
    /// </summary>
    /// <returns></returns>
   
    public WebMsg GetJwtToken()
    {
      var token = jwtHelper.GetJwtToken();
      return new WebMsg(token);
    }
}
https://i-blog.csdnimg.cn/blog_migrate/24c78c08da734e68d7be8c2af3b15049.png
接下来我们修改一下GetJwtToken这个函数即可
完善JwtConfig

using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace JtwTestWebApi.Models
{
    public class JwtConfig
    {
      /// <summary>
      /// 密钥
      /// </summary>
      public string SecretKey { get; set; }

      /// <summary>
      /// 发布者
      /// </summary>
      public string Issuer { get; set; }

      /// <summary>
      /// 接受者
      /// </summary>
      public string Audience { get; set; }

      /// <summary>
      /// 过期时间(min)
      /// </summary>
      public int Expired { get; set; }

      /// <summary>
      /// 生效时间
      /// </summary>
      public DateTime NotBefore => DateTime.Now;

      /// <summary>
      /// 过期时间
      /// </summary>
      public DateTime Expiration => DateTime.Now.AddMinutes(Expired);

      /// <summary>
      /// 密钥Bytes
      /// </summary>
      private SecurityKey SigningKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));

      /// <summary>
      /// 加密后的密钥,使用HmacSha256加密
      /// </summary>
      public SigningCredentials SigningCredentials =>
            new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256);
    }
}

在JwtHelper内里利用
/// <summary>
/// 最简单的JwtToken
/// </summary>
/// <returns></returns>
public string GetJwtToken()
{
    var claims = new List<Claim>();
    var jwtSecurityToken = new JwtSecurityToken(
            JwtConfig.Issuer,
            JwtConfig.Audience,
            claims,
            JwtConfig.NotBefore,
            JwtConfig.Expiration,
            JwtConfig.SigningCredentials
      );
    var token = new JwtSecurityTokenHandler().WriteToken( jwtSecurityToken );
    return token;
}
      
运行报错,说明密钥的长度太短了,我们加长一下
https://i-blog.csdnimg.cn/blog_migrate/6827e54c44188e2d4a7ffdca720d3bc4.png
https://i-blog.csdnimg.cn/blog_migrate/49ee9a0494dd0db33b7ecdec2b9e69ee.png
返回乐成!
https://i-blog.csdnimg.cn/blog_migrate/836bf0b8635575de4c2c763a2a411296.png
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MTAzODUyODgsImV4cCI6MTcxMDM4NzA4OCwiaXNzIjoiWGlhb1dhbmciLCJhdWQiOiJYaWFvV2FuZyJ9.v7UbQOba7VoNgoiRsoIQkFJKQTFBMLJlYEKBIfdFV4o
授权

加密了,肯定要有对应的授权
在JwtConfig内里添加对应的授权
public class JwtConfig
{
    /// <summary>
    /// 密钥
    /// </summary>
    public string SecretKey { get; set; }

    /// <summary>
    /// 发布者
    /// </summary>
    public string Issuer { get; set; }

    /// <summary>
    /// 接受者
    /// </summary>
    public string Audience { get; set; }

    /// <summary>
    /// 过期时间(min)
    /// </summary>
    public int Expired { get; set; }

    /// <summary>
    /// 生效时间
    /// </summary>
    public DateTime NotBefore => DateTime.Now;

    /// <summary>
    /// 过期时间
    /// </summary>
    public DateTime Expiration => DateTime.Now.AddMinutes(Expired);

    /// <summary>
    /// 密钥Bytes
    /// </summary>
    private SecurityKey SigningKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));

    /// <summary>
    /// 加密后的密钥,使用HmacSha256加密
    /// </summary>
    public SigningCredentials SigningCredentials =>
      new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256);

    /// <summary>
    /// 认证用的密钥
    /// </summary>
    public SymmetricSecurityKey SymmetricSecurityKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
}
https://i-blog.csdnimg.cn/blog_migrate/a2dcc6df2ae712b0dcdc2023d34f70f3.png
在JwtHelper中
/// <summary>
/// 添加Jwt服务
/// </summary>
public void AddJwtService(IServiceCollection services)
{
    services.AddAuthentication(option =>
    {
      //认证middleware配置
      option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
      option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
      options.TokenValidationParameters = new TokenValidationParameters
      {
            //Token颁发机构
            ValidIssuer = JwtConfig.Issuer,
            //颁发给谁
            ValidAudience = JwtConfig.Audience,
            //这里的key要进行加密
            IssuerSigningKey = JwtConfig.SymmetricSecurityKey,
            //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
            ValidateLifetime = true,
      };
    });
}
调用JwtHelper
var jwtConfig = new JwtConfig();
builder.Configuration.Bind("JwtConfig",jwtConfig);
var jwtHelper = new JwtHelper() {
      JwtConfig = jwtConfig
};
//将JwtHelper添加到Services里面
builder.Services.AddSingleton<JwtHelper>(jwtHelper);
jwtHelper.AddJwtService(builder.Services);
在app中启用
            app.UseHttpsRedirection();
            app.UseAuthentication();//要在授权之前认证,这个和特性有关
            app.UseAuthorization();
一定要在za之前利用ca
https://i-blog.csdnimg.cn/blog_migrate/149c3680c9375d63a4c01a3e3af4b543.png
授权测试

我们接口最好单独开一个获取Token的接口
TestController
using JtwTestWebApi.Models;
using JtwTestWebApi.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace JtwTestWebApi.Controllers
{
    /// <summary>
    /// 测试控制器
    /// </summary>
    /")]
   
    public class TestController : ControllerBase
    {
      private JwtHelper jwtHelper;

      /// <summary>
      /// 通过Ioc得到Jwt
      /// </summary>
      /// <param name="jwtHelper"></param>
      public TestController(JwtHelper jwtHelper) {
            this.jwtHelper = jwtHelper;
      }
      /// <summary>
      /// 测试返回值
      /// </summary>
      /// <returns></returns>
      
      public WebMsg GetTest()
      {
            return new WebMsg("测试返回值");
      }


      /// <summary>
      /// 获取JwtToken
      /// </summary>
      /// <returns></returns>
      
      public WebMsg GetJwtToken()
      {
            var token = jwtHelper.GetJwtToken();
            return new WebMsg(token);
      }

      /// <summary>
      /// 可以在方法前面加Authorize
      /// </summary>
      /// <returns></returns>
      
      
      public WebMsg GetByJwt()
      {
            return new WebMsg("Jwt测试成功!");
      }
    }
}

JtwTestWebApi
如果我们在类前面添加了,下面全部的接口都有Jwt认证。想放开Jtw认证,利用标记方法即可
using JtwTestWebApi.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace JtwTestWebApi.Controllers
{
    /// <summary>
    /// Jwt测试类
    /// </summary>
    /")]
   
   
    public class JwtTestController : ControllerBase
    {
      /// <summary>
      /// 不需要Jwt
      /// </summary>
      /// <returns></returns>
      
      
      public WebMsg NoJwtGet()
      {
            return new WebMsg("我不需要Jwt");
      }


      /// <summary>
      /// 需要Jwt
      /// </summary>
      /// <returns></returns>
      
      public WebMsg JwtGet()
      {
            return new WebMsg("我需要Jwt");
      }
    }
}

认证失败的效果
https://i-blog.csdnimg.cn/blog_migrate/9cc5968a69533254a1c6e2b789f069b6.png
能通过Jwt的请求

既然我们拦截了请求,我们也得对符合规范的请求放行。那什么样的请求能放行呢?在Header中的【Authorization】中添加【Bearer {token}】才气放行。
PostMan测试

https://i-blog.csdnimg.cn/blog_migrate/0eec98256671090dac4e65979011ef3a.png
https://i-blog.csdnimg.cn/blog_migrate/31060d1c60ad052b30d929bf4fb092d7.png
测试乐成!
https://i-blog.csdnimg.cn/blog_migrate/54690286695fd80c9ee6b597281eeb6f.png
逾期测试

   .NetCore JWT token逾期时间设置
Jwt的逾期是由两个数据综合相加的。一个是生成Token的逾期时间
https://i-blog.csdnimg.cn/blog_migrate/8a377de7497c1e0a07aac79c5fe33cfe.png
一个是缓冲时间,默认5分钟。因为服务器的时间大概不同步。
https://i-blog.csdnimg.cn/blog_migrate/09cbd790bb6a5cea4713bbd897f325bc.png
所以总逾期时间=逾期时间+缓冲时间
https://i-blog.csdnimg.cn/blog_migrate/e9c375241f5859206640f15c5374f655.gif
为了更简单的获取Token,我们直接把返回的Token自带这个前缀好了
Swagger 全局Header

在JwtHelper中添加全局静态方法
/// <summary>
/// Swagger添加Jwt功能
/// </summary>
/// <param name="options"></param>
public static void SwaggerAddJwtHeader(SwaggerGenOptions options)
{
    options.AddSecurityRequirement(new OpenApiSecurityRequirement()
      {
            {
                new OpenApiSecurityScheme
                {
                  Reference = new OpenApiReference {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer",
                  }
                },
            new string[] { }
            }
      });
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
      Description = "JWT授权(数据将在请求头中进行传输) 在下方输入Bearer {token} 即可,注意两者之间有空格",
      Name = "Authorization",//jwt默认的参数名称
      In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
      Type = SecuritySchemeType.ApiKey,
      BearerFormat = "JWT",
      Scheme = "Bearer"
    });
}
https://i-blog.csdnimg.cn/blog_migrate/f245c3bcf91dccd363c19fb364771401.png
https://i-blog.csdnimg.cn/blog_migrate/a565d9ab12f22db31275688f4d4cf9e6.png
https://i-blog.csdnimg.cn/blog_migrate/2994e2c59bf8e4fc8127ad50dda02952.png
https://i-blog.csdnimg.cn/blog_migrate/0dfd30bd3dd96f20b75dfe41b58ead6b.png
https://i-blog.csdnimg.cn/blog_migrate/b347a3aeaa646da948f5ac7891b85edb.png
获取Jwt中的信息

因为我们的Jwt是自带【Bearer 】这个请求头的,所以去掉前面的头,内里其实是可以解密的
/// <summary>
/// 获取Jwt的信息
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public IEnumerable<Claim> Decode(HttpRequest request)
{
   
   var authorization = request.Headers["Authorization"].ToString();
   //因为我们的Jwt是自带【Bearer 】这个请求头的,所以去掉前面的头
   var auth = authorization.Split(" ");
   var handler = new JwtSecurityTokenHandler();
   //反解密,获取其中的Claims
   var payload = handler.ReadJwtToken(auth).Payload;
   var claims = payload.Claims;
   return claims;
}
解密能拿到是因为我们加密的时间就已经放进去了
/// <summary>
/// 添加claims信息
/// </summary>
/// <param name="claims"></param>
/// <returns></returns>
public string GetJwtToken(List<Claim> claims)
{
   var jwtSecurityToken = new JwtSecurityToken(
             JwtConfig.Issuer,
             JwtConfig.Audience,
             claims,
             JwtConfig.NotBefore,
             JwtConfig.Expiration,
             JwtConfig.SigningCredentials
         );
   var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
   token = "Bearer " + token;
   return token;
}
/// <summary>
/// 获取JwtToken
/// </summary>
/// <returns></returns>

public WebMsg GetJwtToken2()
{
        //我们加密的时候,就放进去了一些额外的信息
   var token = jwtHelper.GetJwtToken(new List<Claim>()
   {
         new Claim("UserId","2"),
         new Claim("UserName","3")
   });
   return new WebMsg(token);
}
/// <summary>
/// 可以在方法前面加Authorize
/// </summary>
/// <returns></returns>


public WebMsg GetByJwt()
{
        //获取解密后的Claim
    var dic = jwtHelper.Decode(this.Request);
    return new WebMsg("Jwt测试成功!");
}
https://i-blog.csdnimg.cn/blog_migrate/b23bea8c1175a70ab4f8ffa455e48988.png
这样我们就可以把一些比力隐私的数据放在内里,比如用户Id,这样防止走漏。
简单封装一下

using JtwTestWebApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JtwTestWebApi.Utils
{
    public class JwtHelper
    {

      public JwtConfig JwtConfig { get; set; }
      public JwtHelper()
      {

      }

      /// <summary>
      /// 添加Jwt服务
      /// </summary>
      public void AddJwtService(IServiceCollection services)
      {
            services.AddAuthentication(option =>
            {
                //认证middleware配置
                option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                  //Token颁发机构
                  ValidIssuer = JwtConfig.Issuer,
                  //颁发给谁
                  ValidAudience = JwtConfig.Audience,
                  //这里的key要进行加密
                  IssuerSigningKey = JwtConfig.SymmetricSecurityKey,
                  //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
                  ValidateLifetime = true,
                  ValidateIssuer = true,
                  ValidateAudience = true,
                  ValidateIssuerSigningKey = true,
                  RequireExpirationTime = true,
                };
            });
      }

      /// <summary>
      /// 最简单的JwtToken
      /// </summary>
      /// <returns></returns>
      public string GetJwtToken()
      {
            var claims = new List<Claim>();
            var jwtSecurityToken = new JwtSecurityToken(
                  JwtConfig.Issuer,
                  JwtConfig.Audience,
                  claims,
                  JwtConfig.NotBefore,
                  JwtConfig.Expiration,
                  JwtConfig.SigningCredentials
                );
            var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
            token = "Bearer " + token;
            return token;
      }

      /// <summary>
      /// 添加claims信息
      /// </summary>
      /// <param name="claims"></param>
      /// <returns></returns>
      public string GetJwtToken(List<Claim> claims)
      {
            var jwtSecurityToken = new JwtSecurityToken(
                  JwtConfig.Issuer,
                  JwtConfig.Audience,
                  claims,
                  JwtConfig.NotBefore,
                  JwtConfig.Expiration,
                  JwtConfig.SigningCredentials
                );
            var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
            token = "Bearer " + token;
            return token;
      }

      /// <summary>
      /// UserModel类Token
      /// </summary>
      /// <param name="user"></param>
      /// <returns></returns>
      public string GetJwtToken(JwtUserModel user)
      {
            var claims = new List<Claim>() {
                new Claim("UserId",user.UserId.ToString()),
                new Claim("UserName",user.UserName),
                new Claim("UserType",user.UserType.ToString()),
            };
            return GetJwtToken(claims);
      }

      /// <summary>
      /// Swagger添加Jwt功能
      /// </summary>
      /// <param name="options"></param>
      public static void SwaggerAddJwtHeader(SwaggerGenOptions options)
      {
            options.AddSecurityRequirement(new OpenApiSecurityRequirement()
                {
                  {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference {
                              Type = ReferenceType.SecurityScheme,
                              Id = "Bearer",
                            }
                        },
                  new string[] { }
                  }
                });
            options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                Description = "JWT授权(数据将在请求头中进行传输) 在下方输入Bearer {token} 即可,注意两者之间有空格",
                Name = "Authorization",//jwt默认的参数名称
                In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
                Type = SecuritySchemeType.ApiKey,
                BearerFormat = "JWT",
                Scheme = "Bearer"
            });
      }

      /// <summary>
      /// 获取Jwt的信息
      /// </summary>
      /// <param name="request"></param>
      /// <returns></returns>
      public IEnumerable<Claim> Decode(HttpRequest request)
      {

            var authorization = request.Headers["Authorization"].ToString();
            //因为我们的Jwt是自带【Bearer 】这个请求头的,所以去掉前面的头
            var auth = authorization.Split(" ");
            var handler = new JwtSecurityTokenHandler();
            //反解密,获取其中的Claims
            var payload = handler.ReadJwtToken(auth).Payload;
            var claims = payload.Claims;
            return claims;
      }

      /// <summary>
      /// 解析得到User
      /// </summary>
      /// <param name="request"></param>
      /// <returns></returns>
      public JwtUserModel DecodeToUser(HttpRequest request)
      {
            var claims = Decode(request);
            var user = new JwtUserModel()
            {
                UserId = claims.Where(t => t.Type == "UserId").First().Value,
                UserName = claims.Where(t => t.Type == "UserName").First().Value,
                UserType = claims.Where(t => t.Type == "UserType").First().Value
            };
            return user;
      }
    }
}


public WebMsg GetJwtToken3()
{
    var token = jwtHelper.GetJwtToken(new JwtUserModel()
    {
      UserName = "小王",
      UserId = "32",
      UserType = "admin"
    });
    return new WebMsg(token);
}


public WebMsg GetByJwt3()
{
    var dic = jwtHelper.DecodeToUser(this.Request);

    return new WebMsg("Jwt测试成功!");
}
运行得到效果
https://i-blog.csdnimg.cn/blog_migrate/8a9161bd7047e3ad2a0685686bfee60a.png
Jwt授权模式底子讲解

介绍三种授权方式(Policy、Role、Scheme),Scheme这种用的太少了,我们就简单讲解一下Policy和Role这两种方式。Policy和Role的核心就在于我们封装Token的时间添加的Claim对象。大家看到这里就会发现,其实Jwt的核心就是装包和拆包。网页请求Token的时间拿到了个包裹,网页发送Http的时间解开这个包裹。
我写了一会,感觉有点累了。这里有个别人写好的代码,有兴趣的自己去看吧。
   ASP.NET Core 6.0 添加 JWT 认证和授权
简单的【角色授权】

获取不同权限的Token

获取不同权限的Token
/// <summary>
/// 获取Role = admin的Token
/// </summary>
/// <returns></returns>

public WebMsg GetJwtToken_Admin()
{
    var token = jwtHelper.GetJwtToken(new List<Claim>()
    {
      new Claim(ClaimTypes.Role,"admin")
    });

    return new WebMsg(token);
}
/// <summary>
/// 获取Role = user
/// </summary>
/// <returns></returns>

public WebMsg GetJwtToken_User()
{
    var token = jwtHelper.GetJwtToken(new List<Claim>()
    {
      new Claim(ClaimTypes.Role,"user")
    });

    return new WebMsg(token);
}

/// <summary>
/// 获取Role = admin和user
/// </summary>
/// <returns></returns>

public WebMsg GetJwtToken_UserAndAdmin()
{
    var token = jwtHelper.GetJwtToken(new List<Claim>()
    {
      new Claim(ClaimTypes.Role,"user"),
      new Claim(ClaimTypes.Role,"admin")
    });

    return new WebMsg(token);
}
不同权限的jwt认证

/// <summary>
/// 需要role=admin
/// </summary>
/// <returns></returns>


public WebMsg AdminGet()
{
    return new WebMsg("admin");
}
/// <summary>
/// role=user
/// </summary>
/// <returns></returns>


public WebMsg UserGet()
{
    return new WebMsg("user");
}

/// <summary>
/// role = admin或user
/// </summary>
/// <returns></returns>


public WebMsg AdminOrUserGet()
{
    return new WebMsg("admin or user");
}
/// <summary>
/// role=admin和user
/// </summary>
/// <returns></returns>



public WebMsg AdminAndUserGet()
{
    return new WebMsg("admin and user");
}
这里推荐利用enum枚举范例,这样不会出现拼写错误
封装好的代码

using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace JtwTestWebApi.Models
{
    public class JwtConfig
    {
      /// <summary>
      /// 密钥
      /// </summary>
      public string SecretKey { get; set; }

      /// <summary>
      /// 发布者
      /// </summary>
      public string Issuer { get; set; }

      /// <summary>
      /// 接受者
      /// </summary>
      public string Audience { get; set; }

      /// <summary>
      /// 过期时间(min)
      /// </summary>
      public int Expired { get; set; }

      /// <summary>
      /// 生效时间
      /// </summary>
      public DateTime NotBefore => DateTime.Now;

      /// <summary>
      /// 过期时间
      /// </summary>
      public DateTime Expiration => DateTime.Now.AddMinutes(Expired);

      /// <summary>
      /// 密钥Bytes
      /// </summary>
      private SecurityKey SigningKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));

      /// <summary>
      /// 加密后的密钥,使用HmacSha256加密
      /// </summary>
      public SigningCredentials SigningCredentials =>
            new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256);

      /// <summary>
      /// 认证用的密钥
      /// </summary>
      public SymmetricSecurityKey SymmetricSecurityKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));



    }
}

using JtwTestWebApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JtwTestWebApi.Utils
{
    public class JwtHelper
    {

      public JwtConfig JwtConfig { get; set; }
      public JwtHelper()
      {

      }

      /// <summary>
      /// 添加Jwt服务
      /// </summary>
      public void AddJwtService(IServiceCollection services)
      {
            services.AddAuthentication(option =>
            {
                //认证middleware配置
                option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                  //Token颁发机构
                  ValidIssuer = JwtConfig.Issuer,
                  //颁发给谁
                  ValidAudience = JwtConfig.Audience,
                  //这里的key要进行加密
                  IssuerSigningKey = JwtConfig.SymmetricSecurityKey,
                  //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
                  ValidateLifetime = true,
                  ValidateIssuer = true,
                  ValidateAudience = true,
                  ValidateIssuerSigningKey = true,
                  RequireExpirationTime = true,
                };
            });
      }

      /// <summary>
      /// 最简单的JwtToken
      /// </summary>
      /// <returns></returns>
      public string GetJwtToken()
      {
            var claims = new List<Claim>();
            var jwtSecurityToken = new JwtSecurityToken(
                  JwtConfig.Issuer,
                  JwtConfig.Audience,
                  claims,
                  JwtConfig.NotBefore,
                  JwtConfig.Expiration,
                  JwtConfig.SigningCredentials
                );
            var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
            token = "Bearer " + token;
            return token;
      }

      /// <summary>
      /// 添加claims信息
      /// </summary>
      /// <param name="claims"></param>
      /// <returns></returns>
      public string GetJwtToken(List<Claim> claims)
      {
            var jwtSecurityToken = new JwtSecurityToken(
                  JwtConfig.Issuer,
                  JwtConfig.Audience,
                  claims,
                  JwtConfig.NotBefore,
                  JwtConfig.Expiration,
                  JwtConfig.SigningCredentials
                );
            var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
            token = "Bearer " + token;
            return token;
      }

      /// <summary>
      /// UserModel类Token
      /// </summary>
      /// <param name="user"></param>
      /// <returns></returns>
      public string GetJwtToken(JwtUserModel user)
      {
            var claims = new List<Claim>() {
                new Claim("UserId",user.UserId.ToString()),
                new Claim("UserName",user.UserName),
                new Claim("UserType",user.UserType.ToString()),
            };
            return GetJwtToken(claims);
      }

      /// <summary>
      /// Swagger添加Jwt功能
      /// </summary>
      /// <param name="options"></param>
      public static void SwaggerAddJwtHeader(SwaggerGenOptions options)
      {
            options.AddSecurityRequirement(new OpenApiSecurityRequirement()
                {
                  {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference {
                              Type = ReferenceType.SecurityScheme,
                              Id = "Bearer",
                            }
                        },
                  new string[] { }
                  }
                });
            options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                Description = "JWT授权(数据将在请求头中进行传输) 在下方输入Bearer {token} 即可,注意两者之间有空格",
                Name = "Authorization",//jwt默认的参数名称
                In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
                Type = SecuritySchemeType.ApiKey,
                BearerFormat = "JWT",
                Scheme = "Bearer"
            });
      }

      /// <summary>
      /// 获取Jwt的信息
      /// </summary>
      /// <param name="request"></param>
      /// <returns></returns>
      public IEnumerable<Claim> Decode(HttpRequest request)
      {

            var authorization = request.Headers["Authorization"].ToString();
            //因为我们的Jwt是自带【Bearer 】这个请求头的,所以去掉前面的头
            var auth = authorization.Split(" ");
            var handler = new JwtSecurityTokenHandler();
            //反解密,获取其中的Claims
            var payload = handler.ReadJwtToken(auth).Payload;
            var claims = payload.Claims;
            return claims;
      }

      /// <summary>
      /// 解析得到User
      /// </summary>
      /// <param name="request"></param>
      /// <returns></returns>
      public JwtUserModel DecodeToUser(HttpRequest request)
      {
            var claims = Decode(request);
            var user = new JwtUserModel()
            {
                UserId = claims.Where(t => t.Type == "UserId").First().Value,
                UserName = claims.Where(t => t.Type == "UserName").First().Value,
                UserType = claims.Where(t => t.Type == "UserType").First().Value
            };
            return user;
      }
    }
}

namespace JtwTestWebApi.Models
{
    public class JwtUserModel
    {

      public string UserId { get; set; }

      public string UserName { get; set; }

      public string UserType { get; set; }
    }
}

using JtwTestWebApi.Models;using JtwTestWebApi.Utils;using Microsoft.Extensions.Configuration;using Microsoft.OpenApi.Models;using System.IdentityModel.Tokens.Jwt;using System.Reflection;namespace JtwTestWebApi{    public class Program    {      public static void Main(string[] args)      {            var builder = WebApplication.CreateBuilder(args);            var MyPolicy = "MyPolicy";            // Add services to the container.            builder.Services.AddControllers();            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle            builder.Services.AddEndpointsApiExplorer();            builder.Services.AddSwaggerGen();            //为了防止配套太多,代码杂乱。这里自定义了一个方法            AddMyService(builder);            var app = builder.Build();            // Configure the HTTP request pipeline.            if (app.Environment.IsDevelopment())            {                            }            app.UseSwagger();            app.UseSwaggerUI();            app.UseStatusCodePagesWithRedirects("/swagger/index.html");            app.UseHttpsRedirection();
            app.UseAuthentication();//要在授权之前认证,这个和特性有关
            app.UseAuthorization();
            app.MapControllers();            app.Run();      }      /// <summary>      /// 为了防止代码过于痴肥,将新的配置放在这里写      /// </summary>      /// <param name="builder"></param>      public static void AddMyService(WebApplicationBuilder builder)      {            #region 默认的Webapi配置            builder.Services.AddCors(options =>            {                options.AddPolicy("MyPolicy", policy =>                {                  policy.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod();                });            });            builder.Services.AddSwaggerGen(options =>            {                options.SwaggerDoc("v1", new OpenApiInfo                {                  Version = "v1",                  Title = "API标题",                  Description = $"API形貌,v1版本"                });                var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";                //IncludeXmlComments 第二参数 true 则表现 控制器 解释                options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename), true);                JwtHelper.SwaggerAddJwtHeader(options);            });            #endregion             #region 添加Jwt服务            var jwtConfig = new JwtConfig();            builder.Configuration.Bind("JwtConfig",jwtConfig);            var jwtHelper = new JwtHelper() {                JwtConfig = jwtConfig            };            //将JwtHelper添加到Services内里            builder.Services.AddSingleton<JwtHelper>(jwtHelper);            jwtHelper.AddJwtService(builder.Services);            #endregion      }    }} appsettings.json中添加
"JwtConfig": {
    "SecretKey": "lisheng741@qq.comlisheng741@qq.com",
    "Issuer": "WebAppIssuer",
    "Audience": "WebAppAudience",
    "Expired": 30 // 过期时间(30min)
}
总结

Jwt其实也不是特别难,就是第一次配置的时间容易被绕晕。Jwt的策略我暂时先跳过了,对于解决普通题目一般来说已经够用了。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: ASP.NET Core 8.0 WebApi 从零开始学习JWT登录认证