JWT的介绍和使用
JWT的含义
Json web token(JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(sso)场景,JWT的声明一般被用来在身份提供者和服务者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其他业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
token的应用于web方向的称之为jwt
JWT的构成
JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。
header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
"typ": "JWT",
"alg": "HS256"
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload
荷载就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, "secret"); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
JWT原理和校验过程
原理
1)jwt分三段式:头.体.签名 (head.payload.sgin)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间
{
"user_id": 1,
...
}
6)签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密
{
"head": "头的加密字符串",
"payload": "体的加密字符串",
"secret_key": "安全码"
}
校验过程
1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的
3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户
drf项目的jwt认证开发流程
使用的是django_rest_framework里面的jwt模块
1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中
2)校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户
注:登录接口需要做 认证 + 权限 两个局部禁用
drf-jwt安装与使用
安装
pip3 install djangorestframework-jwt
使用
1)迁移表,表需要继承auth里内置的user表,因为它默认使用auth的user表签发token
2)创建超级用户(auth的user表中要右记录)
3)不需要写登录接口了,如果是使用auth的user表作为用户表,它可以快速签发
4)签发(登录):只需要在路由中配置(因为它帮咱们写好登录接口了)
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path("login/", obtain_jwt_token),
]
# 返回结果
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Inp4ciIsImV4cCI6MTY2NTU3MjU4OSwiZW1haWwiOiJ6eHJAMTYzLmNvbSJ9.0jDd56Jk04-SdZ4AchLHkcfLECS1RvhFwAQ8VoNKiMM"
}
当登录127.0.0.1:8000/login/时采用post请求,然后携带登录的用户名和密码的信息,服务器会自动响应,返回一个token给前端,效果图如下:
内置认证类的使用
在需要添加认证的视图函数接口上添加以下配置:
from rest_framework.views import APIView
from rest_framework.response import Response
# Create your views here.
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class BookView(APIView):
authentication_classes = [JSONWebTokenAuthentication,]
permission_classes = [IsAuthenticated, ]
def get(self,request):
return Response("ok")
def post(self,request):
return Response("ok11")
添加之后,在访问该视图时,token需要放在请求头中,并且格式为Authorization:jwt token串
Authorization:jwt token串
# 例如:
Authorization:jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Inp4ciIsImV4cCI6MTY2NTU3MzMzMCwiZW1haWwiOiJ6eHJAMTYzLmNvbSJ9.DAX1wLhHYnpGwVzGvMnSFzUudkPgXsYvlrfk-XFdSAc
注意:上述请求头的key必须位Authorization,值为jwt+空格+token串,如果想要修改jwt可以在配置文件中进行如下配置:
自定义auth认证类的使用
在一个py文件夹里定义如下一个认证类
from rest_framework_jwt.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.authentication import jwt_decode_handler
from app01.models import UserInfo
# 写一个类继承自BaseAuthentication,重写authenticate方法
class JWTAuthentication(BaseAuthentication):
def authenticate(self, request):
# token在请求头的值为token-->HTTP_TOKEN token在请求头的值为Authorization时--->HTTP_AUTHORIZATION
# print(request.META)
jwt_value = request.META.get("HTTP_TOKEN")
# 验证token是否合法,jwt模块下一定有个验证token的函数
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed("token过期了")
except jwt.DecodeError:
raise AuthenticationFailed("token解码失败")
except jwt.InvalidTokenError:
raise AuthenticationFailed("认证失败")
# 执行到这,说明token合法,payload可以使用
user = payload.get("username")
user = UserInfo.objects.filter(username=user).first() # 每次都要查数据库,效率不太好
return (user, jwt_value) #user对应了request.user jwt_value对应了request.auth
# return (payload,jwt_value)
在需要认证的视图上进行局部配置自己的写的认证类
class BookView(APIView):
authentication_classes = [JWTAuthentication,]
permission_classes = [IsAuthenticated, ]
def post(self,request):
print(request.user,request.auth)
return Response("ok11")
drf-jwt修改返回格式
登录成功后,前端看到的格式,太固定了,只有token,我们想做成:
{code:100,msg:"登录成功",token:adfasdfasdf}
固定写法:
写一个函数,函数返回什么,前端就看到什么,然后需要在配置文件中配置一下自己写的这个函数
写一个函数
def jwt_response_payload_handler(token, user=None, request=None):
return {
"code": 100,
"msg": "登录成功",
"username": user.username,
"token": token
}
将函数配置在配置文件中
# djangorestframework-jwt的配置,这个配置了以后优先使用这个
JWT_AUTH = {
"JWT_RESPONSE_PAYLOAD_HANDLER": "app01.response.jwt_response_payload_handler",
}
手动签发token(多方式登录)
使用用户名,手机号,邮箱,都可以登录
前端需要传的数据格式
{
"username":"lqz/1332323223/33@qq.com",
"password":"lqz12345"
}
视图
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin, ViewSet
from app02 import ser
class Login2View(ViewSet):
def login(self, request, *args, **kwargs):
# 1 需要 有个序列化的类
login_ser = ser.LoginModelSerializer(data=request.data,context={"request":request})
# 2 生成序列化类对象
# 3 调用序列号对象的is_validad
login_ser.is_valid(raise_exception=True)
token=login_ser.context.get("token")
# 4 return
return Response({"status":100,"msg":"登录成功","token":token,"username":login_ser.context.get("username")})
序列化类
from rest_framework import serializers
from api import models
import re
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler,jwt_payload_handler
class LoginModelSerializer(serializers.ModelSerializer):
username=serializers.CharField() # 重新覆盖username字段,数据中它是unique,post,认为你保存数据,自己有校验没过
class Meta:
model=models.User
fields=["username","password"]
def validate(self, attrs):
print(self.context)
# 在这写逻辑
username=attrs.get("username") # 用户名有三种方式
password=attrs.get("password")
# 通过判断,username数据不同,查询字段不一样
# 正则匹配,如果是手机号
if re.match("^1[3-9][0-9]{9}$",username):
user=models.User.objects.filter(mobile=username).first()
elif re.match("^.+@.+$",username):# 邮箱
user=models.User.objects.filter(email=username).first()
else:
user=models.User.objects.filter(username=username).first()
if user: # 存在用户
# 校验密码,因为是密文,要用check_password
if user.check_password(password):
# 签发token
payload = jwt_payload_handler(user) # 把user传入,得到payload
token = jwt_encode_handler(payload) # 把payload传入,得到token
self.context["token"]=token
self.context["username"]=user.username
return attrs
else:
raise ValidationError("密码错误")
else:
raise ValidationError("用户不存在")
配置签发过期时间
# jwt的配置
import datetime
JWT_AUTH={
"JWT_RESPONSE_PAYLOAD_HANDLER":"app02.utils.my_jwt_response_payload_handler",
"JWT_EXPIRATION_DELTA": datetime.timedelta(days=7), # 过期时间,手动配置
}