从壹开始先后端分离【 .NET Core2.2/3.0 +Vue2.0 】框架之五 || Swagger的使用 3.3 JWT权限验证【必看】

本文3.0版本文章

https://mp.weixin.qq.com/s/7135y3MkUlPIp-flfwscightml

 

 

前言

关于JWT一共三篇 姊妹篇,内容分别从简单到复杂,必定要多看多想:vue

      1、Swagger的使用 3.3 JWT权限验证【修改】android

      2、解决JWT权限验证过时问题git

      3、JWT完美实现权限与接口的动态分配github

 

本文章不只在Blog.Core 框架里有代码,并且我也单写了一个关于 JWT 的小demo,在文章末,你们能够下载看看。web

书接上文,在前边的两篇文章中,咱们简单提到了接口文档神器Swagger,算法

三 || Swagger的使用 3.1》、json

四 || Swagger的使用 3.2》,后端

 

两个文章中,也对常见的几个问题作了简单的讨论,最后还剩下一个小问题,api

一、如何给接口实现权限验证?

其实关于这一块,我思考了下,由于毕竟个人项目中是使用的vue + api 搭建一个前台展现,大部分页面都没有涉及到权限验证,原本要忽略这一章节,但是犹豫再三,仍是给你们简单分析了下,我的仍是但愿陪你们一直搭建一个较为强大的,只要是涉及到后端那必定就须要 登陆=》验证了,本文主要是参考网友https://www.cnblogs.com/RayWang/p/9255093.html的思路,我本身稍加改动,你们均可以看看。

根据维基百科定义,JWT(读做 [/dʒɒt/]),即JSON Web Tokens,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT一般由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。它是一种用于双方之间传递安全信息的表述性声明规范。JWT做为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法,从而使通讯双方实现以JSON对象的形式安全的传递信息。

以上是JWT的官方解释,能够看出JWT并非一种只能权限验证的工具,而是一种标准化的数据传输规范。因此,只要是在系统之间须要传输简短但却须要必定安全等级的数据时,均可以使用JWT规范来传输。规范是不因平台而受限制的,这也是JWT作为受权验证能够跨平台的缘由。

若是理解仍是有困难的话,咱们能够拿JWT和JSON类比:

JSON是一种轻量级的数据交换格式,是一种数据层次结构规范。它并非只用来给接口传递数据的工具,只要有层级结构的数据均可以使用JSON来存储和表示。固然,JSON也是跨平台的,无论是Win仍是Linux,.NET仍是Java,均可以使用它做为数据传输形式。

1)客户端向受权服务系统发起请求,申请获取“令牌”。

2)受权服务根据用户身份,生成一张专属“令牌”,并将该“令牌”以JWT规范返回给客户端

3)客户端将获取到的“令牌”放到http请求的headers中后,向主服务系统发起请求。主服务系统收到请求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,而后作出相应的处理(赞成或拒绝返回资源)


 

 

零、生成 Token 令牌

关于JWT受权,其实过程是很简单的,你们其实这个时候静下心想想就能明白,这个就是四步走:

首先咱们须要一个具备必定规则的 Token 令牌,也就是 JWT 令牌(好比咱们的公司门禁卡),//登陆

而后呢,咱们再定义哪些地方须要什么样的角色(好比领导办公室咱们是没办法进去的),//受权机制

接下来,整个公司须要定一个规则,就是如何对这个 Token 进行验证,不能随便写个字条,这样容易被造假(好比咱们公司门上的每一道刷卡机),//认证方案

最后,就是安所有门,开启认证中间件服务(那这个服务能够关闭的,好比咱们电影里看到的黑客会把这个服务给关掉,这样整个公司安保就形同虚设了)。//开启中间件

 

那如今咱们就是须要一个具备必定规则的 Token 令牌,你们能够参考:

