从0到1详解Jwt以及在Asp.NetCore 3.1中使用JWT Bearer授权认证

编辑于 2020/03/15
修改于 2020/04/02

环境 Asp. Net Core WebAPI 3.1

JWT介绍


jwt全称JSON Web Token,是目前最流行的跨域授权认证解决方案。
jwt是一个记录着用户身份信息的令牌,访问api时持有这个令牌就可以访问api,这就是jwt。
JWT官网

  • 授权:当用户登陆成功时,返回jwt的Token,后续的每次请求都将携带Jwt发放的token,来实现认证并返回所请求的数据。
    </br>
  • 认证:因JWT签名使用公钥私钥对,只要私钥不丢失,就可以确认发放token的人,而且使用了计算签名,可以有效的判断验证内容是否被修改。

JWT Token结构


HEADER.PAYLOAD.SIGNATURE
  • HEADER
  • PAYLOAD 载荷
  • SIGNATURE 签名
  1. HEADER 头
    HEADER包含token的元数据,使用的加密算法,和签名的类型,如下所示,加密类型是JWT,算法是HMAC SHA-256

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

    再通过base64算法编码,形成JWT头
    </br>

  2. PAYLOAD 载荷
    PAYLOAD也是一个Json,用来存放需要传递的数据,JWT官方解释了7个字段可以使用。

    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号

    同时也可以自定义添加字段,如

    {
        "Account": "Tuser",
        "Name": "我叫Tuser",
        "Age": 18
    }
    

    需要注意的是,PAYLOAD包含的字段是通过Base64编码的,所以任何获取到token的人都可以读取到里面的内容
    </br>

  3. SIGNATURE 签名
    SIGNATURE 部分是对前两部分的签名,防止数据篡改。
    首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)
    

    下面显示了一个JWT,它具有先前的头和​​有效负载编码,并使用机密签名。
    JWT官网提供了解码器,大家可以自行尝试

    JWT内容

JWT运行详解


一般在身份验证中用户请求登陆成功后,后端将返回JWT Token给前端,其中包含了用户的部分可见信息。
当前端请求受到请求限制的资源时,前端在请求头中带着JWT Token进行请求,如Authorization: Bearer <token>
</br>
在请求受保护的路由将检查Authorization标头中的有效JWT,如果JWT包含必要的数据,则可减能可以少查询数据库以进行某些操作的需要。
</br>
使用Jwt身份验证,那么跨域资源共享将不会成为问题,因为它不使用cookie。
</br>

Asp .Net WebAPI中使用Jwt


一. 创建. Net Core WebAPI 3.1项目并安装NuGet包+前置工作

  1. 创建项目
    这里就不演示了
    </br>

  2. 安装的NuGet包

    Jwt 包

    也可以通过进行双击项目在包还原下添加,不明白的可以博主最后面提供的源码
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.2" />
    </br>

  3. 创建Utils工具类库

    • 创建. net core 类库并命名为Utils
    • 创建Helper文件夹并添加Appsettings.cs文件
    • 添加Appsettings代码
      Appsettings.cs
        using Microsoft.Extensions.Configuration;
        using Microsoft.Extensions. Configuration.Json;
    
        namespace NetCore.Blogs.Demo.Utils
        {
            /// <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();
    
    
                }
    
                /// <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 "";
                }
            }
        }
    
    • 创建完后Utils类库结构如下


      Utils结构
    • 最后在Startup.cs中注入Appsettings类


      注入cs中注入Appsettings类

      完成后,就可以通过Appsettings.app(string[]) 传入一个数组进行访问appsettings.json配置文件了

二. 创建Jwt授权认证

