Json Web Token (JWT)

为了学习 OAuth(开放授权),我们先从怎么生成一个标准的 token 开始吧~

理论

jwt 的组成

一个标准的 token 是怎么样的,请看下面来自官方的一张图:

jwt
jwt官方介绍

它是一个很长的字符串,中间用点 . 分隔成三个部分。
注意,jwt 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

jwt 的三个部分依次如下:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

写成一行,就是下面的样子:

Header.Payload.Signature

jwt - Header

Header 部分是一个 json 对象,描述 jwt 的元数据,通常是下面的样子:

1
2
3
4
{
"typ": "JWT",
"alg": "HS256"
}

上面代码中:
typ 属性表示这个令牌(token)的类型(type),jwt 令牌统一写为JWT
alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 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):编号

    两个没有中文注释的不好直译,更详细的解释,请看官方文档:jwt 官方字段

除了官方字段,你还可以在这个部分定义私有字段,如下面例子:

1
2
3
4
5
{
"iss": "https://yourwebsite.com",
"sub": "1234567890",
"name": "XiaoBai"
}

注意,jwt 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 json 对象也要使用 Base64URL 算法转成字符串。

jwt - Signature

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

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

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
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

```

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

### Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。
这个算法跟 Base64 算法基本类似,但有一些小的不同。

jwt 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)
Base64 有三个字符 `+`、 `/` 和 `=` ,在 URL 里面有特殊含义,
所以要被替换掉:`=` 被省略、`+` 替换成 `-`,`/` 替换成 `_` 。
这就是 Base64URL 算法。



## .Net 实现

规划一下 .Net 的实现方式:


尝试一下自己用原始的方式生成一个 jwt

``` cs
public class RawJwtHelper
{
const string Secret = "your-256-bit-secret";
const string Header = "{\"typ\":\"JWT\",\"alg\":\"HS256\"}";

public override string CreateToken(string payload)
{
var _header = Base64UrlEncode(Header);
var _payload = Base64UrlEncode(payload);

var _signature = Sign(Header, payload, Secret);

return $"{_header}.{_payload}.{_signature}";
}

private string Sign(string header, string payload, string secret)
{
//HMACSHA256(
// base64UrlEncode(header) + "." +
// base64UrlEncode(payload),
// secret)
var hash = HS256(Base64UrlEncode(header) + "." +
Base64UrlEncode(payload),
secret);
// 如果传 _header, _payload 给Sign 方法
// 那么这里面的 Base64UrlEncode 就可以去掉达到优化效果
// 这里只是为了表明签名的生成方式

var signature = Base64UrlEncode(hash);

return signature;
}

private string Base64UrlEncode(byte[] bytes)
{
var s = Convert.ToBase64String(bytes);
s = s.Replace('+', '-').Replace('/', '_').Replace("=", "");
return s;
}

private string Base64UrlEncode(string s)
{
var bytes = Encoding.UTF8.GetBytes(s);
return Base64UrlEncode(bytes);
}

private byte[] HS256(string toSign, string secret)
{
var buffer = Encoding.UTF8.GetBytes((toSign));
var key = Encoding.UTF8.GetBytes(secret);

return HS256(buffer, key);
}

private byte[] HS256(byte[] buffer, byte[] key)
{
// Header 部分,alg 属性声明什么加密算法,那么这里就用什么加密算法。
using (var hash = new System.Security.Cryptography.HMACSHA256(key))
{
return hash.ComputeHash(buffer);
}
}
}

生成测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Program
{
static void Main(string[] args)
{
var payload = "{\"iss\": \"https://yourwebsite.com\",\"sub\": \"1234567890\",\"name\": \"XiaoBai\"}";

JwtHelper jwtHelper = new RawJwtHelper();
var token = jwtHelper.CreateToken(payload);

Console.WriteLine(token);
Console.ReadLine();
}
}

如何验证我们生成的 token 是否符合标准呢?
请将生成的 token 复制到 https://jwt.io/#debuggerEncoded PASTE A TOKEN HERE 下的文本框,
然后留意文本框底下是 Signature Verified 还是 Invalid Signature

注意: 如果你修改了上面 const string Secret = "your-256-bit-secret" 的值,
那么需要将 Secret 的值复制到页面上 your-256-bit-secret 文本框处。

现在,我们已经知道 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

using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.JsonWebTokens;

public class MsJwtHelper
{
private const string Secret = "your-256-bit-secret";

private readonly JsonWebTokenHandler jwtHandler;
SigningCredentials signingCredentials;

public MsJwtHelper()
{
jwtHandler = new JsonWebTokenHandler();
signingCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Secret)),
SecurityAlgorithms.HmacSha256);

}

public override string CreateToken(string payload)
{
var token = jwtHandler.CreateToken(payload, signingCredentials);

return token;
}

public override JsonWebToken ReadJsonWebToken(string token)
{
var jsonWebToken = jwtHandler.ReadJsonWebToken(token);
return jsonWebToken;
}
}

// 测试
class Program
{
static void Main(string[] args)
{
var payload = "{\"iss\": \"https://yourwebsite.com\",\"sub\": \"1234567890\",\"name\": \"XiaoBai\"}";

JwtHelper jwtHelper = new MsJwtHelper();
var token = jwtHelper.CreateToken(payload);

Console.WriteLine(token);
// 依旧复制到 https://jwt.io/#debugger 进行验证,
// 会发现会自动补上 iat(签发时间) 和 exp(过期时间) 等属性

var jwt = jwtHelper.ReadJsonWebToken(token);// 使用的 UTC 时间

Console.ReadLine();
}
}

Go 实现

留坑,待填~ ^_^

jwt 的使用方式

客户端收到服务器返回的 jwt 后,
可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 jwt。
你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

1
Authorization: Bearer <token>

另一种做法是,跨域的时候,jwt 就放在 POST 请求的数据体里面。

觉得文章对您有帮助,请我喝瓶肥宅快乐水可好 (๑•̀ㅂ•́)و✧
  • 本文作者: 阿彬~
  • 本文链接: https://iweixubin.github.io/posts/json-web-token/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 免责声明:本媒体部分图片,版权归原作者所有。因条件限制,无法找到来源和作者未进行标注。
         如果侵犯到您的权益,请与我联系删除