目录

【项目必会】用户鉴权是什么?jwt是什么? | 青训营笔记


这是我参与「第三届青训营 -后端场」笔记创作活动的的第四篇笔记。

由于最近14周忙的焦头烂额,所以一直没有时间写笔记,而且也很久没有认真看青训营的课了(期末考试,所有专业课都放在了14周考试,考完这个周,后面就没有重要的考试了(6级英语和蓝桥杯国赛除外😭

我为什么突然将JWT鉴权,因为抖音项目里面的前后端身份识别就是通过传递JWT token进行身份验证的传递。

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。

用户认证

在服务端和客户端之间通信的过程中,服务端需要有效的识别出客户端的身份,才能进行对应的通信,毕竟大部分时候客户端和服务器并不是长连接,一般都是socket都是连接后收发一个包就断开了,所以这种情景下,每次请求肯定是需要重新经过认证过程的。

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

  1. 用户向服务器发送用户名和密码。

  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

  3. 服务器向用户返回一个 session_id,写入用户的 Cookie。

  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

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

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

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

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

JWT到底是什么

JWT原理

为什么叫做JWT,从缩写中又json,就不难猜出,它的数据格式肯定和json格式有关联。

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{
  "姓名": "张三",
  "角色": "管理员",
  "到期时间": "2022年5月27日0点0分"
}

之后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT数据结构

虽然如上文所说,发送的JWT是一个json对象是没错,但实际发送的并不是直接的明文,而应该是由 . 分割的三个字符串。如下图:

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0069621cefa4a99a91fbf1e36a28c63~tplv-k3u1fbpfcp-watermark.image?

以上的三个部分的字符串,分别被称为以下三个组成部分:

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

Header.Paload.Signature

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fad7b9579766402e89518455e621c94d~tplv-k3u1fbpfcp-watermark.image?

下面依次介绍这三个部分。

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

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

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

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

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意:JWT 默认是不加密的(即便后面的签名也只是为了防止数据篡改),任何人都可以读到,所以不要把秘密信息放在这个部分。

Signature

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

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

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

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

JWT实战

根据前面的描述,我们对JWT的数据结构有了一定的了解。

现在转换到我们的Go语言中具体的使用它!

首先go get一下我们处理JWT的包。

go get github.com/dgrijalva/jwt-go

签发JWT的token

根据前面的讲解,我们已经了解到JWT的数据结构,它就是一个json文件,包含三个部分。

那么我们现在需要做的就是填写这三部分,最后通过base64UrlEncode编码得到字符串即为最终的token。

Header部分

这个部分包含两个字段,这两个字段都不用我们填,会由你选择的签名算法自动填写好。

Payload部分

我们查看 jwt.StandardClaims 类型的字段,可以发现它包含如下的类型:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05dbacf42df94899bc08ff61baf37622~tplv-k3u1fbpfcp-watermark.image?

这正好就是Payload部分默认填写的几个字段,我们自定义一个结构体,将该结构体类型嵌入。如下,我定义了Claims类型:

type Claims struct {
	UserId int64
	jwt.StandardClaims
}

然后我们把这些字段填充完整,来用它得到token。

claims := &Claims{
    UserId: user.UserInfoId,
    StandardClaims: jwt.StandardClaims{
        ExpiresAt: expirationTime.Unix(),
        IssuedAt:  time.Now().Unix(),
        Issuer:    "douyin_pro_131",
        Subject:   "L_B__",
    }}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

jwt.NewWithClaims() 将返回一个 Token 对象。

看看源码,我们发现它仅仅构造了Token对象并返回,里面包含了你已经确认的签名方法回调。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cfe030fb35e34666894d22b54efe0731~tplv-k3u1fbpfcp-watermark.image?

Signature部分

最后通过得到的 Token 对象调用它的 SignedString 方法并传入产生签名需要的密钥(自定义byte[]类型的数据),将会自动完成token的整个编码过程:包括对 Signature 字段的填写,以及将上述三个部分json数据通过base64url进行最后的编码返回最终的token字符串。

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5dee5a93382a4779aa8e7fe2466d25a1~tplv-k3u1fbpfcp-watermark.image?

完整源码

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf8e1c133b4c413b8f1b213d95f13445~tplv-k3u1fbpfcp-watermark.image?

解析JWT的token

解析过程就非常的简单了,这相当于一个反序列化的过程,和json的解析过程类似,jwt提供的函数需要传入对象的指针,然后在内部帮你完整反序列化操作绑定到这个对象,唯一的不一样就是我们还需要传入一个回调,这个回调返回密钥以便最终的签名判断防止数据篡改。

代码如下:

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/530c654a903343d782a7c2abda9476b6~tplv-k3u1fbpfcp-watermark.image?

注意:JWT解析过程中,它是会判断token是否过期了,如果过期,那么就会返回err。

最后我们服务端得到claims后,便可得到其中包含的用户信息了,然后对该用户执行对应的操作,比如得到该用户喜欢的视频列表。

以上所有代码均出自我写的抖音项目里的 jwt.go 目录中。

源码链接

JWT的几个特点总结

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

参考链接:

https://jwt.io/introduction/

learn how to use jwt?