zoukankan      html  css  js  c++  java
  • .Net Core3.0 WebApi 四:JWT权限验证

    .Net Core3.0 WebApi 目录

    什么是JWT

    之前也写过一篇,介绍JWT和oAuthor2的随笔。可以参考这篇。

    设计安全的API-JWT与OAuthor2     、    OAuth2、OpenID Connect简介

    这里还是简单介绍一下吧。

    根据维基百科定义,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 进行验证,不能随便写个字条,这样容易被造假(比如我们公司门上的每一道刷卡机),//认证方案

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

    生成Token令牌

    我们需要在appsettings.json中配置jwt参数的值 【注意】 SecretKey必须大于16个,是大于,不是大于等于

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "AllowedHosts": "*",
      "ConnectionStrings": {
        "ConnectionString": "Data Source=127.0.0.1;Initial Catalog=db;User ID=uid;Password=123456;Pooling=True;Max Pool Size=512;Connect Timeout=500;",
        "JwtSetting": {
          "Issuer": "jwtIssuer", //颁发者
          "Audience": "jwtAudience", //可以给哪些客户端使用
          "SecretKey": "chuangqianmingyueguang" //加密的Key
        }
      }
    }

    在api层,Nuget添加包IdentityModel,Microsoft.AspNetCore.Authentication.JwtBearer,Microsoft.AspNetCore.Authorization

     webapi.core.models类库新建TokenModel类

    /// <summary>
    /// 令牌
    /// </summary>
    public class TokenModel
    {
        /// <summary>
        /// Id
        /// </summary>
        public string Uid { get; set; }
        /// <summary>
        /// 角色
        /// </summary>
        public string Role { get; set; }
    
    }

    WebApi.Core.Infrastructure项目Helper文件夹,新建JwtHelper.cs

    public class JwtHelper
    {
        /// <summary>
        /// 颁发JWT字符串
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJwt(TokenModel tokenModel)
        {
            string iss = ConfigHelper.GetSectionValue("ConnectionStrings:JwtSetting:Issuer");
            string aud = ConfigHelper.GetSectionValue("ConnectionStrings:JwtSetting:Audience");
            string secret = ConfigHelper.GetSectionValue("ConnectionStrings:JwtSetting:SecretKey");
            //var claims = new Claim[] //old
            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(ClaimTypes.Expiration, DateTime.Now.AddSeconds(1000).ToString()),
                new Claim(JwtRegisteredClaimNames.Iss,iss),
                new Claim(JwtRegisteredClaimNames.Aud,aud),
    
    
               };
    
            // 可以将一个用户的多个角色全部赋予;
            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 TokenModel SerializeJwt(string jwtStr)
        {
            var jwtHandler = new JwtSecurityTokenHandler();

          if (!jwtHandler.CanReadToken(jwtStr))
          {
            return null;
          }
          string iss = ConfigHelper.GetSectionValue("Authentication:JwtSetting:Issuer");
          JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
          if (jwtToken.Issuer != iss)
          {
            return null;//不正确
          }
          if (jwtToken.ValidTo < DateTime.Now)
          {
            return null;//过期
          }

         object role;
            try
            {
                jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
            var tm = new TokenModel
            {
                Uid = jwtToken.Id.ToString(),
                Role = role != null ? role.ToString() : "",
            };
            return tm;
        }
    }

    获取Token

    UserController新建Login接口,用来获取token

    /// <summary>
    /// 登录
    /// </summary>
    /// <param name="role">用户</param>
    /// <returns></returns>  
    [HttpGet]
    public IActionResult Login(string role)
    {
        string jwtStr = string.Empty;
        bool suc = false;
    
        if (role != null)
        {
            // 将用户id和角色名,作为单独的自定义变量封装进 token 字符串中。
            TokenModel tokenModel = new TokenModel { Uid = "abcde", Role = role };
            jwtStr = JwtHelper.IssueJwt(tokenModel);//登录,获取到一定规则的 Token 令牌
            suc = true;
        }
        else
        {
            jwtStr = "login fail!!!";
        }
    
        return Ok(new
        {
            success = suc,
            token = jwtStr
        });
    }

    运行项目,使用swagger 调试login接口

     可以看到成功的获取了Token

    Swagger中开启JWT服务

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

    但是我们现在使用了 Swagger 作为接口文档,那怎么输入呢,别着急, Swagger 已经帮我们实现了这个录入 Token令牌的功能。

    在SwaggerSetUp.cs的AddSwaggerSetup方法的AddSwaggerGen服务中,增加以下代码:

     public static class SwaggerSetUp
     {
         public static void AddSwaggerSetup(this IServiceCollection services)
         {
    
             if (services == null) throw new ArgumentNullException(nameof(services));
    
             var ApiName = "Webapi.Core";
    
             services.AddSwaggerGen(c =>
             {
                 c.SwaggerDoc("V1", new OpenApiInfo
                 {
                     // {ApiName} 定义成全局变量,方便修改
                     Version = "V1",
                     Title = $"{ApiName} 接口文档——Netcore 3.0",
                     Description = $"{ApiName} HTTP API V1",
    
                 });
                 c.OrderActionsBy(o => o.RelativePath);
    
                 // 获取xml注释文件的目录
                 var xmlPath = Path.Combine(AppContext.BaseDirectory, "Webapi.Core.xml");
                 c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改
    
                 var xmlModelPath = Path.Combine(AppContext.BaseDirectory, "Webapi.Core.Model.xml");//这个就是Model层的xml文件名
                 c.IncludeXmlComments(xmlModelPath);
    
                 // 在header中添加token,传递到后台
                 c.OperationFilter<SecurityRequirementsOperationFilter>();
    
                 #region Token绑定到ConfigureServices
                 c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
                 {
                     Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)"",
                     Name = "Authorization",//jwt默认的参数名称
                     In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
                     Type = SecuritySchemeType.ApiKey
                 });
                 #endregion
    
             });
    
         }
     }

    运行项目,就可以看到这个Token的入口了:

     

    JWT授权认证

    Setup文件夹新建AuthorizationSetup.cs,新建注册服务方法

    namespace WebApi.Core.Api.SetUp
    {
        public static class AuthorizationSetup
        {
            public static void AddAuthorizationSetup(this IServiceCollection services)
            {
                if (services == null) throw new ArgumentNullException(nameof(services));
    
                // 1【授权】、这个和上边的异曲同工,好处就是不用在controller中,写多个 roles 。
                // 然后这么写 [Authorize(Policy = "Admin")]
                services.AddAuthorization(options =>
                {
                    options.AddPolicy("User", policy => policy.RequireRole("User").Build());
                    options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));
    
                });
    
                //读取配置文件
                var symmetricKeyAsBase64 = ConfigHelper.GetSectionValue("ConnectionStrings:JwtSetting:SecretKey");
                var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
                var signingKey = new SymmetricSecurityKey(keyByteArray);
                var Issuer = ConfigHelper.GetSectionValue("ConnectionStrings:JwtSetting:Issuer");
                var Audience = ConfigHelper.GetSectionValue("ConnectionStrings:JwtSetting:Audience");
    
    
    
                // 令牌验证参数
                var tokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = signingKey,
                    ValidateIssuer = true,
                    ValidIssuer = Issuer,//发行人
                    ValidateAudience = true,
                    ValidAudience = Audience,//订阅人
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.FromSeconds(30),
                    RequireExpirationTime = true,
                };
    
                //2.1【认证】、core自带官方JWT认证
                // 开启Bearer认证
                services.AddAuthentication(o =>
                {
                    o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    
                })
                 // 添加JwtBearer服务
                 .AddJwtBearer(o =>
                 {
                     o.TokenValidationParameters = tokenValidationParameters;
                     o.Events = new JwtBearerEvents
                     {
                         OnAuthenticationFailed = context =>
                         {
                             // 如果过期,则把<是否过期>添加到,返回头信息中
                             if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                             {
                                 context.Response.Headers.Add("Token-Expired", "true");
                             }
                             return Task.CompletedTask;
                         }
                     };
                 });
            }
        }
    }

    startup.cs的ConfigureServices方法添加验证服务

    //jwt授权验证
    services.AddAuthorizationSetup();

    Configure方法添加如下代码:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint($"/swagger/V1/swagger.json", "WebApi.Core V1");
    
            //路径配置,设置为空,表示直接在根域名(localhost:8001)访问该文件,注意localhost:8001/swagger是访问不到的,去launchSettings.json把launchUrl去掉,如果你想换一个路径,直接写名字即可,比如直接写c.RoutePrefix = "doc";
            c.RoutePrefix = "";
        });
    
        app.UseRouting();
    
        app.UseAuthentication();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            //endpoints.MapControllerRoute(
            //    name: "default",
            //    pattern: "{controller=Home}/{action=Index}/{id?}"
            //    );
        });
    }

    API接口授权策略

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

     /// <summary>
     /// 需要Admin权限
     /// </summary>
     /// <returns></returns>
     [HttpGet]
     [Authorize(Roles = "Admin")]
     public IActionResult Admin()
     {
         return Ok("hello admin");
     }
    
    
     /// <summary>
     /// 需要System权限
     /// </summary>
     /// <returns></returns>
     [HttpGet]
     [Authorize(Roles = "System")]
     public IActionResult System()
     {
         return Ok("hello System");
     }

    运行项目,获取一个Admin权限的token,并放到swagger 的权限验证按钮里面

     

     请求admin方法,可以访问

     请求system方法,访问失败,权限不足

     但是如果我们的接口需要对应多个角色的时候,我们就可以直接写多个:

    /// <summary>
    /// 需要System和Admin权限
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [Authorize(Policy = "SystemOrAdmin")]
    
    public IActionResult SystemAndAdmin()
    {
        return Ok("hello SystemOrAdmin");
    }

     这里有一个情况,如果角色多的话,不仅不利于我们阅读,还可能在配置的时候少一两个role,比如这个 api接口1 少了一个 system 的角色,再比如那个 api接口2 把 Admin 角色写成了 Adnin 这种不必要的错误,那怎么办呢,欸!这个时候就出现了基于策略的授权机制:

    我们在 AuthorizationSetup 中可以这么设置:

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

    /// <summary>
    /// 需要System和Admin权限
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [Authorize(Policy = "SystemOrAdmin")]
    
    public IActionResult SystemAndAdmin()
    {
        return Ok("hello SystemOrAdmin");
    }

    运行项目,分别请求system和admin的token,发现都可以访问这个请求

    解析Token

    添加一个接口用来解析token:

    /// <summary>
    /// 解析Token
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [Authorize]
    public IActionResult ParseToken()
    {
        //需要截取Bearer 
        var tokenHeader = HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
        var user = JwtHelper.SerializeJwt(tokenHeader);
        return Ok(user);
    
    }

    运行项目,生成一个Token,然后调用解析方法,可以看到程序解析了我们的Token

    常见疑惑解析

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

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

    2、生成 JWT 的时候需要 secret ,但是 解密的时候 为啥没有用到 secret ?

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

  • 相关阅读:
    Java常用的技术网站
    Eclipse启动Tomcat时发生java.lang.IllegalArgumentException: <sessionconfig> element is limited to 1 occurrence
    MySQL存储过程动态SQL语句的生成
    GitHub起步创建第一个项目
    安装Java的IDE Eclipse时出现java.net.SocketException,出现错误Installer failed,show.log
    转:POI操作Excel导出
    POI完美解析Excel数据到对象集合中(可用于将EXCEL数据导入到数据库)
    Java后台发送邮件
    (转)指针函数与函数指针的区别
    ROS下创建第一个节点工程
  • 原文地址:https://www.cnblogs.com/taotaozhuanyong/p/13793538.html
Copyright © 2011-2022 走看看