一、JWT 简介
jwt(JSON Web Tokens),是一种开发的行业标准 RFC 7519
,用于安全的表示双方之间的声明。目前,jwt广泛应用在系统的用户认证方面,特别是现在前后端分离项目。
1.1 Jwt认证流程
- 前端用户填写好用户名和密码,点击登录
- 后端对提交的用户名和密码进行校验,校验通过则发送token给前端
- 前端将token保存在cookie中,并在每一次请求时都将cookie一并发送给后端
- 后端对用户发送过来的token进行校验,并通过token识别是哪个用户。
1.2 session & Token 区别
-
session
在django中,如果使用session进行认证,会在django_session表中存储用户登录记录,随着用户增加,数据库中的记录也会越来越多,增加了服务器压力
-
token
传统token
用户登录成功后,服务端生成一个随机token给用户,并且在服务端(数据库或缓存)中保存一份token,以后用户再来访问时需携带
token
,服务端接收到token之后,去数据库或缓存中进行校验token的是否超时、是否合法。jwt 形式
用户登录成功后,服务端通过jwt生成一个随机token给用户(服务端无需保留token),以后用户再来访问时需携带token,服务端 接收到token之后,通过jwt对token进行校验是否超时、是否合法。
1.3 JWT 格式
jwt
是一段由.(点)
组成的三段式密文
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
-
第一段称为头部(header)
头部存储了两部分信息,分别是类别和加密算法。加密算法通常使用sha256(这里指整体加密时采用的算法),将头部进行base64url编码得到一段内容
# 密文 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 # 解密 { "alg": "HS256", "typ": "JWT" }
-
第二段称为payload(载荷)
payload 里面包含用户有部分数据,比如用户id和用户名等。第二段内容也是通过base64url进行加密,所以内容中不能包含敏感数据
# 密文 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ # 解密 { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
-
第三段为签名(signature)
把前两段的
base64url
密文通过.
拼接起来,并加入秘钥,然后对其(两段密文和盐)进行HS256
加密(header中定义的类别),再然后对整体密文进行base64url
加密,最终得到token的第三段。base64url( HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret (秘钥加盐) ) )
二、PyJWT 使用
2.1 简单实用
-
安装
pip install pyjwt
在django系统中使用pyjwt来实现jwt认证。
>>> import jwt
>>> key = "secret"
>>> encoded = jwt.encode({"some": "payload"}, key, algorithm="HS256")
>>> print(encoded)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg
>>> jwt.decode(encoded, key, algorithms="HS256")
{'some': 'payload'}
- payload中的特殊参数
The JWT specification defines some registered claim names and defines how they should be used. PyJWT supports these registered claim names:
“exp” (Expiration Time) Claim
“nbf” (Not Before Time) Claim
“iss” (Issuer) Claim
“aud” (Audience) Claim
“iat” (Issued At) Claim
-
exp:指定过期时间
payload = { "id":result.id, "username":result.username, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=1) # 超时时间,其中exp是固定写法 }
在进行decode时,会对该值进行校验token是否过期
2.2 JWT实现用户token认证案例
以django为例
路由接口
urlpatterns = [
path('login',views.LoginView.as_view()), # 生成token,并返回给用户
path('order',views.OrderView.as_view()) # 验证token
]
视图书写
# 登录视图类
class LoginView(APIView):
def post(self,request):
username = request.POST.get('username')
password = request.POST.get('password')
user_obj = models.UserInfo.objects.filter(username=username,password=password).first()
if not user_obj:
return JsonResponse({"code":"201","msg":"用户名或密码错误"})
payload = {"uid":user_obj.id,"username":user_obj.username}
token = jwt_auth.creata_token(payload)
return JsonResponse({"code":"200","msg":"post successful","token":token,})
# 订单类
class OrderView(APIView):
def get(self,request):
token = request.query_params.get("token")
result = jwt_auth.verify_token(token)
return JsonResponse(result)
token创建&认证函数
# utils/jwt_auth
import jwt
import datetime
from django.conf import settings
def creata_token(payload,):
"""
生成token
:param payload: 用于生成token的部分用户信息
:return: 生成的token
"""
# 1.构造headers
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
# 2.构造payload
payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=1) # 指定过期时间为1分钟
# 3.生成token并返回
token = jwt.encode(payload=payload, key=settings.SECRET_KEY, algorithm="HS256")
return token
def verify_token(token):
"""
验证token的有效性
:param token:
:return:
"""
result = {"code": "202"}
try:
# true 表示集成了对时间等校验
verified_payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
print(verified_payload)
result['msg'] = verified_payload
result['code'] = 200
except jwt.exceptions.ExpiredSignatureError:
result['error'] = "身份信息已失效,请重新登录"
except jwt.DecodeError:
result['error'] = "Token认证失败"
except jwt.InvalidTokenError:
result['error'] = "非法token"
return result
实验测试结果
- 生成token
-
有效期token结果
-
过期token
三、利用DRF设置用户认证
自定义认证类
# utils/jwt_auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
class JwtGlobalAuth(BaseAuthentication):
def authenticate(self, request):
token = request.query_params.get("token")
result = {"code": "202"}
try:
# true 表示集成了对时间等校验
verified_payload = jwt.decode(token, settings.SECRET_KEY, algorithms="HS256")
result['msg'] = verified_payload
result['code'] = 200
except jwt.exceptions.ExpiredSignatureError:
result['error'] = "身份信息已失效,请重新登录"
raise AuthenticationFailed(result)
except jwt.DecodeError:
result['error'] = "Token认证失败"
raise AuthenticationFailed(result)
except jwt.InvalidTokenError:
result['error'] = "非法token"
raise AuthenticationFailed(result)
# drf 认证类可以返回三种类型值
# 1.抛出异常
# 2.返回一个元组
return (verified_payload,token)
# 3.返回None
视图中加载用户认证 - 局部认证
# api/views.py
class OrderView(APIView):
authentication_classes = [jwt_auth.JwtGlobalAuth,]
def get(self,request):
ret = "这里是订单详情"
return JsonResponse(ret,safe=False)
全局token认证
# settings.py
import api
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES":['api.utils.jwt_auth.JwtGlobalAuth'] # 默认认证类,全局有效
}
-
视图类
# api/views.py class LoginView(APIView): authentication_classes = [] # 解除登录时的token认证 def post(self,request): username = request.POST.get('username') password = request.POST.get('password') user_obj = models.UserInfo.objects.filter(username=username,password=password).first() if not user_obj: return JsonResponse({"code":"201","msg":"用户名或密码错误"}) payload = {"uid":user_obj.id,"username":user_obj.username} token = jwt_auth.creata_token(payload) return JsonResponse({"code":"200","msg":"post successful","token":token,}) class OrderView(APIView): def get(self,request): ret = "这里是订单详情" return JsonResponse(ret,safe=False)