JWT介绍

Json web token(JWT)是一种认证授权机制。 为了网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC 7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

什么情况下使用JWT比较适合?

授权:这是最常见的使用场景,解决单点登录问题。因为JWT使用起来轻便,开销小,服务端不用记录用户状态信息(无状态),所以使用比较广泛;

信息交换:JWT是在各个服务之间安全传输信息的好方法。因为JWT可以签名,例如,使用公钥/私钥对儿 - 可以确定请求方是合法的。此外,由于使用标头和有效负载计算签名,还可以验证内容是否未被篡改

JWT原理

互联网服务离不开用户认证。一般流程是下面这样:

1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT 的认证流程

image-20240629212402896

用户点击登录,后台接收用户请求并根据账号和密码从数据库查询用户信息。用户若存在,则使用 JWT 生成 token 并返回给前台。用户若不存在,则返回错误信息。

前端在请求其他资源时将 token 放到请求头中。

后台从请求头中获取 token 信息,如果 token 校验失败,则返回错误信息。如果校验成功,就将业务数据返回给前端。

JWT 的结构

令牌的结构组成:

    1. 标头(Header)
    1. 载荷(Payload)
    1. 签名(Signature)

令牌最终的样子是由这三部分组成的字符串:

1
Header.Payload.Signature

例如:

1
hjYGH1dajUU.dajhjksfiu2h27jjghg2.kjbhjkf982bhh2lk2

标头

标头是使用 Base64 编码将令牌类型签名算法经过加工后生成的一段字符串

image-20240629212511003

标头包含两部分:

  • 令牌的类型:JWT(一般是默认的)
  • 签名算法:例如 SHA256、HMAC等
1
2
3
4
json复制代码{
"alg": "HS256",
"typ": "JWT"
}

载荷

载荷主要存储一些自定义信息。它也是使用 Base64 编码加工后生成的一段字符串。

image-20240629212505824

签名

签名是通过一个秘钥和标头中提供的算法再将标头和载荷进行加工后生成的一段字符串。例如:

image-20240629212526153

栗子

假设我们有一个JWT,它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Header: 
{
"alg": "HS256",
"typ": "JWT"
}

Payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

Signature: 使用密钥“mysecret”生成的签名

生成JWT

  1. 创建Header

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

    然后进行Base64Url编码:

    1
    Header = base64UrlEncode({"alg":"HS256","typ":"JWT"})

    结果(假设编码后为):

    1
    Header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
  2. 创建Payload

    1
    2
    3
    4
    5
    {
    "sub": "1234567890",
    "name": "John Doe",
    "iat": 1516239022
    }

    然后进行Base64Url编码:

    1
    Payload = base64UrlEncode({"sub":"1234567890","name":"John Doe","iat":1516239022})

    结果(假设编码后为):

    1
    Payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
  3. 创建Signature

    使用HMAC SHA256算法和密钥“mysecret”对前两部分进行签名:

    1
    2
    3
    4
    Signature = HMACSHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    "mysecret"
    )

    签名结果(假设为):

    1
    Signature = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

最终的JWT为:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

验证JWT

现在假设服务器接收到这个JWT,它需要验证JWT是否有效:

  1. 分离JWT

    将JWT分为三部分:

    1
    2
    3
    Header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
    Payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
    Signature = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
  2. 重新计算Signature

    使用与生成签名相同的算法和密钥“mysecret”对前两部分进行签名:

    1
    2
    3
    4
    New Signature = HMACSHA256(
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + "." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
    "mysecret"
    )

    得到的New Signature:

    1
    New Signature = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
  3. 比较签名

    将重新计算的New Signature与收到的Signature进行比较:

    1
    2
    3
    4
    mathematicaCopy codeif New Signature == Signature:
    JWT有效,未被篡改
    else:
    JWT无效,可能被篡改

由于New Signature与原始Signature一致,说明JWT未被篡改,可以信任该JWT的内容。

使用的话就是引入依赖

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>

参考文献: