查看原文
其他

ASP.NET Core 2.1 中使用jwt从原理到精通

DotNet 2019-08-02

(点击上方蓝字,可快速关注我们)


来源:roberg

cnblogs.com/lechengbo/p/9860711.html


原理


jwt对所有语言都是通用的,只要知道秘钥,另一一种语言有可以对jwt的有效性进行判断;


jwt的组成;Header部分Base64转化.Payload部分Base64转化.使用HS256方式根据秘钥对前面两部分进行加密后再Base64转化,其中使用的hs256加密是header部分指定的,也可以通过官网的查看,如下图:



原理就这么简单,那究竟用怎样使用C#来实现呢,又怎么确定它的正确性呢?,请继续


使用C#实现


我们定义一个今天方法,其中需要使用到Microsoft.IdentityModel.Tokens.dll,asp.net core 2.1再带,如果其他版本,没有自带,需要nuget 一下这个类库


/// <summary>

/// 创建jwttoken,源码自定义

/// </summary>

/// <param name="payLoad"></param>

/// <param name="header"></param>

/// <returns></returns>

public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null)

{

    if (header == null)

    {

        header = new Dictionary<string, object>(new List<KeyValuePair<string, object>>() {

            new KeyValuePair<string, object>("alg", "HS256"),

            new KeyValuePair<string, object>("typ", "JWT")

        });

    }

    //添加jwt可用时间(应该必须要的)

    var now = DateTime.UtcNow;

    payLoad["nbf"] = ToUnixEpochDate( now);//可用时间起始

    payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));//可用时间结束


    var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));

    var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad));


    var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));

    var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload))));


    var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature);

    return encodedJwt;

}

public static long ToUnixEpochDate(DateTime date) => 

(long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);


该方法很简单,只需要传入header键值对和payLoad键值对,然后根据原理进行Base64转换和hs256加密,接下来我们来使用一个测试类对其进行测试,代码如下:


[TestMethod]

public void TokenValidateTest()

{

    Dictionary<string, object> payLoad = new Dictionary<string, object>();

    payLoad.Add("sub", "rober");

    payLoad.Add("jti", "09e572c7-62d0-4198-9cce-0915d7493806");

    payLoad.Add("nbf", null);

    payLoad.Add("exp", null);

    payLoad.Add("iss", "roberIssuer");

    payLoad.Add("aud", "roberAudience");

    payLoad.Add("age", 30);


    var encodeJwt = TokenContext.CreateToken(payLoad, 30);


    var result = TokenContext.Validate(encodeJwt, (load) => { return true; });

    Assert.IsTrue(result);

}


先不管后面的验证,我们先看看其中生成的encodeJwt的值:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ.7Is2KYHAtSr5fW2gPU1jGeHPzz2ULCZJGcWb40LSYyw



第一部分和第二部分,并不是加密,只是Base64转换,我们可以通过其他语言轻松转换回来,如下使用javascript进行转,window.atob(base64加密) window.btoa(base64解密)


 var header=JSON.parse(window.atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))


如下图:



我再对payLoa进行转换回来, var payLoad=JSON.parse(window.atob('eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ'))  ,如下图:



所以,从这里可以看出来,Base64并不是属于加密,只是简单转换,因此,不能在payLoad中存放重要内容,比如密码等。


使用aspnetcore 中自带的类生成jwt


aspnet core中自带了一个jwt帮助类,其实原理一样,对上面做了封装,丰富了一个内容,我们继续使用一个静态方法,如下


/// <summary>

/// 创建jwtToken,采用微软内部方法,默认使用HS256加密,如果需要其他加密方式,请更改源码

/// 返回的结果和CreateToken一样

/// </summary>

/// <param name="payLoad"></param>

/// <param name="expiresMinute">有效分钟</param>

/// <returns></returns>

public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)

{

   

    var now = DateTime.UtcNow;


    // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.

    // You can add other claims here, if you want:

    var claims = new List<Claim>();

    foreach (var key in payLoad.Keys)

    {

        var tempClaim = new Claim(key, payLoad[key]?.ToString());

        claims.Add(tempClaim);

    }

   


    // Create the JWT and write it to a string

    var jwt = new JwtSecurityToken(

        issuer: null,

        audience: null,

        claims: claims,

        notBefore: now,

        expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),

        signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));

    var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

    return encodedJwt;

}


它效果和上面一模一样,如果使用同样的header 、payload、秘钥,生成的jwt肯定一样,这里就不演示了,感兴趣的可以自行尝试;


aspnetcore中如何使用自定义jwt验证


上面讲了那么多,只是为了大家更好的理解如何使用jwt进行验证,那是jwt是如何进行验证的呢?,如果一个http请求过来,一般jwt携带在http请求头部的Authorization中;先不看如何获取,先看看他是如何验证的,我们再定义个静态方法,如下:


/// <summary>

/// 验证身份 验证签名的有效性,

/// </summary>

/// <param name="encodeJwt"></param>

/// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值, </param>

/// 例如:payLoad["aud"]?.ToString() == "roberAuddience";

/// 例如:验证是否过期 等

/// <returns></returns>

public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)

{

    var success = true;

    var jwtArr = encodeJwt.Split('.');

    var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));

    var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));


    var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));

    //首先验证签名是否正确(必须的)

    success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));

    if (!success)

    {

        return success;//签名不正确直接返回

    }

    //其次验证是否在有效期内(也应该必须)

    var now = ToUnixEpochDate(DateTime.UtcNow);

    success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));

    //再其次 进行自定义的验证

    success = success && validatePayLoad(payLoad);

    return success;

}