这个实体类就是用来生成 Token 的,代码记录以下:

    public class JwtHelper
    {

        /// <summary>
        /// 颁发JWT字符串
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJwt(TokenModelJwt tokenModel)
        {
            // 本身封装的 appsettign.json 操做类,看下文
            string iss = Appsettings.app(new string[] { "Audience", "Issuer" });
            string aud = Appsettings.app(new string[] { "Audience", "Audience" });
            string secret = Appsettings.app(new string[] { "Audience", "Secret" });

            //var claims = new Claim[] //old
            var claims = new List<Claim>
                {
                 /*
                 * 特别重要:
                   一、这里将用户的部分信息,好比 uid 存到了Claim 中,若是你想知道如何在其余地方将这个 uid从 Token 中取出来,请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了!
                   二、你也能够研究下 HttpContext.User.Claims ,具体的你能够看看 Policys/PermissionHandler.cs 类中是如何使用的。
                 */

                    

                new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()),
                new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),
                new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                //这个就是过时时间,目前是过时1000秒,可自定义,注意JWT有本身的缓冲过时时间
                new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds()}"),
                new Claim(JwtRegisteredClaimNames.Iss,iss),
                new Claim(JwtRegisteredClaimNames.Aud,aud),
                
                //new Claim(ClaimTypes.Role,tokenModel.Role),//为了解决一个用户多个角色(好比:Admin,System),用下边的方法
               };

            // 能够将一个用户的多个角色所有赋予;
            // 做者:DX 提供技术支持;
            claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s)));



            //秘钥 (SymmetricSecurityKey 对安全性的要求,密钥的长度过短会报出异常)
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var jwt = new JwtSecurityToken(
                issuer: iss,
                claims: claims,
                signingCredentials: creds);

            var jwtHandler = new JwtSecurityTokenHandler();
            var encodedJwt = jwtHandler.WriteToken(jwt);

            return encodedJwt;
        }

        /// <summary>
        /// 解析
        /// </summary>
        /// <param name="jwtStr"></param>
        /// <returns></returns>
        public static TokenModelJwt SerializeJwt(string jwtStr)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
            object role;
            try
            {
                jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
            var tm = new TokenModelJwt
            {
                Uid = (jwtToken.Id).ObjToInt(),
                Role = role != null ? role.ObjToString() : "",
            };
            return tm;
        }
    }

    /// <summary>
    /// 令牌
    /// </summary>
    public class TokenModelJwt
    {
        /// <summary>
        /// Id
        /// </summary>
        public long Uid { get; set; }
        /// <summary>
        /// 角色
        /// </summary>
        public string Role { get; set; }
        /// <summary>
        /// 职能
        /// </summary>
        public string Work { get; set; }

    }

 

    public class Appsettings
    {
        static IConfiguration Configuration { get; set; }

        //static Appsettings()
        //{
        //    //ReloadOnChange = true 当appsettings.json被修改时从新加载
        //    Configuration = new ConfigurationBuilder()
        //    .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true })//请注意要把当前appsetting.json 文件->右键->属性->复制到输出目录->始终复制
        //    .Build();
        //}

        static Appsettings()
        {
            string Path = "appsettings.json";
            {
                //若是你把配置文件 是 根据环境变量来分开了,能够这样写
                //Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json";
            }

            //Configuration = new ConfigurationBuilder()
            //.Add(new JsonConfigurationSource { Path = Path, ReloadOnChange = true })//请注意要把当前appsetting.json 文件->右键->属性->复制到输出目录->始终复制
            //.Build();

            Configuration = new ConfigurationBuilder()
               .SetBasePath(Directory.GetCurrentDirectory())
               .Add(new JsonConfigurationSource { Path = Path, Optional = false, ReloadOnChange = true })//这样的话,能够直接读目录里的json文件,而不是 bin 文件夹下的,因此不用修改复制属性
               .Build();


        }

        /// <summary>
        /// 封装要操做的字符
        /// </summary>
        /// <param name="sections"></param>
        /// <returns></returns>
        public static string app(params string[] sections)
        {
            try
            {
                var val = string.Empty;
                for (int i = 0; i < sections.Length; i++)
                {
                    val += sections[i] + ":";
                }

                return Configuration[val.TrimEnd(':')];
            }
            catch (Exception)
            {
                return "";
            }

        }
    }
Appsettings —— appsetting.json 操做类

 

 

 

这个接口如何调用呢,很简单,就是咱们的登陆api:

      public async Task<object> GetJwtStr(string name, string pass)
        {
            string jwtStr = string.Empty;
            bool suc = false;

// 获取用户的角色名,请暂时忽略其内部是如何获取的,能够直接用 var userRole="Admin"; 来代替更好理解。 var userRole = await _sysUserInfoServices.GetUserRoleNameStr(name, pass);
      
if (userRole != null) {
// 将用户id和角色名,做为单独的自定义变量封装进 token 字符串中。 TokenModelJwt tokenModel
= new TokenModelJwt {Uid = 1, Role = userRole}; jwtStr = JwtHelper.IssueJwt(tokenModel);//登陆,获取到必定规则的 Token 令牌 suc = true; } else { jwtStr = "login fail!!!"; } return Ok(new { success = suc, token = jwtStr }); }

 

 /// <summary>
 /// 令牌
 /// </summary>
 public class TokenModelJwt
    {
        /// <summary>
        /// Id
        /// </summary>
        public long Uid { get; set; }
        /// <summary>
        /// 角色
        /// </summary>
        public string Role { get; set; }
        /// <summary>
        /// 职能
        /// </summary>
        public string Work { get; set; }

    }

 

如今咱们获取到Token了,那如何进行受权认证呢,别着急,重头戏立刻到来!

 

1、JWT受权认证流程——自定义中间件

在以前的搭建中,swagger已经基本成型,其实其功能之多,不是我这三篇所能写完的,想要添加权限,先从服务开始

0、Swagger中开启JWT服务

咱们要测试 JWT 受权认证,就一定要输入 Token令牌,那怎么输入呢,平时的话,咱们可使用 Postman 来控制输入,就是在请求的时候,在 Header 中,添加Authorization属性,

可是咱们如今使用了 Swagger 做为接口文档,那怎么输入呢,别着急, Swagger 已经帮咱们实现了这个录入 Token令牌的功能:

在ConfigureServices  -> AddSwaggerGen 服务中,增长如下红色代码,注意是swagger服务内部

/// <summary>
        /// ConfigureServices 方法
        /// </summary>
        /// <param name="services"></param>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            #region Swagger
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Version = "v0.1.0",
                    Title = "Blog.Core API",
                    Description = "框架说明文档",
                    TermsOfService = "None",
                    Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" }
                });

                //就是这里

                #region 读取xml信息
                var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;
                var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名
                var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//这个就是Model层的xml文件名
                c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改
                c.IncludeXmlComments(xmlModelPath);
                #endregion

                #region Token绑定到ConfigureServices
                //添加header验证信息
                //c.OperationFilter<SwaggerHeader>();
                var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, };
                c.AddSecurityRequirement(security);
                //方案名称“Blog.Core”可自定义,上下一致便可
                c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme
                {
                    Description = "JWT受权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意二者之间是一个空格)\"",
                    Name = "Authorization",//jwt默认的参数名称
                    In = "header",//jwt默认存放Authorization信息的位置(请求头中)
                    Type = "apiKey" }); 
                #endregion


            });
            #endregion

        }

 

