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)
posted @ 2019-11-05 17:23  齐建伟  阅读(681)  评论(0编辑  收藏  举报