jwt原理以及使用
jwt原理以及使用
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
原本通过客户端cookie和服务端session各存一个随机字符串的方式对服务端的存储压力很大,于是token应运而生,它只要求客户端存储一串加密字段,而不需要服务端存储。
jwt的组成
- 头header,用于声明类型,声明加密算法等,甚至会添加公司信息
- 荷载payload,存放有效信息的地方,过期时间、签发时间、用户id等
- 签名signature,是通过前两个部分通过秘钥+加密的方式得到的
整体是以.
隔开的三段密文,而密文的加密方式一般是base64。
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
base64编码
base64是可以解码的一种编码格式,它加密的结果一定是4的倍数(不足会以=补足)
import base64
import json
# base64编码
payload = {'user_id': 1, 'exp': 1234567890}
dic_str = json.dumps(payload) # 将字典转为字符串
enc_payload = base64.b64encode(dic_str.encode('utf-8')) # 二进制加密
print(enc_payload)
# base64解密
print(base64.b64decode('base64的密文')) # 是不是二进制都可以
base编码常用于网络运输,让数据看起来像密文,一般人就不去尝试解密了。
ps:因为base64是对二进制进行加密,所以照片数据等也都可以用base64加密传输
jwt签发认证流程
访问登录接口时,发送header和payload的数据,然后在服务端使用加密算法和秘钥进行签名,生成token,然后响应时将token返回给浏览器,让浏览器进行保存。
访问其他接口时,浏览器将token发往服务端,再次将header和payload按相同的加密方式和秘钥加密,得到的签名与token第三段(也就是第一次的签名)比对,比对成功则认证成功。
drf-jwt使用
django+drf 平台开发jwt有两个常用的模块:
- djangorestframework-jwt:老版本了,但一直可以用
- djangorestframework-simplejwt:实际工作中,公司用的多一些
- 也可以尝试自己封装jwt签发和认证
使用djangorestframework-jwt
设置登录接口
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
...
path('login/', obtain_jwt_token),
]
配置个路由即可,它会基于django原生的auth_user表去封装登录接口。
设置过期时间和返回格式
import datetime
JWT_AUTH = {
# 过期时间1天
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 如果不自定义,返回的格式是固定的,只有token字段
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}
如果想要定制返回格式,让它不止有token字段,可以重写jwt_response_payload_handler,然后直接配置到字典中即可。
def jwt_response_payload_handler(token, user=None, request=None):
return {
'code': 100,
'msg': '登录成功',
'token': token,
'username': user.username
# 'icon':user.icon
}
# 配置一下
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler', }
认证类配置
rest_framework_jwt配套了认证类,可以直接导入视图类配置。
但是注意,这个认证类在没有传token时是直接通过的,这个逻辑有点小尴尬,所以一般配合一个drf的权限类使用。
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated] # 登录用户有权限,不登录用户没权限
前端请求的请求头形式:Authorization=有效的token值
drf-jwt源码分析
首先,最基础的用法告诉我们,只需要配置个路由,就可以使用登录接口了。那么它所配置的obtain_jwt_token,我们就从这里开始分析。这里的登录功能拥有签发token的功能又叫签发接口。
签发接口
obtain_jwt_token = ObtainJSONWebToken.as_view() # 是一个视图类
# 返回数据的格式是可以定制的
jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER
class ObtainJSONWebToken(JSONWebTokenAPIView):
# 视图类中继承了一个APIView,并配置了一个序列化类
serializer_class = JSONWebTokenSerializer
class JSONWebTokenAPIView(APIView):
permission_classes = ()
authentication_classes = ()
def 。。。
# 在父类中有一个函数是8大网络请求中的请求方式post
def post(self, request, *args, **kwargs):
# 可以配置的serializer
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
# 默认的response_data只拿一个{token:token}
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data) # 产生响应对象
if api_settings.JWT_AUTH_COOKIE: # 如果设置了cookie的键
# 那么就发送cookie键值,我们可以将键设置为"token"
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True)
return response # 返回响应,可以保存到cookie
# 验证不成功时返回的响应
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
签发接口总结:
-
被封装在一个视图类中,通过post请求可以访问到
-
按配置的序列化类去反序列化校验,如果校验成功则继续,失败则返回400错误响应
-
拿到token和user(猜想:token可能是在执行is_valid时在校验过程中顺带产生的),传入配置的响应处理函数
默认的函数jwt_response_payload_handler是只返回token的。 -
产生响应Response对象,如果设置了cookie的键,那么将token保存在cookie中
-
将Response对象返回。
token的产生
在is_valid中,会依次执行各种校验,包括钩子函数,这些都是放在序列化类中。
而默认的视图化类中,使用的序列化类使用的钩子函数如下:
from django.contrib.auth import authenticate
from django.utils.translation import ugettext as _
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER # 荷载获取函数可配置
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER # jwt编码函数可配置
class JSONWebTokenSerializer(Serializer):
。。。
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field), # 'username':数据
'password': attrs.get('password')
}
if all(credentials.values()): # 如果所有的数据都是传了的则执行校验
user = authenticate(**credentials) # 通过传入的数据认证得到user
if user: # 如果是有效的user
# 校验是否活跃
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
# 通过user包装得到荷载
payload = jwt_payload_handler(user)
# 将token和user数据作为有效数据传出
return {
'token': jwt_encode_handler(payload), # 通过荷载编码签发
'user': user
}
# 如果没有匹配用户则报校验错误
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
# 如果数据没传全,也报校验错误
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
## 认证函数authenticate:功能就是基于auth_user表进行密码校验,返回user数据对象
## 荷载获取函数
def jwt_payload_handler(user):
username_field = get_username_field()
username = get_username(user)
...
payload = {
'user_id': user.pk,
'username': username,
'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
}
... # 各种添加payload字段,
return payload
## jwt编码函数 -- 传入荷载,返回token值
def jwt_encode_handler(payload):
key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload)
return jwt.encode(
payload,
key,
api_settings.JWT_ALGORITHM
).decode('utf-8')
token的认证
def authenticate(self, request):
# 拿到请求中携带过来的token参数,根据内层源码,要求请求头的键为authorization
"""
def get_jwt_value(self, request):
auth = get_authorization_header(request).split()
...
return auth[1]
"""
jwt_value = self.get_jwt_value(request)
# 如果没有携带就不校验,转而去校验下一个认证类
if jwt_value is None:
return None
try:
# 核心验证代码,对token进行校验
payload = jwt_decode_handler(jwt_value)
except 。。。# 有异常则或捕获处理
user = self.authenticate_credentials(payload)
return (user, jwt_value)