文章目录
一、概念
Json web token (JWT)是啥?
JWT 是 JSON Web Token 的缩写,JWT 本身没有定义任何技术实现,它只是定义了一种基于 Token 的会话管理的规则,涵盖 Token 需要包含的标准内容和 Token 的生成过程。
是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
二、背景分析
HTTP 是一个无状态的协议,一次请求结束后,下次在发送服务器就不知道这个请求是谁发来的了(同一个 IP 不代表同一个用户),在 Web 应用中,用户的认证和鉴权是非常重要的一环,实践中有多种可用方案,并且各有千秋。
1.传统基于session
的会话管理/认证
a. session
基本认知
在 Web 应用发展的初期,大部分采用基于 Session 的会话管理方式,逻辑如下:
-
客户端使用用户名密码进行认证
-
服务端认证成功后,生成并存储 Session,将 SessionID 通过 Cookie 返回给客户端
-
客户端访问需要认证的接口时在 Cookie 中携带 SessionID,服务端通过 SessionID 查找 Session 并进行鉴权,返回给客户端需要的数据
b. session
的认证方式暴露的问题
-
服务端开销大:为便用户下次请求的鉴别,服务端会存储
session
,为了快速认证,通常而言session
都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。 -
扩展差:session存储在客户当时请求的服务器的内存中,当需要做分布式等扩展性服务时,数据同步的问题,会变得很麻烦,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
-
易被CSRF攻击:由于客户端使用 Cookie 存储 SessionID,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范 CSRF 攻击。
2. 基于token的会话管理/认证
鉴于基于 Session 的会话管理方式存在上述多个缺点,无状态的基于 Token 的会话管理方式诞生了,所谓无状态,就是服务端不再存储信息,甚至是不再存储 Session,意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,逻辑如下:
- 客户端使用用户名密码进行认证
- 服务端认证成功后,利用特定算法,生成token,将token返回给客户端
- 客户端访问需要认证的接口时在 URL 参数或 HTTP Header (推荐)中加入 Token,服务端通过解码 Token 进行鉴权,返回给客户端需要的数据
基于 Token 的会话管理方式有效解决了基于 Session 的会话管理方式带来的问题。
- 服务端不需要存储和用户鉴权有关的信息,鉴权信息会被加密到 Token 中
- 服务端只需要读取 Token 中包含的鉴权信息即可
- 避免了共享 Session 导致的不易扩展问题不需要依赖 Cookie,有效避免 Cookie 带来的 CSRF 攻击问题
- 服务器使用
CORS
(跨域资源共享) 可以快速解决跨域问题Access-Control-Allow-Origin: *
三、主角JWT
1. jwt基本结构
Jwt是由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样
jwt 结构生成下
- 其构成由三部分构成,分别为:头部(
header
),载荷(payload
),签证(signature
). - 头部和负载以
json
形式存在,三部分分别单独经过了base64
编码,并以"."拼接成一个JWT Token
A .头部 header
头部承载两部分信息:
-
声明类型
typ
-
声明加密算法
alg
,通常是使用默认的HMAC SHA256
// 完整头部header格式如下
{
"typ": "JWT",
"alg": "HS256"
}
//经过base64(urlsafe_b64encode,并把补位符"="换成了"",下同)编码转换,得到jwt第一部分 header_b64
// python 实现:header_b64 = base64.urlsafe_b64encode(json.dumps(header, separators=(',', ':')).encode("utf-8")).replace(b'=', b'')
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
B. 负载 payload
负载(载荷)是存放有效信息的地方,通常会存放一些跟用户相关的自定义信息以及过期时间之类的一些必要信息。
jwt规范中规定了一些字段,并推荐使用。当然开发者也可自定义。
负载也可以认为有三个部分组成(其实就是不同约定的字段):标准中注册的声明、公共的声明、私有的声明
-
标准中注册的声明(建议但不强制使用,一种约定而已)
- iss: jwt签发者
-
sub: jwt所面向的用户
- aud: 接收jwt的一方
-
iat: jwt的签发时间(unix时间戳)
- exp: jwt的过期时间,这个过期时间必须要大于签发时间(unix时间戳)
-
nbf: 定义在什么时间之前,该jwt都是不可用的.
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
-
公共的声明
- 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息
-
私有的声明
- 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息
// payload实例
{
"sub": "7ec1047d81bbc53cce3aa93483551e1c",
"name": "Lucy",
"role": "admin",
"exp": 1567652129
}
// 经过base64(urlsafe_b64encode)编码转换,得到jwt第二部分 payload_b64
// python实现:payload_b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(',', ':')).encode("utf-8")).replace(b'=', b'')
eyJzdWIiOiI3ZWMxMDQ3ZDgxYmJjNTNjY2UzYWE5MzQ4MzU1MWUxYyIsIm5hbWUiOiJMdWN5Iiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNTY3NjUyMTI5fQ
注意:
payload
的内容只不过是经过了base64编码,相当于明文存储,所以切忌防止敏感信息!
C. 签证/签名 signature
-
对头部和负载信息,做签名认证,防止被篡改。
-
需要三部分来生成签名
- header(经过base64编码后的头部)
- payload(经过base64编码后的负载)
- secret(进行加密需要的秘钥)
# 实现方式(python)
# 将 头部header_b64 和 负载payload_b64 用"."连接起来,指定加密的key
import base64
import json
import hmac
secret_key = "abcd123456"
a = hmac.new(secret_key.encode("utf-8"), b'.'.join([header_b64, payload_b64]), digestmod="sha256").digest()
signature = base64.urlsafe_b64encode(a).replace(b'=', b'')
segments.append(signature)
# 得到第三部分签名值
rxD-kI42kRIFHNSMcjoJe30sbcHToyP_VsWG6kH_uMI
// js实现
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, secret_key);
将三部分用"."连起来就是一个完整的token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3ZWMxMDQ3ZDgxYmJjNTNjY2UzYWE5MzQ4MzU1MWUxYyIsIm5hbWUiOiJMdWN5Iiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNTY3NjUyMTI5fQ.rxD-kI42kRIFHNSMcjoJe30sbcHToyP_VsWG6kH_uMI
注意:
secret_key
是需要严格保密的,如果这部分泄漏出去了,客户端就可以随意签发token凭证了!
# python中jwt包的适用
# 结合上述的三部分数据
import jwt
token = jwt.encode(
payload=payload,
key=secret_key,
algorithm='HS256'
)
print(token)
# 得到的结果,跟上述自己加密是一摸一样的
# 自实现的加密中,把补位符"="换成了"",是由于jwt包内部是这么实现的,为了保证两中方式得到的值一样,所以也做了同样的操作。
2.使用
- 一般是约定好字段,加在请求头中,传递给服务器,由其解析。如:
TOKEN
- 服务端会验证token,如果验证通过就会返回相应的资源
大致流程如下:
3. jwt优缺点分析
-
JWT 拥有基于 Token 的会话管理方式所拥有的一切优势,不依赖 Cookie,使得其可以防止 CSRF 攻击,也能在禁用 Cookie 的浏览器环境中正常运行。
-
服务端不再需要存储 Session,使得服务端认证鉴权业务可以方便扩展,避免存储 Session 所需要引入的 Redis 等组件,降低了系统架构复杂度。
-
因为json的通用性,所以JWT是可以进行跨语言支持
-
jwt的构成非常简单,字节占用很小,便于传输
-
服务端不存储token,也是 JWT 最大的劣势,由于有效期存储在 Token 中,JWT Token 一旦签发,就会在有效期内一直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的 JWT Token,如果需要禁用用户,单纯使用 JWT 就无法做到了
-
一定要保护好secret私钥,该私钥非常重要!
4. jwt的引申扩展
针对jwt的签发后,不易控制的缺点,我们可以做些折中方案,弱化它的影响。
引入 Refresh Token
(token续签模式)
- 客户端使用用户名密码进行认证服务端生成有效时间较短的 Access Token(例如 10 分钟),和有效时间较长的 Refresh Token(例如 7 天)客户端访问需要认证的接口时,携带 Access Token如果 Access Token 没有过期,服务端鉴权后返回给客户端需要的数据如果携带 Access Token 访问需要认证的接口时鉴权失败(例如返回 401 错误),则客户端使用 Refresh Token 向刷新接口申请新的 Access Token如果 Refresh Token 没有过期,服务端向客户端下发新的 Access Token客户端使用新的 Access Token 访问需要认证的接口
Refresh Token
是存储在服务器中,如果它过期了,那么就无法换取token,得以有效的控制Refresh Token
只是用来换token,请求量远低于token验证,可保存在sql数据库中,对于性能影响不会很大- 结合客户端的主动退出登录,也可以删除所有
token
、Refresh Token
,使得控制粒度变得更细
流程:
5. 思考问题:
- 如果token没过期,用
RefreshToken
来刷新token的话 ,因为JWT是无状态的,那么就会存在多个有效Token,怎么处理? - 如果要维护黑名单人员,那么是不是又回到了,存储token的方式,设计token数据同步?
- … … …