前因

项目通过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.测试