其中 validatePayLoad 参数是一个自定义的验证的Fun,执行该Fun方法时会把解密后的payload作为参数传入进去


我们验证通过分为两部分,



我们来通过一个测试类验证


[TestMethod]

public void TokenCustomerValidateTest()

{

    Dictionary<string, object> payLoad = new Dictionary<string, object>();

    payLoad.Add("sub", "rober");

    payLoad.Add("jti", Guid.NewGuid().ToString());

    payLoad.Add("nbf", null);

    payLoad.Add("exp", null);

    payLoad.Add("iss", "roberIssuer");

    payLoad.Add("aud", "roberAudience");

    payLoad.Add("age", 30);


    var encodeJwt = TokenContext.CreateToken(payLoad, 30);


    var result = TokenContext.Validate(encodeJwt, (load) => {

        var success = true;

        //验证是否包含aud 并等于 roberAudience

        success = success&& load["aud"]?.ToString() == "roberAudience";


        //验证age>20等

        int.TryParse(load["age"].ToString(), out int age);

        Assert.IsTrue(age > 30);

        //其他验证 jwt的标识 jti是否加入黑名单等

        return success;

    });

    Assert.IsTrue(result);

}


如上面,我们可以把jwt中的payload解析出来,然后进行各种复杂的想要的验证;


其实,aspnet core中的基于角色,用户、策略,自定义策略的验证就相当这里的自定义验证,一下章将详细说明和对比,这里暂时不讲解


看完上面,是不是觉得jwt很简单就,主要就两部


1、创建jwt;


2、验证jwt;


完整代码如下:


/// <summary>

/// Token上下文,负责token的创建和验证

/// </summary>

public class TokenContext

{

    /// <summary>

    /// 秘钥,可以从配置文件中获取

    /// </summary>

    public static string securityKey = "GQDstclechengroberbojPOXOYg5MbeJ1XT0uFiwDVvVBrk";


    /// <summary>

    /// 创建jwttoken,源码自定义

    /// </summary>

    /// <param name="payLoad"></param>

    /// <param name="header"></param>

    /// <returns></returns>

    public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null)

    {

        if (header == null)

        {

            header = new Dictionary<string, object>(new List<KeyValuePair<string, object>>() {

                new KeyValuePair<string, object>("alg", "HS256"),

                new KeyValuePair<string, object>("typ", "JWT")

            });

        }

        //添加jwt可用时间(应该必须要的)

        var now = DateTime.UtcNow;

        payLoad["nbf"] = ToUnixEpochDate( now);//可用时间起始

        payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));//可用时间结束


        var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));

        var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad));


        var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));

        var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload))));


        var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature);

        return encodedJwt;

    }


    /// <summary>

    /// 创建jwtToken,采用微软内部方法,默认使用HS256加密,如果需要其他加密方式,请更改源码

    /// 返回的结果和CreateToken一样

    /// </summary>

    /// <param name="payLoad"></param>

    /// <param name="expiresMinute">有效分钟</param>

    /// <returns></returns>

    public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)

    {

       

        var now = DateTime.UtcNow;


        // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.

        // You can add other claims here, if you want:

        var claims = new List<Claim>();

        foreach (var key in payLoad.Keys)

        {

            var tempClaim = new Claim(key, payLoad[key]?.ToString());

            claims.Add(tempClaim);

        }

       


        // Create the JWT and write it to a string

        var jwt = new JwtSecurityToken(

            issuer: null,

            audience: null,

            claims: claims,

            notBefore: now,

            expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),

            signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));

        var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

        return encodedJwt;

    }


    /// <summary>

    /// 验证身份 验证签名的有效性,

    /// </summary>

    /// <param name="encodeJwt"></param>

    /// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值, </param>

    /// 例如:payLoad["aud"]?.ToString() == "roberAuddience";

    /// 例如:验证是否过期 等

    /// <returns></returns>

    public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)

    {

        var success = true;

        var jwtArr = encodeJwt.Split('.');

        var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));

        var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));


        var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));

        //首先验证签名是否正确(必须的)

        success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));

        if (!success)

        {

            return success;//签名不正确直接返回

        }

        //其次验证是否在有效期内(也应该必须)

        var now = ToUnixEpochDate(DateTime.UtcNow);

        success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));


        //再其次 进行自定义的验证

        success = success && validatePayLoad(payLoad);


        return success;

    }

    /// <summary>

    /// 获取jwt中的payLoad

    /// </summary>

    /// <param name="encodeJwt"></param>

    /// <returns></returns>

    public static Dictionary<string ,object> GetPayLoad(string encodeJwt)

    {

        var jwtArr = encodeJwt.Split('.');

        var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));

        return payLoad;

    }

    public static long ToUnixEpochDate(DateTime date) => 

        (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);

}


以上就是jwt的基本内容,它确实很简单,不要被aspnet core中的各种写法给搞晕了,只要是jwt相关的验证都是基于上面这些东西


下一章节将讲述:


1、在aspnet core中,自定义jwt管道验证;


2、在aspnet core中,自定义策略验证CommonAuthorizeHandler : AuthorizationHandler<CommonAuthorize>


3、自定义jwt逻辑验证和原生的角色,用户,策略,等进行对比


完本文有收获?请转发分享给更多人

关注「DotNet」,提升.Net技能 

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存