驾一叶之扁舟 举匏樽以相属
寄蜉蝣于天地,渺沧海之一粟。哀吾生之须臾,羡长江之无穷。
挟飞仙以遨游,抱明月而长终。知不可乎骤得,托遗响于悲风。

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

本文3.0版本文章

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

 

下边的是我关于JWT的视频讲解,都可以看看

1、P2直播12期:授权与认证(1)_简单授权

2、P3直播13期:授权与认证(2)_授权与上下文

3、P4四、JWT详解(2.2版)

 

前言

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

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

其实关于这一块,我思考了下,因为毕竟我的项目中是使用的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 进行验证,不能随便写个字条,这样容易被造假(比如我们公司门上的每一道刷卡机),//jwtbearer认证方案

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

 

那现在我们就是需要一个具有一定规则的 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 List<Claim>
              {
                 /*
                 * 特别重要:
                   1、这里将用户的部分信息,比如 uid 存到了Claim 中,如果你想知道如何在其他地方将这个 uid从 Token 中取出来,请看下边的SerializeJwt() 方法,或者在整个解决方案,搜索这个方法,看哪里使用了!
                   2、你也可以研究下 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; }

    }

 

Appsettings操作类:

 /// <summary>
    /// appsettings.json操作类
    /// </summary>
    public class Appsettings
    {
        static IConfiguration Configuration { get; set; }
        static string contentPath { get; set; }

        public Appsettings(string contentPath)
        {
            string Path = "appsettings.json";

            //如果你把配置文件 是 根据环境变量来分开了,可以这样写
            //Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json";

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

        public Appsettings(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        /// <summary>
        /// 封装要操作的字符
        /// </summary>
        /// <param name="sections">节点配置</param>
        /// <returns></returns>
        public static string app(params string[] sections)
        {
            try
            {

                if (sections.Any())
                {
                    return Configuration[string.Join(":", sections)];
                }
            }
            catch (Exception) { }

            return "";
        }

        /// <summary>
        /// 递归获取配置信息数组
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sections"></param>
        /// <returns></returns>
        public static List<T> app<T>(params string[] sections)
        {
            List<T> list = new List<T>();
            // 引用 Microsoft.Extensions.Configuration.Binder 包
            Configuration.Bind(string.Join(":", sections), list);
            return list;
        }
    }

 

 

 

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

      public async Task<object> GetJwtStr(string name, string pass)
      { 
// 将用户id和角色名,作为单独的自定义变量封装进 token 字符串中。 TokenModelJwt tokenModel
= new TokenModelJwt {Uid = 1, Role = "Admin"}; var jwtStr = JwtHelper.IssueJwt(tokenModel);//登录,获取到一定规则的 Token 令牌 var suc = true; return Ok(new { success = suc, token = jwtStr }); }

 

现在我们获取到Token了,那如何进行授权和认证呢,别着急,重头戏马上到来!

 

 

 

一、JWT ——权限三步走

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

0、Swagger中开启JWT服务(netcore2.2)

我们要测试 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 = AppContext.BaseDirectory;
                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);//3.0不需要了,可以删掉
//方案名称“Blog.Core”可自定义,上下一致即可 c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)\"", Name = "Authorization",//jwt默认的参数名称 In = "header",//jwt默认存放Authorization信息的位置(请求头中) Type = "apiKey" });
#endregion }); #endregion }

 

NetCore 3.1 的写法:

 

 

 

 

 

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

 

 

 

 

大家点开,看到输入框,在输入Token的时候,需要在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"));//或的关系
    options.AddPolicy("SystemAndAdmin", policy => policy.RequireRole("Admin").RequireRole("System"));//且的关系
});

 

 

这样的话,我们只需要在 controller 或者 action 上,直接写策略名就可以了:

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

 

当然上边的都太简单的了,如果我想来一个复杂的自定义策略授权呢,你可以先把我这篇简单的学会,然后看看我的视频《P4直播14期:授权(3)_策略详解(高清)

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

 

2、第二步、配置认证服务

上边第一步中,咱们已经对每一个接口api设置好了 授权机制 ,那这里就要开始认证,咱们先看看如何实现JWT的Bearer认证,关于什么是Bearer认证呢,可以看下下边的知识点介绍,简单来说,就是定义的一套逻辑,用来将我们的Jwt三个部分进行处理和校验,你看我们登录的时候,有发行人,订阅人和数字密钥等,JWT Beaer 认证就是实现校验这些的功能。

如果不进行认证服务配置的话,会报错:
No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
 
很简单,只需要在 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,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲,默认好像是7分钟,你可以直接设置为0
          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);

 

 

具体的每个配置的含义呢,我的代码里都有,大家自己可以看看,都很简单。

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

 

3、配置官方认证中间件

 

这个很简单,还是在 configure 中添加:
 
 // netcore 2.2 版本
 app.UseAuthentication();

 

NETCore 3.1,一定要保证顺序正确:

 

 

 

 

 

这样就完成了,结果也不用看了,大家自行测试即可,无论添加或者不添加 token ,都不会报错,虽然不报错,但是如果不添加token的话,会401异常,这是正常的,毕竟我们已经加上了[Authorize]授权特性和认证中间件了。

 

 

4、发起登录请求

现在我们就只需要将上边的登录接口生成好的 Token 令牌,添加到 Swagger 的小锁里就行,就代表我们已经登录了(返回格式可能不太一样,忽略它):

 

 

 

 

 

 

 

 

下边是一些基本名词概念,可以简单看看,巩固印象

 

三、核心知识点梳理

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

1、什么是 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就是驾照中的信息。 

 

 

2、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

3、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 认证的使用方式。

  

4、扩展

自定义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社区中提供了几种实现,可供我们选择:

NameDescription
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,返回客户端,然后判断是否有相应的接口权限。

 

 

四、常见疑惑解析

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

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

2、生成 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

 

 

 

五、结语

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

 

六、Github

本系列开源地址

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

本文章小Demo

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

test
posted @ 2018-08-21 15:56  老张的哲学  阅读(47997)  评论(126编辑  收藏  举报
作者:老张的哲学
好好学习,天天向上
返回顶部小火箭
好友榜:
如果愿意,把你的博客地址放这里
jianshu.com/u/老张
SqlSugar codeisbug.com