而后执行代码,就能够在 swagger/index.html 页面里看到这个Token入口了:

 

 

你们点开,看到输入框,在输入Token的时候,须要在Token令牌的前边加上Bearer (为何要加这个,下文会说明,请必定要注意看,必定要明白为啥要带,由于它涉及到了什么是受权,什么是认证,还要自定义认证中间件仍是官方认证中间件的区别,请注意看下文),好比是这样的:

可是请注意!若是你使用的是中间件 app.UseMiddleware<JwtTokenAuth>() ,要是使用 Bearer xxxx传值的时候,记得在中间件的方法中,把Token的 “Bearer 空格” 字符给截取掉,这样的:


 

1:API接口受权策略

这里能够直接在api接口上,直接设置该接口所对应的角色权限信息:

这个时候咱们就须要对每个接口设置对应的 Roles 信息,可是若是咱们的接口须要对应多个角色的时候,咱们就能够直接写多个:

 

这里有一个状况,若是角色多的话,不只不利于咱们阅读,还可能在配置的时候少一两个role,好比这个 api接口1 少了一个 system 的角色,再好比那个 api接口2 把 Admin 角色写成了 Adnin 这种没必要要的错误,真是很难受,那怎么办呢,欸!这个时候就出现了基于策略的受权机制:

咱们在 ConfigureService 中能够这么设置:

// 1【受权】、这个和上边的殊途同归,好处就是不用在controller中,写多个 roles 。
// 而后这么写 [Authorize(Policy = "Admin")]
services.AddAuthorization(options =>
{
    options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());
    options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());
    options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));
});

 

这样的话,咱们只须要在 controller 或者 action 上,直接写策略名就能够了:

 [HttpGet]
 [Authorize(Policy = "SystemOrAdmin")]
 public ActionResult<IEnumerable<string>> Get()
 {
     return new string[] { "value1", "value2" };
 }

 

这样咱们的第一步就完成了。继续走第二步,身份验证方案。

 关于受权认证有两种方式,可使用官方的认证方式,也可使用自定义中间件的方法,具体请往下看,我们先说说如何进行自定义认证。

 

二、自定义认证之身份验证设置

上边第一步中,我们已经对每个接口api设置好了 受权机制 ,那这里就要开始认证,我们先看看如何实现自定义的认证:

 

JwtTokenAuth,一个中间件,用来过滤每个http请求,就是每当一个用户发送请求的时候,都先走这一步,而后再去访问http请求的接口

 

    public class JwtTokenAuth
    {
        // 中间件必定要有一个next,将管道能够正常的走下去
        private readonly RequestDelegate _next;
        public JwtTokenAuth(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext httpContext)
        {

            //检测是否包含'Authorization'请求头
            if (!httpContext.Request.Headers.ContainsKey("Authorization"))
            {
                return _next(httpContext);
            }
            var tokenHeader = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");

            try
            {
                if (tokenHeader.Length >= 128)
                {
                    TokenModelJwt tm = JwtHelper.SerializeJwt(tokenHeader);

                    //受权 Claim 关键
                    var claimList = new List<Claim>();
                    var claim = new Claim(ClaimTypes.Role, tm.Role);
                    claimList.Add(claim);
                    var identity = new ClaimsIdentity(claimList);
                    var principal = new ClaimsPrincipal(identity);
                    httpContext.User = principal;
                }

            }
            catch (Exception e)
            {
                Console.WriteLine($"{DateTime.Now} middleware wrong:{e.Message}");
            }
            return _next(httpContext);
        }

    }
    // 这里定义一个中间件Helper,主要做用就是给当前模块的中间件取一个别名
    public static class MiddlewareHelpers
    {
        public static IApplicationBuilder UseJwtTokenAuth(this IApplicationBuilder app)
        {
            return app.UseMiddleware<JwtTokenAuth>();
        }
    }

 

 

 

 
前两步我们都完成了,从受权到自定义身份验证方案,就剩下最后一步,开启中间件了。
 

3:开启自定义认证中间件,实现Http信道拦截

这个很简单,只须要在 startup.cs -> Configure 中配置认证中间件
//自定义认证中间件
app.UseJwtTokenAuth(); //也能够app.UseMiddleware<JwtTokenAuth>();

 

4:开始测试

这个时候咱们的自定义JWT受权认证已经结束了,咱们开始测试,假设对某一个 api接口设置了权限:

 

在咱们没有输入 Token 的时候,点击测试接口会报错:

 

 

InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
//没有指定身份验证方案, 也没有发现默认挑战方案。

这个错误很明显,就是说咱们没有配置默认的认证方案,也没有自定义身份验证方案,

