• dotnet core JWT Demo


    JWT介绍

    JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。JWT的官网地址:https://jwt.io/.

    通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。

    JWT中包含了身份认证必须的参数以及用户自定义的参数,JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

    组成结构

    在紧凑的形式中,JSON Web Tokens由dot(.)分隔的三个部分组成,它们是:

    • Header 头
    • Payload 有效载荷
    • Signature 签名

    因此,JWT通常长这个样子: xxxxx.yyyyy.zzzzz

    标头通常由两部分组成: 令牌的类型, 即JWT, 以及正在使用的签名算法,例如HMAC, SHA256或RSA。

    例如:

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

    然后,这个JSON被编码为Base64Url,形成JWT的第一部分。

    Payload

    Payload部分也是一个JSON对象, 用来存放实际需要传递的数据. JWT规定了7个官方字段:

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

    除了官方字段, 你还可以在这个部分定义私有字段, 但是它默认是不加密的, 任何人都可以读到, 所以不要把秘密信息放在这个部分. 这个JSON对象也要使用Base64URL算法转成字符串.

    Signature

    Signature部分是对前两部分的签名, 防止数据篡改.

    首先需要指定一个密钥(secret), 这个密钥只有服务器才知道, 不能泄露给用户. 然后使用Header里面指定的签名算法(默认是HMAC SHA256), 按照下面的公式产生签名:

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

    使用方法

    项目源码请看我的Gitee.

    项目基础

    该项目需要需要使用以下两个nugget包:

    • System.IdentityModel.Tokens.Jwt
    • Microsoft.AspNetCore.Authentication.JwtBearer
      从概念上主要分为两个部分:
    • Authentication: 公司给你发的门禁卡.
    • Authorization: 财务保险柜的钥匙.
      一开始一直对这两个概念模棱两可, 每次都是看了又丢了, 其实不难, 不深究了.

    项目结构

    下面以项目结构对代码进行一个粗略的解说:

    • 注入服务
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddAuthentication(option =>
                {
                    //添加JWT Scheme
                    option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    option.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                    option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                }).AddJwtBearer(option =>
                {
                    //添加JWT验证
                    option.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateLifetime = true,//是否验证失效时间
                        ClockSkew = TimeSpan.FromSeconds(30),
                        ValidateAudience = true,//是否验证Audience
                        //ValidAudience = Const.GetValidudience(),//Audience
                        //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了
                        AudienceValidator = (m, n, z) =>
                        {
                            return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
                        },
                        ValidateIssuer = true,//是否验证Issuer
                        ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致
                        ValidateIssuerSigningKey = true,//是否验证SecurityKey
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
                    };
                    option.Events = new JwtBearerEvents()
                    {
                        OnAuthenticationFailed = context =>
                        {
                            //Token expired
                            if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                            {
                                context.Response.Headers.Add("Token-Expired", "true");
                                context.Response.Redirect("/");
                            }
                            return Task.CompletedTask;
                        }
                    };
                });
    
                //添加策略健全模式
                services.AddAuthorization(option =>
                {
                    option.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));
                });
                //注入授权Handler
                services.AddSingleton<IAuthorizationHandler, PolicyHandler>();
    
                //注入HttpContext的祖先
                services.AddHttpContextAccessor();
    
                services.AddControllersWithViews();
            }
    
    • 启用服务
    app.UseAuthentication();
    
    • 权限要求
        public class PolicyRequirement : IAuthorizationRequirement
        {
            /// <summary>
            /// 用户权限集合
            /// </summary>
            public List<UserPermission> UserPermissions { get; private set; }
            /// <summary>
            /// 无权限action
            /// </summary>
            public string DeniedAction { get; set; }
            /// <summary>
            /// 构造
            /// </summary>
            public PolicyRequirement()
            {
                //没有权限则跳转到这个路由
                DeniedAction = new PathString("/api/auth/nopermission");
                //用户有权限访问的路由配置,当然可以从数据库获取
                UserPermissions = new List<UserPermission> {
                                  new UserPermission {  Url="/api/values/authorization", UserName="admin"},
                              };
            }
        }
    
        /// <summary>
        /// 用户权限承载实体
        /// </summary>
        public class UserPermission
        {
            /// <summary>
            /// 用户名
            /// </summary>
            public string UserName { get; set; }
            /// <summary>
            /// 请求Url
            /// </summary>
            public string Url { get; set; }
        }
    
    • 权限处理
        public class PolicyHandler : AuthorizationHandler<PolicyRequirement>
        {
            private readonly IHttpContextAccessor _httpContextAccessor;
    
            public PolicyHandler(IHttpContextAccessor httpContextAccessor)
            {
                _httpContextAccessor = httpContextAccessor;
            }
    
            protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
            {
                //赋值用户权限
                var userPermissions = requirement.UserPermissions;
                //从AuthorizationHandlerContext转成HttpContext,以便取出表求信息
                var httpContext = _httpContextAccessor.HttpContext;
                //请求Url
                var questUrl = httpContext.Request.Path.Value.ToUpperInvariant();
                //是否经过验证
                var isAuthenticated = httpContext.User.Identity.IsAuthenticated;
                if (isAuthenticated)
                {
                    if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl))
                    {
                        //用户名
                        var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value;
                        if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl))
                        {
                            context.Succeed(requirement);
                        }
                        else
                        {
                            //无权限跳转到拒绝页面
                            httpContext.Response.Redirect(requirement.DeniedAction);
                        }
                    }
                    else
                    {
                        context.Succeed(requirement);
                    }
                }
                else
                {
                    httpContext.Response.Redirect(requirement.DeniedAction);
                }
                return Task.CompletedTask;
            }
        }
    
    • 授权
            [HttpGet]
            public IActionResult Get(string userName, string pwd)
            {
                if (CheckAccount(userName, pwd, out string role))
                {
                    //每次登陆动态刷新
                    Const.ValidAudience = userName + pwd + DateTime.Now.ToString();
                    // push the user’s name into a claim, so we can identify the user later on.
                    //这里可以随意加入自定义的参数,key可以自己随便起
                    var claims = new[]
                    {
                        new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                        new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"),
                        new Claim(ClaimTypes.NameIdentifier, userName),
                        new Claim("Role", role)
                    };
                    //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.
                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                    //.NET Core’s JwtSecurityToken class takes on the heavy lifting and actually creates the token.
                    var token = new JwtSecurityToken(
                        //颁发者
                        issuer: Const.Domain,
                        //接收者
                        audience: Const.ValidAudience,
                        //过期时间
                        expires: DateTime.Now.AddMinutes(30),
                        //签名证书
                        signingCredentials: creds,
                        //自定义参数
                        claims: claims
                        );
    
                    return Ok(new
                    {
                        token = new JwtSecurityTokenHandler().WriteToken(token)
                    });
                }
                else
                {
                    return BadRequest(new { message = "username or password is incorrect." });
                }
            }
    
    • 访问授权
    // Authentication验证门禁卡
    [Authorize]
    // Authorization验证保险柜钥匙
    [Authorize("Permission")]
    

    项目源码请看我的Gitee.

    调试方法

    Postman

    这里用Postman调试的时候出现了一点小插曲, 因为.Net Core3.0会自己生成https证书, 不知道是Postman不认他还是为什么, 请求一直发不出去, 这里需要设置关闭ssl验证.

    Chrome

    这里可以直接在Chrome控制台里面写请求:

    fetch('https://localhost:5001/api/values/authorization',{
      headers: {
        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTcyOTQxNDM1IiwiZXhwIjoxNTcyOTQzMjM1LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6ImFkbWluIiwiUm9sZSI6ImFkbWluIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6ImFkbWluMTEvMDUvMjAxOSAxNjoxMDozNSJ9.-pQK03wUYH97SDRxaN51CkcpXcs9B6qNwnZ4dfRgv3s'
      }
    })
    .then(res => res.json())
    .then(console.log)
    
  • 相关阅读:
    CCF CSP-J/S2020第二轮认证评级名单
    CSP-J/S2020第二轮提高组北京市获奖分析
    CCF CSP-J/S2020第二轮评级规则
    [ZZ]80% 的信息学竞赛生都不知道的 USACO!含金量极高
    初步估分体验
    【ZZ】近3年北京市科技特长生招生趋势分析,编程好的孩子有哪些优势?
    立冬--2020CSP-J 游记
    P1061 Jam的计数法
    P1476 休息中的小呆
    P1444 [USACO1.3]虫洞wormhole
  • 原文地址:https://www.cnblogs.com/jerryqi/p/11799897.html
Copyright © 2020-2023  润新知