Json Web Token
1、JWT简介
JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:
-
简洁(Compact)
可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
-
自包含(Self-contained)
负载中包含了所有用户所需要的信息,避免了多次查询数据库
2、JWT 组成
- Header 头部
头部包含了两部分,token 类型和采用的加密算法
{
"alg": "HS256",
"typ": "JWT"
}
typ
: (Type)类型。在JOSE Header中这是个可选参数,但这里我们需要指明类型是JWT
。alg
: (Algorithm)算法,必须是JWS支持的算法
它会使用 base64url编码组成 JWT 结构的第一部分
Base64URL 算法
这个算法跟 Base64 算法基本类似,是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是 Base64URL 算法。
- Payload 负载
这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,JWT 规定了7个官方字段,供选用
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
常用的有,
{
"iss": "lee JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "6465644@163.com"
}
同样的,它会使用 base64url 编码组成 JWT 结构的第二部分
- Signature 签名
签名的作用是保证 JWT 没有被篡改过
前面两部分都是使用 base64url 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,这个密钥只有服务器才知道,不能泄露给用户,然后使用 header 中指定的签名算法(HS256)进行签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s
- 签名的目的
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
3、JWT 的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization
字段里面。
-
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
-
后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。
-
后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
-
前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
5.后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
4、JWT 的几个特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
# #!/usr/bin/env python
# # -*- coding:utf-8 -*-
import jwt
import datetime
from jwt import exceptions
JWT_SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv='
def create_token(payload, timeout=20):
"""
:param payload: 例如:{'user_id':1,'username':'wupeiqi'}用户信息
:param timeout: token的过期时间,默认20分钟
:return:
"""#构造headers
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
##构造payload
payload = {
'user_id': 1, # 自定义用户ID
'username': 'pig',
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=5)
}
result = jwt.encode(payload=payload, key=JWT_SALT, algorithm="HS256", headers=headers).decode('utf-8')
return result
def parse_payload(token):
"""
对token进行和发行校验并获取payload
:param token:
:return:
"""
result = {'status': False, 'data': None, 'error': None}
try:
verified_payload = jwt.decode(token, JWT_SALT, True)
result['status'] = True
result['data'] = verified_payload
#print("认证成功!")
except exceptions.ExpiredSignatureError:
result['error'] = 'token已失效'
except jwt.DecodeError:
result['error'] = 'token认证失败'
except jwt.InvalidTokenError:
result['error'] = '非法的token'
return result
if __name__ == '__main__':
token = create_token(payload,1)
print(token)
print(parse_payload(token))
Django REST framework 中使用 JWT认证
使用django-rest-framework-jwt这个库来帮助我们简单的使用jwt进行身份验证
并解决一些前后端分离而产生的跨域问题
- 安装
直接使用pip安装即可,目前支持Python、Django、DRF主流版本
pip install djangorestframework-jwt
- 使用
在settings.py文件中,将JSONWebTokenAuthentication 添加到REST framework框架的DEFAULT_AUTHENTICATION_CLASSES.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
同样,你还可以使用基于APIView
类的视图,在每个视图或每个视图集的基础上设置身份验证方案。与 Token 认证一样,尽可能使用基于APIView
类的视图认证方式。
但使用基于APIView
类的视图认证方式时,不要忘记导入类。
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
在你的urls.py
文件中添加以下URL路由,以便通过POST包含用户名和密码的令牌获取。
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns += [
url(r'^api-token-auth/', obtain_jwt_token)
]
如果你使用用户名admin和密码admin123456创建了用户,则可以通过在终端中执行以下操作来测试JWT是否正常工作。
$ curl -X POST -d "username=admin&password=admin123456" http://127.0.0.1:8000/api-token-auth/
或者,你可以使用Django REST framework支持的所有内容类型来获取身份验证令牌。例如:
$ curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123456"}' http://127.0.0.1:8000/api-token-auth/
现在访问需要认证的API时,就必须要包含Authorization: JWT <your_token> 头信息了:
$ curl -H "Authorization: JWT <your_token>" http://127.0.0.1:8000/virtual/
- 刷新Token
如果JWT_ALLOW_REFRESH
为True,可以“刷新”未过期的令牌以获得具有更新到期时间的全新令牌。像如下这样添加一个URL模式:
from rest_framework_jwt.views import refresh_jwt_token
urlpatterns += [
url(r'^api-token-refresh/', refresh_jwt_token)
]
使用方式就是将现有令牌传递到刷新API,如下所示: {"token": EXISTING_TOKEN}
。请注意,只有非过期的令牌才有效。另外,响应JSON看起来与正常获取令牌端点{"token": NEW_TOKEN}
相同。
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-refresh/
可以重复使用令牌刷新(token1 -> token2 -> token3),但此令牌链存储原始令牌(使用用户名/密码凭据获取)的时间。作为orig_iat,你只能将刷新令牌保留至JWT_REFRESH_EXPIRATION_DELTA。
刷新token以获得新的token的作用在于,持续保持活跃用户登录状态。比如通过用户密码获得的token有效时间为1小时,那么也就意味着1小时后此token失效,用户必须得重新登录,这对于活跃用户来说其实是多余的。如果这个用户在这1小时内都在浏览网站,我们不应该让用户重新登录,就是在token没有失效之前调用刷新接口为用户获得新的token。
- 认证Token
在一些微服务架构中,身份验证由单个服务处理。此服务负责其他服务委派确认用户已登录此身份验证服务的责任。这通常意味着其他服务将从用户接收JWT传递给身份验证服务,并在将受保护资源返回给用户之前等待JWT有效的确认。添加以下URL模式:
from rest_framework_jwt.views import verify_jwt_token
urlpatterns += [
url(r'^api-token-verify/', verify_jwt_token)
]
将Token传递给验证API,如果令牌有效,则返回令牌,返回状态码为200。否则,它将返回400 Bad Request以及识别令牌无效的错误。
- 手动创建Token
有时候你可能希望手动生成令牌,例如在创建帐户后立即将令牌返回给用户。或者,你需要返回的信息不止是Token,可能还有用户权限相关值。你可以这样做:
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
- 其他设置
你可以覆盖一些其他设置,比如变更Token过期时间,以下是所有可用设置的默认值。在settings.py
文件中设置。
JWT_AUTH = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
// 这是用于签署JWT的密钥,确保这是安全的,不共享不公开的
'JWT_SECRET_KEY': settings.SECRET_KEY,
'JWT_GET_USER_SECRET_KEY': None,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
// 如果秘钥是错误的,它会引发一个jwt.DecodeError
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
// Token过期时间设置
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
// 是否开启允许Token刷新服务,及限制Token刷新间隔时间,从原始Token获取开始计算
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
// 定义与令牌一起发送的Authorization标头值前缀
'JWT_AUTH_HEADER_PREFIX': 'JWT',
'JWT_AUTH_COOKIE': None,
一般除了过期时间外,其他配置参数很少改变。具体参数意义当用到时可以查询官网。