前因
项目通过JWT 来实现用户的验证,在注销和异设备登入或密码修改的时候都需要让旧的JWT 失效,但是 DRF JWT 没有内置失效方法,官方推荐通过设置“JWT_GET_USER_SECRET_KEY” 为一个使每次SECRET_KEY 不相同的方法,从而使每次生成的Token 都不一样。
后果
具体方式如下:
1.首先修改用户模型类users.models.py 添加user_secret 字段,如下:
1 from django.db import models 2 from django.contrib.auth.models import AbstractUser 3 from uuid import uuid4 4 5 class User(AbstractUser): 6 """用户模型类""" 7 user_secret = models.UUIDField(default=uuid4(), verbose_name='用户JWT秘钥') 8 9 class Meta: 10 db_table = 'tb_users' 11 verbose_name = '用户' 12 verbose_name_plural = verbose_name
2.并在项目的settings 中指定使用该模型类,如下:
1 # Custom Model 2 AUTH_USER_MODEL = 'users.User'
3.终端执行迁移命令
python manage.py makemigrations
python manage.py migrate
4.在utils.users.py 中定义获取user_secret 的方法,如:
1 def jwt_get_user_secret(user): 2 3 return user.user_secret
5.在项目的settings 的JWT_AUTH 里添加一个属性 'JWT_GET_USER_SECRET_KEY'
1 # JWT_AUTH settings 2 JWT_AUTH = { 3 # JWT expiration time one day 4 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 5 # Custom Return 6 'JWT_RESPONSE_PAYLOAD_HANDLER': 'utils.users.jwt_response_payload_handler', 7 # Custom Get User SECRET 8 'JWT_GET_USER_SECRET_KEY': 'utils.users.jwt_get_user_secret' 9 }
构思
保证一个用户登录的的业务逻辑,就是每次登录的时候都会对token 进行校验,通过就给该用户一个user_jwt 的属性并且在每个请求的时候都去判断请求是否携带合法token ,且该token是否和user.user_jwt 相等,如果不相等,说明有异设备登录,更改了user_jwt,此时根据需求,需要两个用户都重新登录,则重新生成user_secret,让之前的JWT 都失效,从而保证用户只有一个人在线上。同理用户注销或者修改密码的时候,也重新生成一个新的user_secret,这样就能保证旧的JWT 在这三种情况下失效。
6.使用中间件来实现,在项目的settings 里“MIDDLEWARE” 添加一个中间件类,用于每次请求和登录请求的逻辑扩展,如:
1 # MIDDLEWARE_CALSSES = [ # Django 1.4.x ---- 1.9.x 2 MIDDLEWARE = [ # Django 1.11.11 3 ..., 4 'utils.check_token_middleware.CheckTokenMiddleware', 5 6 ]
7.1 utils.check_token_middleware.py (Django 1.4.x ---- Django 1.9.x)
1 from uuid import uuid4 2 from django.http import HttpResponse 3 from django.utils.deprecation import MiddlewareMixin 4 from jwt import InvalidSignatureError 5 from rest_framework.exceptions import ValidationError 6 from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer 7 8 class CheckTokenMiddleware(MiddlewareMixin): 9 """ 10 Django 1.4.x ---- Django 1.9.x 11 每次请求时 判断 JWT 是否与 User.user_jwt 相等 12 相等的话,说明没有以设备登录,且没有修改密码 13 不相等,则说明异常设备登录,或修改了密码,修改用户的uuid并提示用户重新登录 14 每次登录时记录更新JWT 为User 的一个属性user_jwt 15 每次修改密码时 更新修改uuid 16 """ 17 def process_request(self, request): 18 # 处理所有带JWT 的请求 19 jwt_token = request.META.get('Authorization', None) 20 if jwt_token is not None and jwt_token != '': 21 data = { 22 'token': jwt_token.split(' ')[1], # [0] 是前缀,默认为JWT 23 } 24 try: 25 valid_data = VerifyJSONWebTokenSerializer().validate(data) 26 user = valid_data['user'] 27 except (InvalidSignatureError, ValidationError): 28 # 找不到用户,说明token 不合法或者身份过期 29 return HttpResponse({'msg': '身份已经过期,请重新登入'}, content_type='application/json', status=400) 30 else: 31 # 说明进行了第二次登录, user.user_jwt 已经被重新赋值,需要更换签名。注意,此种方法将使无论是第一次登录还是第二次登录的人的 验证信息都失效,从而保证只有一个人在线上 32 if user.user_jwt != data['token']: 33 user.user_secret = uuid4() 34 user.save() 35 return HttpResponse({'msg': '异设备登录,请重新登入或修改密码'}, content_type='application/json', status=400) 36 return None 37 38 def process_response(self, request, response): 39 # 处理login 请求 40 if request.META['PATH_INFO'] == '/users/auths/': 41 # 因为登录认证ObtainJSONWebToken 继承自JSONWebTokenAPIView,所以是Response对象,不是HttpResponse对象,所以使用response.data,而不是response.content 42 rep_data = response.data 43 # 默认response.data 里面必有token ,根据序列化器VerifyJSONWebTokenSerializer()返回token和user 44 valid_data = VerifyJSONWebTokenSerializer().validate(rep_data) 45 user = valid_data['user'] 46 user.user_jwt = rep_data['token'] 47 user.save() 48 return response
7.2 utils.check_token_middleware.py (Django 1.11.11)
1 from uuid import uuid4 2 from django.http import HttpResponse 3 from jwt import InvalidSignatureError 4 from rest_framework.exceptions import ValidationError 5 from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer 6 7 class CheckTokenMiddleware(object): 8 """ 9 Django 1.11.11 10 每次请求时 判断 JWT 是否与 User.user_jwt 相等 11 相等的话,说明没有以设备登录,且没有修改密码 12 不相等,则说明异常设备登录,或修改了密码,修改用户的uuid并提示用户重新登录 13 14 每次登录时记录更新JWT 为User 的一个属性user_jwt 15 每次修改密码时 更新修改uuid 并记录更新JWT 为User 的一个属性user_jwt 16 """ 17 def __init__(self, get_response): 18 # 第一次请求初始化和配置 19 self.get_response = get_response 20 21 def __call__(self, request): 22 # 请求前被调用 23 # 处理所有带JWT 的请求 24 jwt_token = request.META.get('Authorization', None) 25 if jwt_token is not None and jwt_token != '': 26 data = { 27 'token': jwt_token.split(' ')[1], # [0] 是前缀,默认为JWT 28 } 29 try: 30 valid_data = VerifyJSONWebTokenSerializer().validate(data) 31 user = valid_data['user'] 32 except (InvalidSignatureError, ValidationError): 33 # 找不到用户,说明token 不合法或者身份过期 34 return HttpResponse({'msg': '身份已经过期,请重新登入'}, content_type='application/json', status=400) 35 else: 36 # 说明进行了第二次登录, user.user_jwt 已经被重新赋值,需要更换签名 37 if user.user_jwt != data['token']: 38 user.user_secret = uuid4() 39 user.save() 40 return HttpResponse({'msg': '异设备登录,请重新登入或修改密码'}, content_type='application/json', status=400) 41 42 response = self.get_response(request) 43 # 请求后被调用 44 # 处理login 请求 45 if request.META['PATH_INFO'] == '/users/auths/': 46 # 因为登录认证ObtainJSONWebToken 继承自JSONWebTokenAPIView,所以是Response对象,不是HttpResponse对象 47 # 所以使用response.data,而不是response.content 48 rep_data = response.data 49 # 默认response.data 里面必有token ,根据序列化器VerifyJSONWebTokenSerializer()返回token和user 50 valid_data = VerifyJSONWebTokenSerializer().validate(rep_data) 51 user = valid_data['user'] 52 user.user_jwt = rep_data['token'] 53 user.save() 54 return response
8.在注销用户和修改密码的业务逻辑后面添加:
# 注销用户
user = request.user
user.user_secret = uuid4()
user.save()
# 修改密码
user.user_secret = uuid4()
user.save()
9.测试
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Django REST framework学习之JWT失效方式 - Python技术站