可是这个时候咱们再进行试验:

刚刚上边的状况是咱们没有输入 Token ,可是若是咱们输入token呢?看看是否是又会报错?

 

 

咱们发现了什么?!!没有报错,这是由于什么?欸,聪明的你应该想到了,请往下看,什么是 声明主体 ClaimsPrincipal 。

 

五、声明主体 ClaimsPrincipal 是如何保存的?

在上边,咱们解决了一些问题,同时也出现了一个问题,就是为何不输入 Token 就报错了,而输入了 Bearer xxxxxxxxxxx 这样的Token 就不报错了呢?这里要说到 声明主体的做用了。

就是咱们上边写的自定义中间件,你们能够再来看看:

      // 自定义认证中间件,咱们省略部分代码,来分析分析  
      public Task Invoke(HttpContext httpContext)
        {

            //检测是否包含'Authorization'请求头
            if (!httpContext.Request.Headers.ContainsKey("Authorization"))
            {
                //直接返回了 http 信道 ,就出现了咱们上边的报错,没有指定身份验证方案, 也没有发现默认挑战方案
                return _next(httpContext);
            }

             //可是!请注意,这个时候咱们输入了 token,咱们就会在 httpcontext 上下文中,添加上咱们本身自定义的身份验证方案!!!这就是没有继续报错的根本缘由
             var tokenHeader = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
             //........
             //受权
             var claimList = new List<Claim>();
             var claim = new Claim(ClaimTypes.Role, tm.Role);
             claimList.Add(claim);
             var identity = new ClaimsIdentity(claimList);
             var principal = new ClaimsPrincipal(identity);
             httpContext.User = principal;
            }

            return _next(httpContext);
        }

 

 这个时候你就应该明白了吧,

一、首先咱们自定义受权认证,为啥能够不用进行下边截图中官方认证那一块的配置:

 

 

由于这一块官方的服务,就等同于咱们的自定义身份验证方案——中间件

二、你应该明白,为何不输入token的时候报错,而输入了就不报错了?

由于没有输入的时候,直接 return了,并无在 httpContext 上下文中,进行配置声明主体 httpContext.User = principal

因此说,咱们不管是自定义中间件的自定义身份验证方案,仍是官方的认证方案,只要咱们的登陆了,也就是说,只要咱们实现了某种规则:

在 Http 的 Header 里,增长属性Authorization ,并赋值 :Bearer xxxxxxxxxxxxxx;

 

这样,就会触发咱们的内部服务,将当前 token 所携带的信息,进行自动解码,而后填充到声明主体里(自定义中间件须要手动配置,官方的自动就实现该操做),

因此这个时候咱们就能够轻松的拿到想到的东西,好比这里这些:

 

 

六、无策略依然受权错误

上边我们说到了,若是咱们自定义中间件的话,在中间件中,咱们在 Claims 添加了角色的相关权限:

并且很天然的在 接口中,也是分为两种状况:要么没有加权限,要么就是基于角色的加权:

 

 可是若是这个时候,咱们直接对接口增长 无任何策略 的加权:

 

就是没有任何的策略,咱们登陆,而后添加 token,一看,仍是报错了!具体的来看动图:

 

 


原本 [Authorize] 这种 无策略 的受权,按理说只须要咱们登陆了就能够了,不须要其余任何限制就能够访问,可是如今依然报错401 ,证实咱们的中间件并不能对这种方案起到效果,你可能会问,那带有 Roles=“Admin” 的为啥能够呢?反而这种无策略的不行呢,我我的感受可能仍是中间件我们设计的解决方案就是基于角色受权的那种,(我也再研究研究,看看能不能完善下这个自定义中间件,使它能适应这个 无具体策略 的加权方案,可是可能写到最后,就是无限接近官方的受权中间件了哈哈)。

这个时候咱们发现,自定义中间件仍是挺麻烦的,可是你经过本身使用自定义受权中间件,不只仅能够了解到中间件的使用,还能够了解 netcore 究竟是如何受权的机制,可是我仍是建议你们使用官方的认证方案,毕竟他们考虑的很全面的。

 

那么若是咱们想要用官方的认证方案呢,要怎么写呢?请往下看:

 

2、JWT受权认证流程——官方认证

上边我们说完了自定义中间件的形式,发现了也方便的地方,也有不方便之处,虽然灵活的使用了自定义身份验证,可是毕竟很受限,并且也没法对过时时间进行判断,之后的文章你会看到《36 ║解决JWT自定义中间件受权过时问题》,这里先不说,重点说说,如何经过官方认证来实现。

1:API接口受权策略

和上边自定义的过程如出一辙,略。

 

二、官方默认认证配置

在刚刚上边,我们说到了一个错误,不知道还有没有印象:
No authenticationScheme was specified, and there was no DefaultChallengeScheme found. 
就是这个,自定义认证中间件呢,就是前者,那官方的,就是后者 DefaultChallengeScheme;
 
