好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

asp.net core 2.1中如何使用jwt(从原理到精通)

为什么使用 Jwt

最近,移动开发的劲头越来越足,学校搞的各种比赛都需要用手机 APP 来撑场面,所以,作为写后端的,很有必要改进一下以往的基于 Session 的身份认证方式了,理由如下:

移动端经常要保持长时间(1 到 2 星期)在线,但是 Session 却不好在服务端保存这么久,虽然可以持久化到数据库,但是还是挺费资源 移动端往往不是使用的网页技术,所以藏在 Cookie 里面的 SessionId 不是很方便的传递给服务端 服务端暴露给客户端的接口往往是 RESTful 风格的,这是一种无状态的 API 风格,所以身份认证的方式最好也是无状态的才好

所以我选择了使用 Jwt (Json Web Token) 这个技术。Jwt 是一种无状态的分布式的身份验证方式,与 Session 相反,Jwt 将用户信息存放在 Token 的 payload 字段保存在客户端,通过 RSA 加密的方式,保证数据不会被篡改,验证数据有效性。

下面是一个使用 Jwt 的系统的身份验证流程:

可以看出,用户的信息保存在 Token 中,而 Token 分布在用户的设备中,所以服务端不再需要在内存中保存用户信息了
身份认证的 Token 传递时以一种相当简单的格式保存在 header 中,方便客户端对其进行操作

原理

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

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

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

使用C#实现

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

/// <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加密,接下来我们来使用一个测试类对其进行测试,代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

[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解密)

?

1

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

如下图:

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

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

使用aspnetcore 中自带的类生成jwt

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

/// <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中;先不看如何获取,先看看他是如何验证的,我们再定义个静态方法,如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

/// <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作为参数传入进去

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

第一,必须的(自认为的) 

jwt签名是否正确,请看以上代码实现  jwt是否在可以时间内,请看以上代码实现

第二,自定义的(各复杂的,原理就是获取payLoad 的某个值,然后对这个值进行各种判读--等于,大于,包含,)  

该jwt是不是进入黑名单 aud==‘roberAudience'

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

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

[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很简单就,主要就两部

创建jwt; 验证jwt;

完整代码如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

/// <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相关的验证都是基于上面这些东西

下一章节将讲述:

在aspnet core中,自定义jwt管道验证; 在aspnet core中,自定义策略验证CommonAuthorizeHandler : AuthorizationHandler<CommonAuthorize> 自定义jwt逻辑验证和原生的角色,用户,策略,等进行对比

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:http://www.cnblogs.com/lechengbo/p/9860711.html

查看更多关于asp.net core 2.1中如何使用jwt(从原理到精通)的详细内容...

  阅读:70次