JWT鉴权登陆

发布时间 2023-12-23 23:27:31作者: 李若盛开

1、传统的session认证 

http协议本身是一种无状态的协议,而这就意味着如果用户每一次请求时都要向我们的应用提供用户名和密码来进行用户认证。用户认证成功后,服务器开辟空间存储当前用户信息(session),而发给客户端的 sesssion_id 存放到 cookie 中,这样用客户端请求时带上 session_id 就可以验证服务器端是否存在session 数据,以此完成用户的合法校验。

但是这种基于session的认证存在很多弊端,例如:

1)资源开销: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
2)扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
3)安全性: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的CSRF攻击。

2、基于token的鉴权机制

 基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

 

1)用户使用用户名密码来请求服务器
2)服务器进行验证用户的信息
3)服务器通过验证发送给用户一个token
4)客户端存储token,并在每次请求时附送上这个token
5)服务端验证token,并返回数据

3、JWT概述

JWT全称Json web token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,是目前最流行的跨域身份验证解决方案。

JWT基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息,一旦用户完成了登陆,在接下来的每个请求中都会包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限的验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,这也为应用的扩展提供了便利。

JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。以下是JWT官网对JWT的一段介绍描述:

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JWT原理

JWT 的构成
JWT令牌由三个部分组成,由.分隔,分别是:

1)header:令牌头部,记录了整个令牌的类型和签名算法
2)payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
3)signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改
因此,JWT 通常具有如下所示的结构:xxxxx.yyyyy.zzzzz

JWT header

header通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法,例如 HMAC、SHA256 或 RSA。

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

设置好了header的结构之后,还需要对header的JSON对象进行Base64 Url编码,最后编码后生成的字符串才是最终的header部分。

JWT payload

令牌的第二部分是有效负载,其中包含有三种类型的声明:标准中注册的声明、公共声明和私人声明。声明是有关实体(通常是用户)和其他数据的语句。

标准注册声明 (建议但不强制使用) :

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明:

这些可以由使用 JWT 的人随意定义。但为了避免冲突,它们应该在 IANA JSON Web 令牌注册表中定义,或者定义为包含抗冲突命名空间的 URI。

私有的声明:

是提供者和消费者所共同定义的声明,用于在同意使用它们的各方之间共享信息。

以上所有的声明中一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

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

 playload部分和header一样,需要通过Base64编码。

JWT Signature
签名用于验证消息在此过程中未被更改,并且在使用私钥签名的令牌还可以验证 JWT 的发件人是否是它所说的人。

要创建签名部分,您必须获取编码的header、有效负载、秘钥、header中指定的算法并对其进行签名。例如,如果要使用 HMAC SHA256 算法,将按以下方式创建签名:

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

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串, 然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

完整的JWT 

最后的输出是三个由点分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,与基于 XML 的标准(如 SAML)相比更加紧凑。

下图是对header和payload进行了编码并使用秘钥进行签名的 JWT。

 可以使用调试器 jwt.io 解码、验证和生成 JWT。 

JWT的优点

1、无状态
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。

2、有效避免了 CSRF 攻击
CSRF 攻击需要依赖 Cookie。一般情况下我们使用 JWT 的话,在我们登录成功获得 JWT 之后,一般会选择存放在 localStorage 中。前端的每一个请求后续都会附带上这个 JWT,整个过程不会涉及到 Cookie,因此可以避免 CSRF 攻击。

3、适合移动端应用
使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie,当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的。但是,使用 JWT 进行身份认证就不会存在这种问题,因为只要 JWT 可以被客户端存储就能够使用,而且 JWT 还可以跨语言使用。

4、性能开销小
一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多。

JWT 身份认证常见问题
注销问题
传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保存在服务端。但是因为 jwt 是无状态的,服务端通过计算来校验有效性,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。jwt 的无状态使得其注销变得复杂。解决方案有如下几种:

1、将 JWT 存入内存数据库

将 JWT 存入 DB 中,Redis 内存数据库在这里是不错的选择。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会违背了 JWT 的无状态原则。

2、黑名单机制

使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到黑名单即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。这样做同样会违背了 JWT 的无状态原则。

3、修改密钥 (Secret)

我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。

4、保持令牌的有效期限短并经常轮换

很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。

续签问题
传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。而 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,但是因为 payload 是参与签名的,一旦这个过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签。解决方案有如下几种:

1、每次请求刷新 jwt

jwt 修改 payload 中的 exp 后整个 jwt 串就会发生改变,因此每次请求都返回一个新的 jwt 给客户端。这种方案的的优点是思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。

2、刷新快要过期的jwt

服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是对客户端不是很友好,有可能会刚好错过刷新时机。

3、使用Refresh Token刷新

服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。