一、传统session认证的问题
JWT
,英文全名:JSON Web Token
,是目前最流行的跨域身份验证解决方案之一!
在介绍 JWT 之前,我们先来聊一聊基于传统session认证
的方案以及瓶颈。
传统session
交互流程,如下图:
当浏览器向服务器发送登录请求时,验证通过之后,会将用户信息存入seesion
中,然后服务器会生成一个sessionId
放入cookie
中,随后返回给浏览器。
当浏览器再次发送请求时,会在请求头部的cookie
中放入sessionId
,将请求数据一并发送给服务器。
服务器就可以再次从seesion
获取用户信息,整个流程完毕!
通常在服务端会设置seesion
的时长,例如 30 分钟没有活动,会将已经存放的用户信息从seesion
中移除。
session.setMaxInactiveInterval(30 * 60);//30分钟没活动,自动移除
同时,在服务端也可以通过seesion
来判断当前用户是否已经登录,如果为空表示没有登录,直接跳转到登录页面;如果不为空,可以从session
中获取用户信息即可进行后续操作。
在单体应用中,这样的交互方式,是没啥问题的。
但是,假如应用服务器的请求量变得很大,而单台服务器能支撑的请求量是有限的,这个时候就容易出现请求变慢或者OOM。
解决的办法
,要么给单台服务器增加配置
,要么增加新的服务器
,通过负载均衡
来满足业务的需求。
如果是给单台服务器增加配置,请求量继续变大,依然无法支撑业务处理。
显而易见,增加新的服务器,可以实现无限的水平扩展。
但是增加新的服务器之后,不同的服务器之间的sessionId
是不一样的,可能在A服务器上已经登录成功了,能从服务器的session
中获取用户信息,但是在B服务器上却查不到session
信息,此时肯定无比的尴尬,只好退出来继续登录,结果A服务器中的session
因为超时失效,登录之后又被强制退出来要求重新登录,想想都挺尴尬~~
面对这种情况,几位大佬于是合起来商议,想出了一个token方案
。
将各个应用程序与内存数据库redis
相连,对登录成功的用户信息进行一定的算法加密,生成的ID被称为toke
n,将token
还有用户的信息存入redis
;等用户再次发起请求的时候,将token
还有请求数据一并发送给服务器,服务端验证token
是否存在redis
中,如果存在,表示验证通过,如果不存在,告诉浏览器跳转到登录页面,流程结束。
token方案
保证了服务的无状态,所有的信息都是存在分布式缓存
中。基于分布式存储
,这样可以水平扩展来支持高并发。
当然,现在springboot
还提供了session
共享方案,类似token方案
将session
存入到redis
中,在集群环境下实现一次登录之后,每个服务器都可以获取到用户信息。
二、JWT是什么
上文中,我们谈到的session
还有token
的方案,在集群环境下,他们都是靠第三方缓存数据库redis
来实现数据的共享。
那有没有一种方案,不用缓存数据库redis
来实现用户信息的共享,以达到一次登录,处处可见的效果呢?
答案肯定是有的,就是我们今天要介绍的JWT!
JWT
全称JSON Web Token
,实现过程简单的说就是用户登录成功之后,将用户的信息进行加密,然后生成一个token
返回给客户端,与传统的session
交互没太大区别。
交互流程如下:
唯一的不同点就是:token
存放了用户的基本信息,更直观一点就是将原本放入redis
中的用户数据,放入到token
中去了!
这样一来,客户端、服务端都可以从token
中获取用户的基本信息,既然客户端可以获取,肯定是不能存放敏感信息的,因为浏览器可以直接从token获取用户信息。
JWT具体长什么样呢?
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分:我们称它为头部(header)
,用于存放token
类型和加密协议,一般都是固定的;
第二部分:我们称其为载荷(payload)
,用户数据就存放在里面;
第三部分:是签证(signature)
,主要用于服务端的验证;
1、header
JWT的头部承载两部分信息:
-
声明类型,这里是
JWT
; -
声明加密的算法,通常直接使用
HMAC SHA256
;
完整的头部就像下面这样的JSON
:
{
'typ': 'JWT',
'alg': 'HS256'
}
使用base64
加密,构成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2、playload
载荷就是存放有效信息的地方,这些有效信息包含三个部分:
- 标准中注册的声明;
- 公共的声明;
- 私有的声明;
其中,标准中注册的声明 (建议但不强制使用)包括如下几个部分 :
- iss: jwt签发者;
- sub: jwt所面向的用户;
- aud: 接收jwt的一方;
- exp: jwt的过期时间,这个过期时间必须要大于签发时间;
- nbf: 定义在什么时间之前,该jwt都是不可用的;
- iat: jwt的签发时间;
- jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击;
公共的声明部分: 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明部分: 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload
:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64
加密,得到Jwt
的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
3、signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header
(base64后的);payload
(base64后的);secret
(密钥);
这个部分需要base64
加密后的header
和base64
加密后的payload
使用.连接组成的字符串,然后通过header
中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
//javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '密钥');
加密之后,得到signature
签名信息。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,就构成了最终的jwt:
//jwt最终格式
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
这个只是通过javascript实现的一个演示,JWT的签发和密钥的保存都是在服务端来完成。
secret
用来进行jwt的签发和jwt的验证,所以,在任何场景都不应该流露出去。
三、方案实践
介绍了这么多