前言
对于前后端分离的项目中,我们通常会在数据库中给用户表设计一个token的字段,是一种判别用户的手段,但如果当用户数据量大的时候,我们的数据库将存放很多用户token,并且每次登录的时候都会进行数据库查询。
如果我们还想让用户登录完成后的token设置超时时间,那么数据库的字段又需要加一列,显然是非常不友好的,为了解决这个问题,jwt的作用就诞生了。
jwt官网地址:https://jwt.io/introduction
通过python的方式使用jwt需安装pyjwt如下:
pip install pyjwt
一、jwt认证流程、原理
jwt(JSON Web Tokens),是一种开发的行业标准 RFC 7519 ,用于安全的表示双方之间的声明。目前,jwt广泛应用在系统的用户认证方面,特别是现在前后端分离项目
。
那么对于jwt而言是怎么通过内部自实现一个token呢?
基于jwt实现token如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
可以看到通过jwt生成的token是由三段字符串组成
,并且用.连接
起来,而这三段字符串是通过什么样的加密方式演算出来的呢?我们将逐一解析。
- 对于第一段字符串,我们将其密钥称为HEADER(
算法与令牌类型
)是一个字典,其内部包含了使用算法默认为HS256(指哈希消息认证码,包含有很多种哈希加密算法,HS256是其中一种)
,以及token加密方式默认为JWT。
{
"alg": "HS256",
"typ": "JWT"
}
那么对于使用JWT的方式进行token加密,其内部是通过json转化成字符串,然后做base64url加密(base64加密 + 替换
),那么此时第一段字符串的加密就完成了。
- 对于第二段字符串,我们将其密钥称为PAYLOAD(
自定义加密数据
),并且还可以设置token有效时间。
{
"sub": "1234567890",
"name": "John Doe",
"exp": 1516239022 # 超时时间
}
而其加密过程和第一段字符串一样使用了base64url加密,但是这里要注意,因为该加密方式是通过base64加密 + 替换操作实现的,本质上是可以解密的
,所以对于在实际应用场景中,不得将用户敏感数据传入
。
- 对于第三段字符串而言,就有点复杂了该加密方式通过第一、二生成的密文进行拼接,并对拼接完成的密文进行HS256加密并将服务器唯一私钥当成密钥放入,之后在进行一次base64url加密。
那么对于用户携带token后端进行token校验的流程如下:
- 对token进行切割(
以.的方式
) - 对第二段进行base64url解密,并获取payload信息(
通过参数exp判断token1是否已超时
) - 将第1、2段的密文拼接,再次执行sha256加密之后将服务器唯一私钥放入在和之前的加密token进行比较,如果相等表示token未被修改认证通过
那么明白流程后,我们就通过DRF的认证类来使用吧。
二、通过jwt实战DRF认证
这里我们为了方便测试通过sqllite3创建用户表,表结构如下:
姓名 | 密码 |
---|---|
测试员 | 666666 |
models.py如下:
class UserTest(models.Model):
password = models.CharField(verbose_name="密码", max_length=32)
username = models.CharField(verbose_name='姓名', max_length=64)
python manage.py makemigrations -----生成迁移文件
python manage.py migrate -----执行迁移命令
urls.py如下:
from django.conf.urls import url
from api import views
urlpatterns = [
url(r'^jwt_login/$', views.JwtLoginView.as_view()),
url(r'^jwt_order/$', views.JwtOrderView.as_view()),
]
认证类如下:
from rest_framework.authentication import BaseAuthentication
from api import models
from rest_framework.exceptions import AuthenticationFailed
from django.conf import settings
import jwt
from jwt import exceptions
class JwtAuthentication(BaseAuthentication):
def authenticate(self, request):
token = request.META.get('HTTP_AUTHORIZATION', None)
# 如果没有获取到token(即未登录状态)
if not token:
raise AuthenticationFailed()
salt = settings.SECRET_KEY
# 1.切割
# 2.解密第二段/判断过期
# 3.验证第三段合法性
try:
payload = jwt.decode(token, salt, "HS256")
except exceptions.ExpiredSignatureError:
raise AuthenticationFailed({'code': 1003, "error": "token已失效"})
except jwt.DecodeError:
raise AuthenticationFailed({'code': 1003, "error": "token认证失败"})
except jwt.InvalidTokenError:
raise AuthenticationFailed({'code': 1003, "error": "非法的token"})
return (payload, token)
此时在视图上authentication_classes继承该类即可实现通过在请求头AUTHORIZATION中获取到的token进行判断。
认证相关的完成后,就要到通过jwt生成token,我们将其封装成一个函数create_token并放到encrypt.py中。
函数如下:
import datetime
import jwt
from django.conf import settings
def create_token(payload, timeout=1):
salt = settings.SECRET_KEY
# 构造headr
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
# 构造payload
payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)
# 加密
token = jwt.encode(payload=payload, key=salt, algorithm="HS256", headers=headers)
return token
该函数有两个传参,timeout为超时时间(以分钟为单位默认为1分钟
),payload为之前讲过token加密参数中的第二段字符串中加密需要的参数(结合timeout可以给token设置过期时间
),而headers即为第一段字符串中的加密方式(这里选择默认方式
),最后在传入服务器端中的私钥通过HS256
的加密方式进行,返回结果给token。
views.py如下:
from rest_framework.views import APIView
from api.authentication.auth import JwtAuthentication
from api import models
from rest_framework.response import Response
from utils.encrypt import create_token
# 用户登录页
class JwtLoginView(APIView):
def post(self, request, *args, **kwargs):
user = request.data.get('username')
pwd = request.data.get('password')
user_object = models.UserTest.objects.filter(username=user, password=pwd).first()
if not user_object:
return Response({'code': 1000, "error": "用户名或密码错误"})
token = create_token({'id': getattr(user_object, 'id', None), 'name': getattr(user_object, "username", None)})
return Response({'code': 1001, "data": token})
# 登录才能访问
class JwtOrderView(APIView):
authentication_classes = [JwtAuthentication]
def get(self, request, *args, **kwargs):
print(request.user, request.auth)
return Response('订单列表')
这里用了一个知识点通过getattr(“对象”,“字段”,“默认”)也等价于对象.字段。
此时我们通过postman进行测试如下:
可以看到通过jwt认证其token是由三部分组成的(两个.分割
),并且token正确也能通过,token错误、超时认证类也会进行处理,此时的jwt结合DRF认证类将更加强大、安全、效率上也有所提高。
三、jwt校验token源码分析
在上述使用jwt进行token加密、解密中是否按照着我们之前分析的那种情况来的呢?我们现在通过阅读源码的形式进行分析。
在前面中写的认证类解密、登录时加密token的封装其内部都调用了一个jwt的decode、encode函数,并需传多个参数,这里就讲常用的参数。
- decode(
参数:“服务端返回给用户的真实token”,“服务端私钥”,“加密方式”
)该函数将token进行解密 - encode(
参数:“用户返回数据(超时时间)”,“服务端私钥”,“加密方式”,“第一次字符串加密参数headers”
)该函数将会生成token
从生成token的函数encode进行源码分析如下:
def encode(
self,
payload: Dict[str, Any],
key: str,
algorithm: str = "HS256",
headers: Optional[Dict] = None,
json_encoder: Optional[Type[json.JSONEncoder]] = None,
) -> str:
# Check that we get a mapping
if not isinstance(payload, Mapping):
raise TypeError(
"Expecting a mapping object, as JWT only supports "
"JSON objects as payloads."
)
# Payload
payload = payload.copy()
for time_claim in ["exp", "iat", "nbf"]:
# Convert datetime to a intDate value in known time-format claims
if isinstance(payload.get(time_claim), datetime):
payload[time_claim] = timegm(payload[time_claim].utctimetuple())
json_payload = json.dumps(
payload, separators=(",", ":"), cls=json_encoder
).encode("utf-8")
return api_jws.encode(json_payload, key, algorithm, headers, json_encoder)
前面传参数的不管,我们看后面,该函数通过payload.copy()继续了浅拷贝,然后遍历一个数组(内部会帮我们实现token过期时间
)通过判断time_claim的值执行相应函数。而后面的操作就相当于我们前面说过将数据转化为json格式然后进行替换操作了。最后调用了api_jws.encode将替换好的值传入,此时我们访问一下该函数如下:
def encode(
self,
payload: bytes,
key: str,
algorithm: str = "HS256",
headers: Optional[Dict] = None,
json_encoder: Optional[Type[json.JSONEncoder]] = None,
) -> str:
segments = []
if algorithm is None:
algorithm = "none"
if algorithm not in self._valid_algs:
pass
# Header
header = {"typ": self.header_typ, "alg": algorithm}
if headers:
self._validate_headers(headers)
header.update(headers)
json_header = json.dumps(
header, separators=(",", ":"), cls=json_encoder
).encode()
segments.append(base64url_encode(json_header))
segments.append(base64url_encode(payload))
# Segments
signing_input = b".".join(segments)
try:
alg_obj = self._algorithms[algorithm]
key = alg_obj.prepare_key(key)
signature = alg_obj.sign(signing_input, key)
except KeyError:
if not has_crypto and algorithm in requires_cryptography:
raise NotImplementedError(
"Algorithm '%s' could not be found. Do you have cryptography "
"installed?" % algorithm
)
else:
raise NotImplementedError("Algorithm not supported")
segments.append(base64url_encode(signature))
encoded_string = b".".join(segments)
return encoded_string.decode("utf-8")
我们可以看到该函数给header赋了默认值(内部"JWT"和"HS256"
),然后判断了一下我们的headers是否存在,如果存在就用我们的headers,如果不存在就用默认的,所以对于haders我们可传可不传,之后又继续替换操作然后将值进行头部headers(即第一段字符串加密操作
)的base64url_encode加密,并存放到segments列表中(一开始为空列表
),然后继续将payload(即第二段字符串加密操作
)做同样的base64url_encode加密,将这两个加密的数据放入一个列表中。
之后用.进行拼接并返回给signing_input,然后到try中寻找该参数传入的加密算法,将key提高prepare_key函数获取到(key为服务端的私钥,内部就是进行key是否存在的判断
)。
def prepare_key(self, key):
if key == "":
key = None
if key is not None:
raise InvalidKeyError('When alg = "none", key value must be None.')
return key
之后通过加密对象(默认情况下为hs256对象
)调用sign函数。
HMACAlgorithm类/sign函数如下:
import hmac
class HMACAlgorithm(Algorithm):
"""
Performs signing and verification operations using HMAC
and the specified hash function.
"""
SHA256 = hashlib.sha256
SHA384 = hashlib.sha384
SHA512 = hashlib.sha512
def __init__(self, hash_alg):
self.hash_alg = hash_alg
def sign(self, msg, key):
return hmac.new(key, msg, self.hash_alg).digest()
抛开其他函数,我们看sign函数已经不能往下走了,其内部通过hmac库中的算法创建一个sha256加密结果并返回(此时第三段字符已经完成sha256加密
)赋值给了signature ,然后再进行一次base64url加密放入segments列表中最后将这3个加密字符串通过.的形式拼接起来并返回。
可以看出通过源码的分析发现jwt中encode加密函数和我们上述描写的思路是一致的。
那么解密decode的源码如下:
def decode(
self,
jwt: str,
key: str = "",
algorithms: List[str] = None,
options: Dict = None,
**kwargs,
) -> str:
decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs)
return decoded["payload"]
可以看到decode函数调用了decode_complete函数,我们点进去查看:
def decode_complete(
self,
jwt: str,
key: str = "",
algorithms: List[str] = None,
options: Dict = None,
**kwargs,
) -> Dict[str, Any]:
if options is None:
options = {}
merged_options = {**self.options, **options}
verify_signature = merged_options["verify_signature"]
if verify_signature and not algorithms:
raise DecodeError(
'It is required that you pass in a value for the "algorithms" argument when calling decode().'
)
payload, signing_input, header, signature = self._load(jwt)
if verify_signature:
self._verify_signature(signing_input, header, signature, key, algorithms)
return {
"payload": payload,
"header": header,
"signature": signature,
}
前面参数不看,我们直接看decode_complete函数调用了_load函数,还有一个jwt参数(即用户token
),此时我们点进去查看:
def _load(self, jwt):
if isinstance(jwt, str):
jwt = jwt.encode("utf-8")
if not isinstance(jwt, bytes):
raise DecodeError(f"Invalid token type. Token must be a {bytes}")
try:
signing_input, crypto_segment = jwt.rsplit(b".", 1)
header_segment, payload_segment = signing_input.split(b".", 1)
except ValueError as err:
raise DecodeError("Not enough segments") from err
try:
header_data = base64url_decode(header_segment)
except (TypeError, binascii.Error) as err:
raise DecodeError("Invalid header padding") from err
try:
header = json.loads(header_data)
except ValueError as e:
raise DecodeError("Invalid header string: %s" % e) from e
if not isinstance(header, Mapping):
raise DecodeError("Invalid header string: must be a json object")
try:
payload = base64url_decode(payload_segment)
except (TypeError, binascii.Error) as err:
raise DecodeError("Invalid payload padding") from err
try:
signature = base64url_decode(crypto_segment)
except (TypeError, binascii.Error) as err:
raise DecodeError("Invalid crypto padding") from err
return (payload, signing_input, header, signature)
类型判断我们直接跳过,看try中该函数通过rsplit函数将数据通过.的形式分割成两段(第一、二段加密字符串为一段,第三段加密字符串为第二段
),然后将signing_input(一、二段加密字符串
)再分割,使其将3段加密字符串放到不同的变量中,然后依次解密,最后返回解密的结果。
返回后,继续往下走调用_verify_signature函数,其需要参数为base64url解密完成后和加密的方式,函数源码如下:
def _verify_signature(
self,
signing_input,
header,
signature,
key="",
algorithms=None,
):
alg = header.get("alg")
if algorithms is not None and alg not in algorithms:
raise InvalidAlgorithmError("The specified alg value is not allowed")
try:
alg_obj = self._algorithms[alg]
key = alg_obj.prepare_key(key)
if not alg_obj.verify(signing_input, key, signature):
raise InvalidSignatureError("Signature verification failed")
except KeyError:
raise InvalidAlgorithmError("Algorithm not supported")
该函数通过alg获取到加密方式(默认hs256
),然后通过_algorithms函数获取加密对象(默认hs256对象
),再获取服务器唯一密钥后,通过加密对象.verify
方法并把参数全部传入进行验证(判断token是否和当前用户传的一致
)。
HMACAlgorithm类/verify函数如下:
import hmac
class HMACAlgorithm(Algorithm):
"""
Performs signing and verification operations using HMAC
and the specified hash function.
"""
SHA256 = hashlib.sha256
SHA384 = hashlib.sha384
SHA512 = hashlib.sha512
def __init__(self, hash_alg):
self.hash_alg = hash_alg
def verify(self, msg, key, sig):
return hmac.compare_digest(sig, self.sign(msg, key))
抛开其他函数我们只看verify函数,走到这里就不走了,此时verify函数会调用hmac算法进行比较,最后将结果返回。
借鉴hamc中比较的思路我们可以发现通过jwt中decode函数源码分析,也和我们上述描写的概念是一样的。