一、JWT的构成
是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。由三部分构成,第一部分:头部(header) 第二部分:载荷(payload) 第三部分:签证(signature).
允许我们使用JWT在用户和服务器之间传递安全可靠的信息。主要用于向Web应用传递一些非敏感信息。
header:jwt的头部承载两部分信息
- 声明token类型,这里是jwt
- 声明生成签名所使用的加密算法 通常直接使用 HS256
完整的头部就像下面这样的JSON:
{ 'type': 'JWT', 'alg': 'HS256' }
然后将头部进行base64编码(此编码可直接解码,相当于明文),构成了第一部分:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload:载荷存放着有效信息,通常包括有效时间,包含三个部分
- 注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp(expires): jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat(issued at): jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true}
然后将其进行base64编码,得到JWT的第二部分:eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature:签证信息,由三部分组成
- header (base64编码后的)
- payload (base64编码后的)
- secret
这个部分需要:HMACSHA256(base64编码后的header + . + base64编码后的payload, secret
)
所连接组成的字符串就构成了jwt的第三部分。
最后将三部分使用 . 连接得到最终的JWT-token signature签证:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
'密钥泄露 !!!':未泄露时私钥定期动态生成,若已经泄露立即重新生成secret_key,使用 https协议替代http.
二、基于token的鉴权机制
基于token的鉴权机制是类似于http协议,也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了。
流程:
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,另外服务端要支持CORS(跨域资源共享)
策略,一般我们在前端请求头中加入:'Authorization': 'JWT ' + token。在后端的配置文件中加入jwt的认证权限即可,一旦前端传递了此字段的值,后端会自动开始认证操作.前端请求头中加入的字段值'JWT '有一个空格,是python的JWT鉴权机制规定的,后期源码中需要通过分隔拿到
前端存储JWT的位置:前端代码将该JWT存放到 Local Storage 里待用,或是服务端直接在cookie中保存 HttpOnly=false 的JWT(可能会造成另一种攻击:跨站脚本攻击XSS)。
用户注册&登陆请求 --> 校验用户名&密码,服务器为当前用户生成token --> 响应给浏览器进行存储在本地 ---> 浏览器再次登陆时请求体携带token --> 服务器验证token,通过实现状态保持
三、Django框架生成JWT-token
# 手动生成 JWT-token 令牌jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER # 将'生成载荷'的函数 引用给变量jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER # 将'生成JWT-token加密字符串'的函数 引用给变量payload = jwt_payload_handler(user) # 通过user模型调用函数 动态生成属于当前用户的 载荷token = jwt_encode_handler(payload) # 通过载荷调用函数 动态生成属于当前用户的 token令牌user.token = token # 为user对象多设置 token属性,将来前端可将其保存至浏览器中
四、对比session
传统的session认证
http协议本身是无状态的,而这意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还需要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.
基于session认证所显露的问题
1. 每个用户经过应用认证之后,都要将当前用户的信息存储在服务端,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
2. CSRF: 因为session是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到CSRF跨站请求伪造的攻击。
3. cookie遵循`同源策略`,如果在大型项目中将不同的功能模块部署不同服务器,不同域名下时,会出现跨机器跨域名访问,将无法携带cookie 带给服务器。
4. 浏览器是可以禁用掉cookie的,会导致session瘫痪,无法取出通过cookie保存的session_id拿到session记录
5. cookie和session保存的数据没有进行加密,对于保护用户隐私不安全
jwt优点
- 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,Python,PHP等很多语言都可以使用。
- 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
- 易于应用的水平扩展,它不需要在服务端保存会话信息,JWT所生成的token都存储在客户端中。
- 可防护CSRF跨站请求伪造,基于cookis-session的认证会在每次请求时,自动将其发送给服务器,而通过请求头将JWT发送给服务器端,而不再通过cookie自动携带。
- 可实现多站点下的状态保持(共享token)
- 相较于cookie-session更加安全,通过头部声明的加密算法和加盐(secret)对jwt-token进行加密。
- 对于禁用浏览器cookie的用户也能够实现状态保持。
- 拥有内置的过期功能
jwt缺陷
- 更多的空间占用。如果将原本存储在服务端session中的各类信息都放在JWT中,并保存在客户端时,可能会很快超过一个cookie或者URL的大小限制,但如果放在Local Storage,则可能受到XSS攻击。
- 更不安全。这里是特指将JWT保存在Local Storage中,然后使用Javascript取出后作为HTTP header发送给服务端的方案。在Local Storage中保存敏感信息并不安全,容易受到跨站脚本攻击,跨站脚本(Cross site script,简称xss)是一种“HTML注入”,由于攻击的脚本多数时候是跨域的,所以称之为“跨域脚本”,这些脚本代码可以盗取cookie或是Local Storage中的数据。
- 无法作废已颁布的令牌。所有的认证信息都在JWT中,由于在服务端没有状态,即使你知道了某个JWT被盗取了,你也没有办法将其作废。在JWT过期之前(你绝对应该设置过期时间),你无能为力。
- 不易应对数据过期。与上一条类似,JWT有点类似缓存,由于无法作废已颁布的令牌,在其过期前,你只能忍受“无用”的数据。
如何防护
- 不再使用 Local Storage 存储JWT,使用cookie,并且设置HttpOnly=true,这意味着只能由服务端保存以及通过自动回传的cookie取得JWT,以便防御XSS攻击
- 在JWT的内容中加入一个随机值作为CSRF令牌,由服务端将该CSRF令牌也保存在cookie中,但设置HttpOnly=false,这样前端Javascript代码就可以取得该CSRF令牌,并在请求API时作为HTTP header传回。服务端在认证时,从JWT中取出CSRF令牌与header中获得CSRF令牌比较,从而实现对CSRF攻击的防护
- 考虑到cookie的空间限制(大约4k左右),在JWT中尽可能只放“够用”的认证信息,其他信息放在数据库,需要时再获取,同时也解决之前提到的数据过期问题
- jwt主动过期的实现,使用黑名单即可;分成两点,客户端要求失效,服务端记录token到黑名单;用户重置密码,服务端记录uid-time键值对,在此之前的token全部失效
- jwt续签问题,一种解决方式是jwt中存储过期时间,服务端设置刷新时间,请求是判断是否在过期时间或刷新时间,在刷新时间内进行token刷新,失效token记入黑名单;另一种如果黑名单过大问题,可以采用记录UID-刷新时间方式解决,判断jwt签发时间,jwt签发时间小于UID-刷新时间的记为失效。
HttpOnly? 如果在cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击
如何选择
- 在Web应用中,别再把JWT当做session使用,绝大多数情况下,传统的cookie-session机制工作得更好
- JWT适合一次性的命令认证,颁发一个有效期极短的JWT,即使暴露了危险也很小,由于每次操作都会生成新的JWT,因此也没必要保存JWT,真正实现无状态。