重头戏来了!

  1. appsettings.json中添加Jwt字段

    • Issuer(发行人)
    • Audience(受众)
    • SecurityKey(秘钥)
    "JwtSettings": {
        "Issuer": "zyknow",
        "Audience": "audience",
        "SecurityKey": "zyknowzyknowzyknowzyknowzyknowzyknow"
    }
    

    </br>

  2. 在webApi项目中创建Extensions文件夹并创建AuthorizationSetup.cs类,这样把在Startup.cs做的操作提出来,这样Startup.cs看起来更简洁些,如图所示,并且粘贴代码

    Extensions结构

    AuthorizationSetup.cs

    public static class AuthorizationSetup
    {
        public static void AddAuthorizationSetup(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
    
            // 配置策略授权
            services.AddAuthorization(o =>
            {
                // 添加策略,使用时在方法上标注[Authorize(Policy ="AdminPolicy")],就会验证请求token中的ClaimTypes.Role是否包含了Admin
                o.AddPolicy("AdminPolicy", o =>
                {
                    //ClaimTypes.Role == Admin
                    o.RequireRole("Admin").Build();
    
                    //ClaimTypes.Role == Admin 或者 == User
                    //o.RequireRole("Admin","User").Build(); 
    
                    //ClaimTypes.Role == Admin 并且 == User ,关于添加多个角色策略,在Login控制器中
                    //o.RequireRole("Admin").RequireRole("User").Build(); 
                });
    
                //只有User的策略
                o.AddPolicy("onlyUserPolicy", o =>
                {
                    o.RequireRole("User").Build();
                });
    
                //User和Admin都可以访问的策略
                o.AddPolicy("UserOrAdminPolicy", o =>
                {
                    o.RequireRole("User", "Admin").Build();
                });
    
                //User并且是Admin才能请求的策略
                o.AddPolicy("UserAndAdminPolicy", o =>
                {
                    o.RequireRole("User").RequireRole("Admin").Build();
                });
            });
    
            string key = Appsettings.app(new string[] { "JwtSettings", "SecurityKey" });
            string issuer = Appsettings.app(new string[] { "JwtSettings", "Issuer" });
            string audience = Appsettings.app(new string[] { "JwtSettings", "Audience" });
    
            SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key)); //秘钥的长度有要求,必须>=16位
    
            services.AddAuthentication("Bearer").AddJwtBearer(o =>
            {
                o.TokenValidationParameters = new TokenValidationParameters()
                {
                    //是否秘钥认证
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = securityKey, //秘钥
    
                    //是否验证发行人
                    ValidateIssuer = true,
                    ValidIssuer = issuer, //这个字符串可以随便写,就是发行人
    
                    //是否验证订阅
                    ValidateAudience = true,
                    ValidAudience = audience,
    
                    //是否验证过期时间
                    RequireExpirationTime = true,
                    ValidateLifetime = true,
                };
            });
        }
    }
    
    

    </br>

  3. 注册Jwt到Startup.cs中


    ConfigureServices
    Configure

    </br>

  4. 创建Login控制器并创建GetToken方法


    创建Login控制器

    这个控制器是用来发放Jwt Token的,通过把请求中携带的userRole添加到Jwt授权中并返回(实际应该是查询数据库,然后返回对应的权限等级之类的,比如admin,SuperAdmin,User等)

    注意一下代码中自定义的Jwt字段,后面会提到

    [Route("api/[controller]")]
    [ApiController]
    [AllowAnonymous]
    public class LoginController : ControllerBase
    {
        /// <summary>
        /// 颁发令牌接口
        /// </summary>
        /// <returns></returns>
        [HttpGet(nameof(GetJwtToken))]
        public string GetJwtToken(string userRole)
        {
            //获取在配置文件中获取Jwt属性设置
            string key = Appsettings.app(new string[] { "JwtSettings", "securityKey" });
            string issuer = Appsettings.app(new string[] { "JwtSettings", "Issuer" });
            string audience = Appsettings.app(new string[] { "JwtSettings", "Audience" });
    
            //创建授权的token类
            SecurityToken securityToken = new JwtSecurityToken(
                issuer: issuer, //签发人
                audience: audience, //受众
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key)), SecurityAlgorithms.HmacSha256), //秘钥
    
                //创建过期时间
                expires: DateTime.Now.AddHours(1), //过期时间 一小时之后过期
    
                //自定义JWT字段
                claims: new Claim[] {
                    new Claim(ClaimTypes.Role,userRole), //把模拟请求的角色权限添加到Role中
                    //下面的都是自定义字段,可以任意添加到Claim作为信息共享
                    new Claim("Name","我叫Tuser"),
                    new Claim("Age","18"),
                }
                );
    
            //返回请求token
            return JsonConvert.SerializeObject(new {token = new JwtSecurityTokenHandler().WriteToken(securityToken)});
        }
    }
    

    </br>

  5. 开启调试,看看是否能正确请求到Token
    这里给大家推荐一下国人开发的谷歌浏览器调试插件,功能跟Postman一样,非常便携还是中文apizza

    进入正题,使用Http请求工具进行请求Login/GetJwtToken方法


    请求GetJwtToken成功

    可以看到成功的获取到了Token

    我们可以复制token在Jwt官网中解析看看

    Jwt官网解析Token

    接下来输入我们在appsettings.json中的SecurityKey字段


    SecurityKey字段内容
    有效令牌

    效验成功了!有效令牌!
    通过在PAYLOAD定义的公共属性(若没有私钥伪造令牌),可以很好的实现跨域信息共享
    </br>

  6. 在控制器中使用Jwt授权认证
    在创建项目时自动生成的WeatherForecastController.cs控制器中添加如下代码

    [ApiController]
    [Route("[controller]")]
    [Authorize]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };
    
        private readonly ILogger<WeatherForecastController> _logger;
    
        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }
    
        [HttpGet]
        [Route(nameof(AllGet))]
        [AllowAnonymous] //没有token也能访问
        public IEnumerable<WeatherForecast> AllGet()
        {
            var rng = new Random();
    
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    
        [HttpGet]
        [Route(nameof(AllTokenGet))]
        [Authorize]   //只需要有效token
        public IEnumerable<WeatherForecast> AllTokenGet()
        {
            var rng = new Random();
    
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    
        [HttpGet]
        [Route(nameof(AdminPolicy))]
        [Authorize(Policy = "AdminPolicy")] //需要请求携带token的策略为Admin才能访问
        public IEnumerable<WeatherForecast> AdminPolicy()
        {
            var rng = new Random();
    
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
        [HttpGet]
        [Route(nameof(onlyUserPolicy))]
        [Authorize(Policy = "onlyUserPolicy")] //需要请求携带token的策略为Admin才能访问
        public IEnumerable<WeatherForecast> onlyUserPolicy()
        {
            var rng = new Random();
    
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    
        [HttpGet]
        [Route(nameof(AdminRequireMent))]
        [Authorize(Policy = "AdminRequireMent")] //自定义授权
        public IEnumerable<WeatherForecast> AdminRequireMent()
        {
            var rng = new Random();
    
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
    

    </br>

  7. 开启调试
    这里就不一个个方法给大家演示了,有兴趣的同学可以自己下载demo进行演示
    这里只给大家演示一下成功示例

    • 不携带Token请求,无法请求


      不携带token的请求
    • 在请求头中携带token时请注意格式如下!


      Bearer认证格式

      携带没有权限的token请求
      先请求login获取token,注意看,这里使用的User


      注意看,这里使用的User

      复制token,再请求AdminPolicy接口,可以发现状态码为403,请求成功,但是权限错误,所以请求不到数据
      请求成功,但是权限错误
    • 最后,使用Admin生成的token来请求


      请求成功

自定义授权处理

这部分主要是讲解Jwt授权认证的自定义授权处理环节

  1. 创建PolicyRequirement文件夹并创建AdminRequirement.cs类和MustRoleAdminHandler.cs

    </br>

    AdminRequirement.cs

    public class AdminRequirement : IAuthorizationRequirement
    {
        //这里定义的字段是在Login GetToken方法授权时确定的
        public string Name { get; set; }
    }  
    

    </br>

    MustRoleAdminHandler.cs

    //继承AuthorizationHandler<AdminRequirement>
    public class MustRoleAdminHandler : AuthorizationHandler<AdminRequirement>
    {
        //重写授权认证方法,每次jwt认证都会进入这个方法,可以开启断点调试查看
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement)
        {
            //这里获取的是请求时AdminRequirement类中在jwt包含的字段
            Console.WriteLine(requirement);
    
            context.Succeed(requirement);
            return Task.CompletedTask;
        }
    }
    

    </br>

  2. AuthorizationSetup.cs中添加如下代码

    添加代码位置

     // 基于自定义处理策略
    o.AddPolicy("AdminRequireMent", o => {
        o.Requirements.Add(new AdminRequirement() { Name = "zyknow" });//完全自定义
    });  
    
    //依赖注入自定义处理策略
    services.AddSingleton<IAuthorizationHandler, MustRoleAdminHandler>();
    
    

    </br>

  3. WeatherForecastController.cs中添加使用自定义授权的方法

    自定义授权方法

    [HttpGet(nameof(AdminRequireMent))]
    [Authorize(Policy = "AdminRequireMent")] //自定义授权
    public IEnumerable<WeatherForecast> AdminRequireMent()
    {
        var rng = new Random();
    
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
    

    </br>

  4. 接着启动程序,断点调试,带Jwt Token请求方法


    发送请求

    进入断点,可以获取到自定义授权策略的类,这里直接返回成功了,主要是只是为了演示,在这里可以做自己的逻辑处理


    进入断点

源码

下载Jwt分支即可

码云

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容

  • 能不能遵行神的旨意,是信徒能不能真正认识神的分界线。属灵的知识必须与属灵的实践相结合,化知识于行动,照着神的话语去...
    蓝色红宝石阅读 2,797评论 0 1
  • 现在市面上环氧树脂和水性聚氨酯两种地坪是很多人的选择,很多人会在这两种地平之间摇摆不定,不知道选择哪一种地坪,下面...
    老嘤捉小稽阅读 1,229评论 0 0
  • 假装会有很多人看到的样子写下一些关于考研的心得。没错,你猜得没错,我不是17的考生,我是18考研的菜鸟。 今天是早...
    谢大胃阅读 329评论 15 5
  • 智者的聪慧和长者的经验也许能够通过语录得以保存 许多人并不知道查理本人是个天才,而且他对奥林的投资哲学产生了深刻的...
    能量女神_2368阅读 287评论 0 0
  • 本来昨晚是想等妹妹睡着了写日记的,结果妈妈也睡着了,这不,一觉睡到现在,实不相瞒妈妈的心里也是有点小纠结,...
    王童杰妈妈阅读 317评论 1 1