JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案
跨域认证问题
互联网服务离不开用户认证,一般流程是这样的
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
JWT认证流程
在项目开发中,一般会按照上图所示过程进行认证; 即: 用户登录成功之后,服务器给浏览器返回一个token,以后用户再访问浏览器携带token再去服务端发送请求,服务端效验token的合法性,合法则给用户展示数据,否则,返回一些错误信息
深入了解jwt:https://www.freebuf.com/articles/web/180874.html
传统token方式和jwt在认证方面有什么差异
- 传统token方式
用户登录成功以后,服务端生成一个随机token给用户,并且在服务端(数据库或缓存)中保存一份token,以后用户再来访问时需要携带token,服务端接收到token之后,需要去数据库或缓存里取校验token是否超时,合法
- jwt方式
用户登录成功以后,服务端通过jwt生成一个随机token给用户(服务器无需保留token),以后用户访问时需携带token,服务端接受到token之后,通过jwt对token进行校验是否超时,是否合法
JWT创建TOKEN
jwt的原理
jwt的生成格式如下,由.
连接的三段字符串组成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva G4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
生成规则如下
- 第一段header部分,固定包含算法的token类型,对此json进行base64加密,这就是token第一段
- 声明类型,这里是jwt
- 声明加密的算法,通常直接使用hmac sha256
{ ‘typ‘: ‘JWT‘, ‘alg‘: ‘HS256‘ }
- 然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
- 第二段payload部分,包含一些数据,对此json进行base64加密,得到第二部分
- 标准中注册的声明(建议但不强制使用)
- 公有的声明
- 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
- 私有的声明
- 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
{ "sub": "1234567890", "name": "John Doe", "admin": true, .... }
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
- jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- 把前鲁昂段的base64后的密文通过
.
拼接起来,然后进行hs256加密,然后对hs256密文进行base64url加密,得到token第三段 - header (base64后的)
- payload (base64后的)
- secret
base64url( HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret (秘钥加盐) ) )
- 把前鲁昂段的base64后的密文通过
最后将三段字符串通过.
拼接起来,就成了jwt的token
注意: base64url加密是先做base64加密,然后将-
代替+
及_
代替/
代码实现
基于python 的 pyjwt模块创建jwt的token
安装jwt
pip install pyjwt
import jwt import datetime SALT = "ASDASDADFVQWEQFq@eq!dfqwetwgsdfaCADVQEERQERWQEQ134145235!#$!#" # 设置超时 时间
# timeout = datetime.datetime.now() + datetime.timedelta(seconds=70) # 本地时间 timeout = datetime.datetime.utcnow() + datetime.timedelta(seconds=70) # 世界时间 def create_jwt(): # 构造header headers = { ‘typ‘: ‘jwt‘, ‘alg‘: ‘HS256‘ } """headers 中一些固定参数名称的意义""" # jku: 发送JWK的地址;最好用HTTPS来传输 # jwk: 就是之前说的JWK # kid: jwk的ID编号 # x5u: 指向一组X509公共证书的URL # x5c: X509证书链 # x5t:X509证书的SHA-1指纹 # x5t#S256: X509证书的SHA-256指纹 # typ: 在原本未加密的JWT的基础上增加了 JOSE 和 JOSE+ JSON。JOSE序列化后文会说及。适用于JOSE标头的对象与此JWT混合的情况。 # crit: 字符串数组,包含声明的名称,用作实现定义的扩展,必须由 this->JWT的解析器处理。不常见。 # 构造payload payload = { ‘user_id‘: 1, # 自定义用户id ‘username‘: ‘nayue‘, # 自定义用户名 ‘exp‘: timeout # 超时时间 } """payload 中一些固定参数名称的意义, 同时可以在payload中自定义参数""" # iss 【issuer】发布者的url地址 # sub 【subject】该JWT所面向的用户,用于处理特定应用,不是常用的字段 # aud 【audience】接受者的url地址 # exp 【expiration】 该jwt销毁的时间;unix时间戳 # nbf 【not before】 该jwt的使用时间不能早于该时间;unix时间戳 # iat 【issued at】 该jwt的发布时间;unix 时间戳 # jti 【JWT ID】 该jwt的唯一ID编号 result = jwt.encode( headers=headers, # json web token 数据结构包含两部分, payload(有效载体), headers(标头) payload=payload, # payload, 有效载体 key=SALT, # 进行加密签名的密钥 lgorithm=‘HS256‘ # 指明签名算法方式, 默认也是HS256 ).decode("utf-8") # python3 编码后得到 bytes, 再进行解码(指明解码的格式), 得到一个str return result if __name__ == "__main__": token = create_jwt() print(token) # eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im5heXVlIiwiZXhwIjoxNTkxMzYzMTkyfQ.ue2bHYw6f555OiiPAj_Rmd3hKlIQNtOtYJ3unPiB80g
JWT校验token
一般在认证成功之后,会把jwt生成的token返回给用户,以后用户再次访问的时候就要携带token,此时jwt需要对token进行超时和合法性校验
获取token后,会按照以下步骤进行校验
- 将token分割成
header_segment
,payload_segment
,crypto_segment
三部分 - 对第一部分
header_segment
进行base64url解密,得到header
- 对第二部分
payload_segment
进行base64url解密,得到payload
- 对第三部分
crypto_segment
进行base64url解密,得到signature
- 对第三部分
signature
部分数据进行合法性校验- 拼接前两段密文,即:
signing_input
- 从第一段文明中获取加密算法,默认:
HS256
- 使用 算法+盐 对
siging_input
进行加密,将得到的结果和signature密文进行比较
- 拼接前两段密文,即:
import jwt from jwt import exceptions def get_payload(jwt_token): try: # 需要解析的 jwt_token 密钥 使用和加密时相同的算法 data = jwt.decode(jwt_token, SALT, algorithms=‘HS256‘) # 解析出来的就是 payload 内的数据 return data # 如果 jwt 被篡改过; 或者算法不正确; 如果设置有效时间, 过了有效期; 或者密钥不相同; 都会抛出相应的异常 except exceptions.ExpiredSignatureError: return "token失效" except jwt.DecodeError: return "token认证失败" except jwt.InvalidTokenError: return "非法的Tooken" except Exception as error: return error if __name__ == "__main__": # token = create_jwt() token = "eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Im5heXVlIiwiZXhwIjoxNTkxMzM3NjA5fQ.5NHXMITig2reOFUngQg4aknHa3bT5wurVnci6NWvEQw" data = get_payload(token) print(data) # {‘user_id‘: 1, ‘username‘: ‘nayue‘, ‘exp‘: 1591363192} JWT总结
- 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
- 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
- 它不需要在服务端保存会话信息, 所以它易于应用的扩展
注意
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议
JSON WEB TOKEN 实战
Django案例
在用户登录成功以后,生成token并返回,用户再来访问时需携带token
此示例在django的中间件中对token进行效验,内部编写了两个中间件来支持用户通过两种方式传递token