前因
項目通過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.測試