很简单,只须要在 configureService 中,添加【统一认证】便可:
安装 nuget 包 dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
 //2.1【认证】
 services.AddAuthentication(x =>
 {
     //看这个单词熟悉么?没错,就是上边错误里的那个。
     x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
     x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
 })// 也能够直接写字符串,AddAuthentication("Bearer")
  .AddJwtBearer(o =>
  {
      o.TokenValidationParameters = new TokenValidationParameters
      {
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = signingKey,//参数配置在下边
          ValidateIssuer = true,
          ValidIssuer = audienceConfig["Issuer"],//发行人
          ValidateAudience = true,
          ValidAudience = audienceConfig["Audience"],//订阅人
          ValidateLifetime = true,
          ClockSkew = TimeSpan.Zero,
          RequireExpirationTime = true,
      };

  });

 

上边代码中出现的部分参数定义(若是还看不懂,请看项目代码)

 #region 参数
 //读取配置文件
 var audienceConfig = Configuration.GetSection("Audience");
 var symmetricKeyAsBase64 = audienceConfig["Secret"];
 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
 var signingKey = new SymmetricSecurityKey(keyByteArray);


 var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);

 

 

具体的每一个配置的含义呢,个人代码里都有,你们本身能够看看,都很简单。

划重点:咱们就是用的这个官方默认的方案,来替换了咱们自定义中间件的身份验证方案,从而达到目的,说白了,就是官方封装了一套方案,这样咱们就不用写中间件了。

 

三、配置官方认证中间件

这个很简单,仍是在 configure 中添加:
 //若是你想使用官方认证,必须在上边ConfigureService 中,配置JWT的认证服务 (.AddAuthentication 和 .AddJwtBearer 两者缺一不可)
 app.UseAuthentication();

这样就完成了,结果也不用看了,你们自行测试便可,不管添加或者不添加 token ,都不会报错。

 

 

四、补充:什么是 Claim

若是对 claim[] 定义不是很理解,能够看看dudu大神的解释《理解ASP.NET Core验证模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不读的英文博文》:

这篇英文博文是 Andrew Lock 写的 Introduction to Authentication with ASP.NET Core 。

如下是简单的阅读笔记:

-----------------------------------

ASP.NET Core 的验证模型是 claims-based authentication 。Claim 是对被验证主体特征的一种表述,好比:登陆用户名是...,email是...,用户Id是...,其中的“登陆用户名”,“email”,“用户Id”就是ClaimType。

You can think of claims as being a statement about...That statement consists of a name and a value.

对应现实中的事物,好比驾照,驾照中的“身份证号码:xxx”是一个claim,“姓名:xxx”是另外一个claim。

一组claims构成了一个identity,具备这些claims的identity就是 ClaimsIdentity ,驾照就是一种ClaimsIdentity,能够把ClaimsIdentity理解为“证件”,驾照是一种证件,护照也是一种证件。

ClaimsIdentity的持有者就是 ClaimsPrincipal ,一个ClaimsPrincipal能够持有多个ClaimsIdentity,就好比一我的既持有驾照,又持有护照。

------------------------------------

理解了Claim, ClaimsIdentity, ClaimsPrincipal这三个概念,就能理解生成登陆Cookie为何要用下面的代码?

var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, loginName) }, "Basic");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await context.Authentication.SignInAsync(_cookieAuthOptions.AuthenticationScheme, claimsPrincipal);
要用Cookie表明一个经过验证的主体,必须包含Claim, ClaimsIdentity, ClaimsPrincipal这三个信息,以一个持有合法驾照的人作比方,ClaimsPrincipal就是持有证件的人,ClaimsIdentity就是证件,"Basic"就是证件类型(这里假设是驾照),Claim就是驾照中的信息。 

 

五、其余注意点

一、而后再Startup的Configure中,将TokenAuth注册中间件

注意1:HTTP管道是有前后顺序的,必定要写在 app.Mvc() 以前,不然不起做用。

 

注意2:这里咱们是自定义了认证中间件,来对JWT的字符串进行自定义受权认证,因此上边都很正常,甚至咱们的Token能够不用带 Bearer 特定字符串,若是你之后遇到了使用官方认证中间件 UseAuthentication(),那么就必须在 configureService 中对认证进行配置(并且Token传递的时候,也必须带上"Bearer " 这样的特定字符串,这也就是解释了上文,为啥要带Bearer),这里先打个预防针,由于个人最新 Github 上已经使用了官方的认证中间件,因此除了上边配置的那些服务外,还须要配置 Service.AddAuthentication 和 Service.AddJwtBearer 两个服务。

// 若是你想使用官方认证,必须在上边ConfigureService 中,配置JWT的认证服务
// .AddAuthentication 和 .AddJwtBearer 两者缺一不可
app.UseAuthentication();

 

 若是你感受上边没看懂,继续用下边的知识点来巩固吧!

 

3、核心知识点梳理

如下是参考大神文章:@ASP.NET Core 认证与受权[4]:JwtBearer认证 ,必定要多看多想,下边的代码我没有试验正确性,你们看个意思便可,不用纠结正确与否,重点跟着这个系列日后走就行。

一、Bearer认证

HTTP提供了一套标准的身份验证框架:服务器能够用来针对客户端的请求发送质询(challenge),客户端根据质询提供身份验证凭证。质询与应答的工做流程以下:服务器端向客户端返回401(Unauthorized,未受权)状态码,并在WWW-Authenticate头中添加如何进行验证的信息,其中至少包含有一种质询方式。而后客户端能够在请求中添加Authorization头进行验证,其Value为身份验证的凭证信息。

HTTPAuth

