秒懂 JWT!

1d8c633368c34174d3cc28f4276d641e.gif

c1332b32fb1e324de9183d40993821fe.png

作者 | 喵叔

责编 | 胡巍巍

出品 | 程序人生(ID:coder_life)

JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。

JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。

在讲解 JWT 之前我们先来看一个问题。我们都知道 Internet 服务的身份验正过程是这样的,客户端向服务器发送登录名和登录密码,服务器验证后将对应的相关信息保存到当前会话中,这些信息包括权限、角色等数据。

服务器向客户端返回 Session ,Session 信息都会写入到客户端的 Cookie 中,后面的请求都会从 Cookie 中读取 Session 发送给服务器,服务器在收到 Session 后会对比保存的数据来确认客户端身份。

但是上述模式存在一个问题,无法横向扩展。在服务器集群或者面向服务且跨域的结构中,需要数据库来保存 Session 会话,实现服务器之间的会话数据共享。

在单点登录中我们会遇到上述问题,当有多个网站提供同一拨服务,那么我们该怎么实现在甲网站登陆后其他网站也同时登录呢?

其中一种方法是持久化 Session 数据,也就是上面所说的将 Session 会话存到数据库中。这个方法的优点是架构清晰明了。

但是缺点也非常明显,就是架构修改很困难,验证逻辑需要重写,并且整体依赖于数据库,如果存储 Session 会话的数据库挂掉那么整个身份认证就无法使用,进而导致系统无法登录。要解决这个问题我们就用到了 JWT 。

5c7e5d4746fc25398b547bac6ef512fa.png

JWT 简述

客户端身份经过服务器验证通过后,会生成带有签名的 JSON 对象并将它返回给客户端。客户端在收到这个 JSON 对象后存储起来。

在以后的请求中客户端将 JSON 对象连同请求内容一起发送给服务器,服务器收到请求后通过 JSON 对象标识用户,如果验证不通过则不返回请求的数据。

验证不通过的情况有很多,比如签名不正确、无权限等。在 JWT 中服务器不保存任何会话数据,使得服务器更加容易扩展。

a1d578ad51faacf9cbd1eb9d40619f6f.png

Base64URL 算法

在讲解 JWT 的组成结构前我们先来讲解一下 Base64URL 算法。这个算法和 Base64 算法类似,但是有一点区别。

我们通过名字可以得知这个算法使用于 URL 的,因此它将 Base64 中的 +  、 / 、 = 三个字符替换成了 - 、 _ ,删除掉了 = 。因为这个三个字符在 URL 中有特殊含义。

1588530e9b5bbb92db4fc870c23b10b3.png

JWT 组成结构

JWT 是由三段字符串和两个 . 组成,每个字符串和字符串之间没有换行(类似于这样:xxxxxx.yyyyyy.zzzzzz),每个字符串代表了不同的功能,我们将这三个字符串的功能按顺序列出来并讲解:

1. JWT 头

JWT 头描述了 JWT 元数据,是一个 JSON 对象,它的格式如下:

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

这里的 alg 属性表示签名所使用的算法,JWT 签名默认的算法为 HMAC SHA256 , alg 属性值 HS256 就是 HMAC SHA256 算法。typ 属性表示令牌类型,这里就是 JWT。

2. 有效载荷

有效载荷是 JWT 的主体,同样也是个 JSON 对象。有效载荷包含三个部分:

  • 标准注册声明

标准注册声明不是强制使用是的,但是我建议使用。它一般包括以下内容:

iss:jwt的签发者/发行人;

sub:主题;

aud:接收方;

exp:jwt过期时间;

nbf:jwt生效时间;

iat:签发时间

jti:jwt唯一身份标识,可以避免重放攻击 

  • 公共声明:

可以在公共声明添加任何信息,我们一般会在里面添加用户信息和业务信息,但是不建议添加敏感信息,因为公共声明部分可以在客户端解密。

  • 私有声明:

私有声明是服务器和客户端共同定义的声明,同样这里不建议添加敏感信息。

下面这个代码段就是定义了一个有效载荷:

json
{
  "exp":"201909181230",
  "role":"admin",
  "isShow":false
}

3. 哈希签名

哈希签名的算法主要是确保数据不会被篡改。它主要是对前面所讲的两个部分进行签名,通过 JWT 头定义的算法生成哈希。哈希签名的过程如下:

1. 指定密码,密码保存在服务器中,不能向客户端公开;

2. 使用 JWT 头指定的算法进行签名,进行签名前需要对 JWT 头和有效载荷进行 Base64URL 编码,JWT 头和邮箱载荷编码后的结果之间需要用 . 来连接。

简单示例如下:

HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密码)

最终结果如下:

base64UrlEncode(JWT 头)+"."+base64UrlEncode(有效载荷)+"."+HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密码)

8d8daba432984669db91b58d3d8429f7.png

JWT 注意事项

在使用 JWT 时需要注意以下事项:

1. JWT 默认不加密,如果要写入敏感信息必须加密,可以用生成的原始令牌再次对内容进行加密;

2. JWT 无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改;

3. JWT 包含认证信息,如果泄露了,任何人都可以获得令牌所有的权限;因此 JWT 有效期不能太长,对于重要操作每次请求都必须进行身份验证。

b6b381f2317731b2df4d0ad8b8577803.png

JWT 例子

1. 自定义 JWT

  • 定义 JWT 头

csharp
string jwtHeader = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  • 定义有效载荷