在HTTP标准验证方案中,咱们比较熟悉的是"Basic"和"Digest",前者将用户名密码使用BASE64编码后做为验证凭证,后者是Basic的升级版,更加安全,由于Basic是明文传输密码信息,而Digest是加密后传输。在前文介绍的Cookie认证属于Form认证,并不属于HTTP标准验证。

本文要介绍的Bearer验证也属于HTTP协议标准验证,它随着OAuth协议而开始流行,详细定义见: RFC 6570

A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).

Bearer验证中的凭证称为BEARER_TOKEN,或者是access_token,它的颁发和验证彻底由咱们本身的应用程序来控制,而不依赖于系统和Web服务器,Bearer验证的标准请求方式以下:

Authorization: Bearer [BEARER_TOKEN]

那么使用Bearer验证有什么好处呢?

  • CORS: cookies + CORS 并不能跨不一样的域名。而Bearer验证在任何域名下均可以使用HTTP header头部来传输用户信息。

  • 对移动端友好: 当你在一个原平生台(iOS, Android, WindowsPhone等)时,使用Cookie验证并非一个好主意,由于你得和Cookie容器打交道,而使用Bearer验证则简单的多。

  • CSRF: 由于Bearer验证再也不依赖于cookies, 也就避免了跨站请求攻击。

  • 标准:在Cookie认证中,用户未登陆时,返回一个302到登陆页面,这在非浏览器状况下很难处理,而Bearer验证则返回的是标准的401 challenge

二、JWT(JSON WEB TOKEN)

上面介绍的Bearer认证,其核心即是BEARER_TOKEN,而最流行的Token编码方式即是:JSON WEB TOKEN。

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明通常被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够增长一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT是由.分割的以下三部分组成:

头部(Header)

Header 通常由两个部分组成:

  • alg
  • typ

alg是是所使用的hash算法,如:HMAC SHA256或RSA,typ是Token的类型,在这里就是:JWT。

{
  "alg": "HS256", "typ": "JWT" }

而后使用Base64Url编码成第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>

载荷(Payload)

这一部分是JWT主要的信息存储部分,其中包含了许多种的声明(claims)。

Claims的实体通常包含用户和一些元数据,这些claims分红三种类型:

  • reserved claims:预约义的 一些声明,并非强制的可是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(这里都使用三个字母的缘由是保证 JWT 的紧凑)。

  • public claims: 公有声明,这个部分能够随便定义,可是要注意和 IANA JSON Web Token 冲突。

  • private claims: 私有声明,这个部分是共享被认定信息中自定义部分。

一个简单的Pyload能够是这样子的:

{
  "sub": "1234567890", "name": "John Doe", "admin": true }

这部分一样使用Base64Url编码成第二部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>

签名(Signature)

Signature是用来验证发送者的JWT的同时也能确保在期间不被篡改。

在建立该部分时候你应该已经有了编码后的Header和Payload,而后使用保存在服务端的秘钥对其签名,一个完整的JWT以下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

所以使用JWT具备以下好处:

  • 通用:由于json的通用性,因此JWT是能够进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等不少语言均可以使用。

  • 紧凑:JWT的构成很是简单,字节占用很小,能够经过 GET、POST 等放在 HTTP 的 header 中,很是便于传输。

  • 扩展:JWT是自我包涵的,包含了必要的全部信息,不须要在服务端保存会话信息, 很是易于应用的扩展。

关于更多JWT的介绍,网上很是多,这里就再也不多作介绍。下面,演示一下 ASP.NET Core 中 JwtBearer 认证的使用方式。

三、示例

模拟Token

ASP.NET Core 内置的JwtBearer验证,并不包含Token的发放,咱们先模拟一个简单的实现:

[HttpPost("authenticate")] public IActionResult Authenticate([FromBody]UserDto userDto) { var user = _store.FindUser(userDto.UserName, userDto.Password); if (user == null) return Unauthorized(); var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(Consts.Secret); var authTime = DateTime.UtcNow; var expiresAt = authTime.AddDays(7); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new Claim[] { new Claim(JwtClaimTypes.Audience,"api"), new Claim(JwtClaimTypes.Issuer,"http://localhost:5200"), new Claim(JwtClaimTypes.Id, user.Id.ToString()), new Claim(JwtClaimTypes.Name, user.Name), new Claim(JwtClaimTypes.Email, user.Email), new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber) }), Expires = expiresAt, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); return Ok(new { access_token = tokenString, token_type = "Bearer", profile = new { sid = user.Id, name = user.Name, auth_time = new DateTimeOffset(authTime).ToUnixTimeSeconds(), expires_at = new DateTimeOffset(expiresAt).ToUnixTimeSeconds() } }); }

如上,使用微软提供的Microsoft.IdentityModel.Tokens帮助类(源码地址:azure-activedirectory-identitymodel-extensions-for-dotnet),能够很容易的建立出JwtToen,就再也不多说。

注册JwtBearer认证

首先添加JwtBearer包引用:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 2.0.0

而后在Startup类中添加以下配置:

public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role, ValidIssuer = "http://localhost:5200", ValidAudience = "api", IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Consts.Secret)) /***********************************TokenValidationParameters的参数默认值***********************************/ // RequireSignedTokens = true, // SaveSigninToken = false, // ValidateActor = false, // 将下面两个参数设置为false,能够不验证Issuer和Audience,可是不建议这样作。 // ValidateAudience = true, // ValidateIssuer = true, // ValidateIssuerSigningKey = false, // 是否要求Token的Claims中必须包含Expires // RequireExpirationTime = true, // 容许的服务器时间偏移量 // ClockSkew = TimeSpan.FromSeconds(300), // 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比 // ValidateLifetime = true }; }); } public void Configure(IApplicationBuilder app) { app.UseAuthentication(); }

JwtBearerOptions的配置中,一般IssuerSigningKey(签名秘钥)ValidIssuer(Token颁发机构)ValidAudience(颁发给谁) 三个参数是必须的,后二者用于与TokenClaims中的IssuerAudience进行对比,不一致则验证失败(与上面发放Token中的Claims对应)。

NameClaimTypeRoleClaimType需与Token中的ClaimType一致,在IdentityServer中也是使用的JwtClaimTypes,不然会形成User.Identity.Name为空等问题。

添加受保护资源

建立一个须要受权的控制器,直接使用Authorize便可:

[Authorize] [Route("api/[controller]")] public class SampleDataController : Controller { [HttpGet("[action]")] public IEnumerable<WeatherForecast> WeatherForecasts() { return ... } }

运行

最后运行,直接访问/api/SampleData/WeatherForecasts,将返回一个401:

HTTP/1.1 401 Unauthorized
Server: Kestrel
Content-Length: 0
WWW-Authenticate: Bearer

让咱们调用api/oauth/authenticate,获取一个JWT:

请求:
POST http://localhost:5200/api/oauth/authenticate HTTP/1.1
content-type: application/json

{
  "username": "alice",
  "password": "alice"
}

响应:
HTTP/1.1 200 OK
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc","token_type":"Bearer","profile":{"sid":1,"name":"alice","auth_time":1509464340,"expires_at":1510069140}}

最后使用该Token,再次调用受保护资源:

GET http://localhost:5200/api/SampleData/WeatherForecasts HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc

受权成功,返回了预期的数据:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[{"dateFormatted":"2017/11/3","temperatureC":35,"summary":"Chilly","temperatureF":94}]

四、扩展

自定义Token获取方式

JwtBearer认证中,默认是经过Http的Authorization头来获取的,这也是最推荐的作法,可是在某些场景下,咱们可能会使用Url或者是Cookie来传递Token,那要怎么来实现呢?

其实实现起来很是简单,如前几章介绍的同样,JwtBearer也在认证的各个阶段为咱们提供了事件,来执行咱们的自定义逻辑:

.AddJwtBearer(o =>
{
    o.Events = new JwtBearerEvents() { OnMessageReceived = context => { context.Token = context.Request.Query["access_token"]; return Task.CompletedTask; } }; o.TokenValidationParameters = new TokenValidationParameters { ... };

而后在Url中添加access_token=[token],直接在浏览器中访问:

access_token_in_url

一样的,咱们也能够很容易的在Cookie中读取Token,就再也不演示。

除了OnMessageReceived外,还提供了以下几个事件:

  • TokenValidated:在Token验证经过后调用。

  • AuthenticationFailed: 认证失败时调用。

  • Challenge: 未受权时调用。

使用OIDC服务

在上面的示例中,咱们简单模拟的Token颁发,功能很是简单,并不适合在生产环境中使用,但是微软也没有提供OIDC服务的实现,好在.NET社区中提供了几种实现,可供咱们选择:

Name Description
AspNet.Security.OpenIdConnect.Server (ASOS) Low-level/protocol-first OpenID Connect server framework for ASP.NET Core and OWIN/Katana
IdentityServer4 OpenID Connect and OAuth 2.0 framework for ASP.NET Core - officially certified by the OpenID Foundation and under governance of the .NET Foundation
OpenIddict Easy-to-use OpenID Connect server for ASP.NET Core
PwdLess Simple, stateless, passwordless authentication for ASP.NET Core

咱们在这里使用IdentityServer4来搭建一个OIDC服务器,并添加以下配置:

/********************OIDC服务器代码片断********************/ public void ConfigureServices(IServiceCollection services) { services.AddMvc(); // 配置IdentitryServer services.AddIdentityServer() .AddInMemoryPersistedGrants() .AddInMemoryApiResources(Config.GetApis()) .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers()) .AddDeveloperSigningCredential(); } new Client { ClientId = "jwt.implicit", ClientName = "Implicit Client (Web)", AllowedGrantTypes = GrantTypes.Implicit, AllowAccessTokensViaBrowser = true, RedirectUris = { "http://localhost:5200/callback" }, PostLogoutRedirectUris = { "http://localhost:5200/home" }, AllowedCorsOrigins = { "http://localhost:5200" }, AllowedScopes = { "openid", "profile", "email", "api" }, }

而JwtBearer客户端的配置就更加简单了,由于OIDC具备配置发现的功能:

public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.Authority = "https://oidc.faasx.com/"; o.Audience = "api"; o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role, }; }); }

如上,最重要的是Authority参数,用来表示OIDC服务的地址,而后即可以自动发现IssuerIssuerSigningKey等配置,而o.Audienceo.TokenValidationParameters = new TokenValidationParameters { ValidAudience = "api" }是等效的,后面分析源码时会介绍。

OIDC兼容OAuth2协议,咱们可使用上一章介绍的受权码模式来获取Token,也能够直接用户名密码模式来获取Token:

请求:
POST https://oidc.faasx.com/connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=client.rop&client_secret=secret&grant_type=password&scope=api&username=alice&password=alice

响应:
HTTP/1.1 200 OK
Content-Type: application/json

{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlYzk5MjVlMmUzMTA2NmY2ZmU2ODgzMDRhZjU1ZmM0IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDk2NzI1NjksImV4cCI6MTUwOTY3NjE2OSwiaXNzIjoiaHR0cHM6Ly9vaWRjLmZhYXN4LmNvbSIsImF1ZCI6WyJodHRwczovL29pZGMuZmFhc3guY29tL3Jlc291cmNlcyIsImFwaSJdLCJjbGllbnRfaWQiOiJjbGllbnQucm9wIiwic3ViIjoiMDAxIiwiYXV0aF90aW1lIjoxNTA5NjcyNTY5LCJpZHAiOiJsb2NhbCIsIm5hbWUiOiJBbGljZSBTbWl0aCIsImVtYWlsIjoiQWxpY2VTbWl0aEBlbWFpbC5jb20iLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbInB3ZCJdfQ.PM93LThOZA3lkgPFVwieqGQQQtgmYDCY0oSFVmudv1hpKO6UaaZsmnn4ci9QjbGl5g2433JkDks5UIZsZ0xE62Qqq8PicPBBuaNoYrCf6dxR7j-0uZcoa7-FCKGu-0TrM8OL-NuMvN6_KEpbWa3jlkwibCK9YDIwJZilVoWUOrbbIEsKTa-DdLScmzHLUzksT8GBr0PAVhge9PRFiGqg8cgMLjsA62ZeDsR35f55BucSV5Pj0SAj26anYvrBNTHKOF7ze1DGW51Dbz6DRu1X7uEIxSzWiNi4cRVJ6Totjkwk5F78R9R38o_mYEdehZBjRHFe6zLd91hXcCKqOEh5eQ","expires_in":3600,"token_type":"Bearer"}

咱们使用https://jwt.io解析一下OIDC服务器颁发的Token中的Claims:

{
  "nbf": 1509672569, // 2017/11/3 1:29:29 NotBefore Token生效时间,在此以前不可用 "exp": 1509676169, // 2017/11/3 2:29:29 Expiration Token过时时间,在此以后不可用 "iss": "https://oidc.faasx.com", // Issuer 颁发者,一般为STS服务器地址 "aud": [ // Audience Token的做用对象,也就是被访问的资源服务器受权标识 "https://oidc.faasx.com/resources", "api" ], "client_id": "client.rop", // 客户端标识 "sub": "001", "auth_time": 1509672569, // Token颁发时间 "idp": "local", "name": "Alice Smith", "email": "AliceSmith@email.com", "scope": [ "api" ], "amr": [ "pwd" ] }

 

这一篇呢,写的比较潦草,主要是讲如何使用,具体的细节知识,仍是你们摸索,仍是那句话,这里只是抛砖引玉的做用哟,经过阅读本文,你会了解到,什么是JWT,如何添加配置.net core 中间件,如何使用Token验证,在之后的项目里你就能够在登陆的时候,调用Token,返回客户端,而后判断是否有相应的接口权限。

 

4、常见疑惑解析

一、JWT里会存在一些用户的信息,好比用户id、角色role 等等,这样会不会不安全,信息被泄露?

答:JWT 原本就是一种无状态的登陆受权认证,用来替代每次请求都须要输入用户名+密码的尴尬状况,存在一些不重要的明文很正常,只要不把隐私放出去就行,就算是被动机不良的人获得,也作不了什么事情。

二、生成 JWT 的时候须要 secret ,可是 解密的时候 为啥没有用到 secret ?

答:secret的做用,主要是用来防止 token 被伪造和篡改的,想一想上边的那个第一个问题,用户获得了你的令牌,获取到了你的我的信息,这个是没事儿的,他什么也干不了,可是若是用户本身随便的生成一个 token ,带上你的uid,岂不是随便就能够访问资源服务器了,因此这个时候就须要一个 secret 来生成 token,这样的话,就能保证数字签名的正确性。

  并且,在咱们资源服务器里,将token解析的时候,微软封装了方法,将secret进行校验了,这就是保证了token的安全性,从而保证咱们的资源api是安全的,你不信的话,能够用你网站的 token 来访问个人在线项目,就算是 uid,role等等所有正确,仍是不能访问个人网站,由于你不知道个人secret,因此你生成的令牌对个人是无效的。

 

能够看看这个视频:https://www.bilibili.com/video/av52076900?share_medium=android&share_source=qq&bbid=XZ786B57591674D68847894D8F16996AAFFB6&ts=1559452290064

 

 

 

5、结语

好啦!项目准备阶段就这么结束了,之后我们就能够直接用swagger来调试了,而不是每次都用F5运行等,接下来咱们就要正式开始搭建项目了,主要采用的是泛型仓储模式 Repository+Service,也是一种常见的模式。

6、Github

本系列开源地址

https://github.com/anjoy8/Blog.Core.git

本文章小Demo

https://github.com/anjoy8/BlogArti/tree/master/Blog.Core_JWT