csharp
string exp = GetTimeStamp(DateTime.Now.AddHours(1));
string jwtHeader = "{\"name\":\"zhangsan\",\"exp\":\"" + exp + "\",\"jti\":\"123123\"}";
  • 加密 JWT 头和有效载荷

  • 生成哈希签名

string signature = HMACSHA256(jwtHeaderBase64Url + "." + jwtPlayloadBase64Url,"123123");
  • 按顺序链接三部分,最终形成 JWT:

string jwtStr = jwtHeaderBase64Url + "." + jwtPlayloadBase64Url + "." + signature;

完整代码如下:

csharp
static void Main(string[] args)
{
    string jwtHeader = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
    string exp = GetTimeStamp(DateTime.Now.AddHours(1));
    string jwtPlayload = "{\"name\":\"zhangsan\",\"exp\":\"" + exp + "\",\"jti\":\"123123\"}";
    string jwtHeaderBase64Url = Base64Url(jwtHeader);
    string jwtPlayloadBase64Url = Base64Url(jwtPlayload);
    string signature = HMACSHA256(jwtHeaderBase64Url + "." + jwtPlayloadBase64Url,"123123");
    string jwtStr = jwtHeaderBase64Url + "." + jwtPlayloadBase64Url + "." + signature;
    Console.WriteLine(jwtStr);
    Console.ReadLine();
}

private static string HMACSHA256(string message, string key)
{
    var encoding = new System.Text.UTF8Encoding();
    byte[] keyByte = encoding.GetBytes(key);
    byte[] messageBytes = encoding.GetBytes(message);
    using (var hmacSHA256 = new HMACSHA256(keyByte))
    {
        byte[] hashMessage = hmacSHA256.ComputeHash(messageBytes);
        return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
    }
}
private static string Base64Url(string str)
{
    byte[] encodedBytes = Encoding.UTF8.GetBytes(str);
    string base64EncodedText = Convert.ToBase64String(encodedBytes);
    base64EncodedText = base64EncodedText
        .Replace("=", String.Empty)
        .Replace('+', '-')
        .Replace('/', '_');
    return base64EncodedText;
}

private static string GetTimeStamp(DateTime dt)
{
    DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1, 0, 0, 0, 0));
    DateTime nowTime = dt;
    long unixTime =
        (long)System.Math.Round((nowTime - startTime).TotalMilliseconds, MidpointRounding.AwayFromZero);
    return unixTime.ToString();
}

运行结果如下:

![nosk7V.png](https://s2.ax1x.com/2019/09/18/nosk7V.png)

2. 使用 .NET JWT 包

上面的代码我们在造轮子,但是 NuGet 中已经有造好的轮子了。在 NuGet 中搜索 JWT 并安装。使用 JWT 包我们只需要自定义有效载荷和密码即可,可生成三段格式的字符串

JWT 生成代码如下:

static void Main(string[] args)
{
    string exp = GetTimeStamp(DateTime.Now.AddHours(1));
    var payload = new Dictionary<string, object>
    {
        {"name", "zhangsan"},
        {"exp", exp},
        {"jti", "123123"}
    };
    IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
    IJsonSerializer serializer = new JsonNetSerializer();
    IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
    IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
    string jwtStr= encoder.Encode(payload, "123123");
    Console.WriteLine(jwtStr);
    Console.ReadLine();
}

同样,我们可以利用 JWT 包对生成的 JWT 进行解密,代码如下:

IDateTimeProvider provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);

var json = decoder.Decode(jwtStr, "123123", verify: true);

上述代码在项目中可以直接拿来用,只需修改其中的有效载荷和密码。

f3c87a176228925a3f90b735b202a8b0.png

总结

本篇首先讲解了 JWT 的相关知识,然后通过自定义的方式和调用 JWT 包的方式手动创建了 JWT 。文章所讲的内容大家一定要切记,这些知识在开发中至关重要,在最后再补充两个小知识:

1. 客户端发送 JWT 发送给服务器时,最好把 JWT 放在HTTP请求的Header Authorization,格式是:Authorization: Bearer jwt。

2. JWT 不仅仅可以实现身份认证还可以在跨域 post 请求时将请求参数加入到有效载荷中,实现 post 跨域请求。

作者简介:朱钢,笔名喵叔,CSDN博客专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于北京恒创融慧科技发展有限公司,从事企业级安全监控系统的开发。CSDN博客地址:https://programercat.blog.csdn.net/

e81adcfa9dc7d313c69eadcaa584cc33.png

8a5d12d5096df3c0f77b0c91022971b2.png

 热 文 推 荐 

横扫阿里、滴滴、美团后,阿里程序媛整理出这份厚厚的面经!

300 秒就完成第一超算 1 万年的计算量,量子霸权真时代要来了吗

“不给钱就删库”的勒索病毒, 程序员该如何防护?

谷歌称已实现量子霸权;iOS 捷径功能被诉侵权;Chrome 78 Beta 发布 | 极客头条

看完这篇还不会kafka,我跪榴莲!

使用Python对大脑成像数据进行可视化分析

旷视张祥雨:高效轻量级深度模型的研究和实践 |  AI ProCon 2019

一文读懂分片基础原理, 数据分片, 跨分片交易, 区块链分片和缩放究竟是什么鬼?

☞厉害!接班马云的为何是张勇?

57071b5251fec1b07fc7f99bc264e881.gif点击阅读原文,即刻阅读《程序员大本营》最新期刊。

df2f6d98b5d718ecc5a7a2a83f57722b.png

你点的每个“在看”,我都认真当成了喜欢

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值