djangorestframework-jwt自带的认证视图进行用户登录验证源代码学习
Django REST framework JWT
djangorestframework-jwt自带的认证视图进行用户登录验证源代码学习
SECRET_KEY = '1)q(f8jrz^edwtr2#h8vj=$u)ip4fx7#h@c41gvxtgc!dj#wkc'
定期动态生成SECRET_KEY
字符串导包 https://blog.csdn.net/chaoguo1234/article/details/81277590
安装配置
安装
pip install djangorestframework-jwt
配置
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), } JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), }
Django REST framework JWT 扩展的说明文档中提供了手动签发JWT的方法
from rest_framework_jwt.settings import api_settings jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) token = jwt_encode_handler(payload)
从api_settigs下去找,在rest_framework_jwt.settings下面
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) # 这三个参数分别对应settings文件下的参数
DEFAULTS 这个参数
DEFAULTS = { ... 'JWT_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_payload_handler', ... }
从源码可以看出对应的就是
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 中的 JWT_PAYLOAD_HANDLER ,key对应的value就是 'rest_framework_jwt.utils.jwt_payload_handler'
而rest_framework_jwt.utils.jwt_payload_handler其实就是一个导包路径
现在从这个路径下去寻找到utils下的jwt_payload_handler函数
def jwt_payload_handler(user): username_field = get_username_field() username = get_username(user) warnings.warn( 'The following fields will be removed in the future: ' '`email` and `user_id`. ', DeprecationWarning ) payload = { 'user_id': user.pk, 'username': username, 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA # JWT_EXPIRATION_DELTA对应的就是在我们配置里指定的过期时间 } if hasattr(user, 'email'): payload['email'] = user.email if isinstance(user.pk, uuid.UUID): payload['user_id'] = str(user.pk) payload[username_field] = username # Include original issued at time for a brand new token, # to allow token refresh if api_settings.JWT_ALLOW_REFRESH: payload['orig_iat'] = timegm( datetime.utcnow().utctimetuple() ) if api_settings.JWT_AUDIENCE is not None: payload['aud'] = api_settings.JWT_AUDIENCE if api_settings.JWT_ISSUER is not None: payload['iss'] = api_settings.JWT_ISSUER return payload
下面在 jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 用同样的方法找到JWT_ENCODE_HANDLER对应的value, 也就是导包路径
DEFAULTS = { ... 'JWT_ENCODE_HANDLER': 'rest_framework_jwt.utils.jwt_encode_handler', ... }
同样根据导包路径寻找
def jwt_encode_handler(payload): key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload) return jwt.encode( payload, key, api_settings.JWT_ALGORITHM ).decode('utf-8')
生成token的过程
浏览器的保存策略
Django REST framework JWT提供了登录签发JWT的视图,可以直接使用
from rest_framework_jwt.views import obtain_jwt_token urlpatterns = [ url(r'^authorizations/$', obtain_jwt_token), ]
但是默认的返回值仅有token,我们还需在返回值中增加username和user_id。
从 obtain_jwt_token 进去
路由: url(r'^authorizations/, obtain_jwt_token),
obtain_jwt_token来自$PYTHON_ENVTIONS_PATH/site-packages/rest_framework_jwt/views.py
的102行和74-80行,代码如下
class ObtainJSONWebToken(JSONWebTokenAPIView): """ API View that receives a POST with a user's username and password. Returns a JSON Web Token that can be used for authenticated requests. """ serializer_class = JSONWebTokenSerializer """ 中间省略部分不相关代码 """ obtain_jwt_token = ObtainJSONWebToken.as_view()
很明显:这个就是一个登录的视图集
查看下继承的JSONWebTokenAPIView视图
jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER
...
class JSONWebTokenAPIView(APIView): # 继承至APIView ... def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): user = serializer.object.get('user') or request.user token = serializer.object.get('token') response_data = jwt_response_payload_handler(token, user, request) # jwt_response_payload_handler 响应对象 response = Response(response_data) if api_settings.JWT_AUTH_COOKIE: expiration = (datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA) response.set_cookie(api_settings.JWT_AUTH_COOKIE, token, expires=expiration, httponly=True) return response return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
在jwt_response_payload_handler 响应对象中找到
def jwt_response_payload_handler(token, user=None, request=None): """ Returns the response data for both the login and refresh views. Override to return a custom response such as including the serialized representation of the User. Example: def jwt_response_payload_handler(token, user=None, request=None): return { 'token': token, 'user': UserSerializer(user, context={'request': request}).data } """ return { 'token': token }
可以看出,登录后返回的响应对象仅仅有token一个key , 这对于大多数场景来说都是不合适的,所以需要来重写该方法
def jwt_response_payload_handler(token, user=None, request=None): """ 自定义jwt认证成功返回数据 """ return { 'token': token, 'user_id': user.id, 'username': user.username }
因为我们自定义的该方法,所以也需要修改它的导包路径,之前也找到了它的导包路径传入的源码,则在配置文件中进行如下配置:
# JWT JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler', }
这样,就实现了修改response响应对象
现在看完了继承的类视图,下面来看下序列化器:
在刚刚的源码中能看得到指定的序列化器就是 serializer_class = JSONWebTokenSerializer
既然指定了serializer_class = JSONWebTokenSerializer
说明是使用了DRF框架做验证, 那么验证用户登录时传输的参数的代码就是在序列化器类的代码中
序列化器类来自于$PYTHON_ENVTIONS_PATH/site-packages/rest_framework_jwt/serializers.py
22-69行, 代码如下:
class JSONWebTokenSerializer(Serializer): """ 省略部分代码 """ def validate(self, attrs): # 获取参数: 用户登录名称 + 密码 credentials = { self.username_field: attrs.get(self.username_field), 'password': attrs.get('password') } if all(credentials.values()): # 用户登录时传入的参数完整, 则验证用户并获取用户对象 # 获取用户对象的代码在下面👇这行代码中!!! user = authenticate(**credentials) if user: if not user.is_active: msg = _('User account is disabled.') raise serializers.ValidationError(msg) payload = jwt_payload_handler(user) return { 'token': jwt_encode_handler(payload), 'user': user } else: msg = _('Unable to log in with provided credentials.') raise serializers.ValidationError(msg) else: msg = _('Must include "{username_field}" and "password".') msg = msg.format(username_field=self.username_field) raise serializers.ValidationError(msg)
获取用户对象的关键代码在第50行 user = authenticate(**credentials)
; 而authenticate
到包自$PYTHON_ENVTIONS_PATH/site-packages/django/contrib/auth/init.py`的64行至81行, 代码如下:
def authenticate(request=None, **credentials): """ If the given credentials are valid, return a User object. """ # 获取验证后端的backend对象的关键代码在下面👇这行!!! for backend, backend_path in _get_backends(return_tuples=True): try: user = _authenticate_with_backend(backend, backend_path, request, credentials) except PermissionDenied: # This backend says to stop in our tracks - this user should not be allowed in at all. break if user is None: continue # Annotate the user object with the path of the backend. user.backend = backend_path return user # The credentials supplied are invalid to all backends, fire signal user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
这段代码的核心就是
user = _authenticate_with_backend(backend, backend_path, request, credentials) # 而这句代码的核心就是_authenticate_with_backend 这个名义上的私有方法
往下找
就在上面这个方法的下面
def _authenticate_with_backend(backend, backend_path, request, credentials): credentials = credentials.copy() # Prevent a mutation from propagating. args = (request,) # Does the backend accept a request argument? try: inspect.getcallargs(backend.authenticate, request, **credentials) # 很明显backend.authenticate 中的 authenticate 就是核心逻辑
except TypeError: args = () credentials.pop('request', None) # Does the backend accept a request keyword argument? try: inspect.getcallargs(backend.authenticate, request=request, **credentials) except TypeError: # Does the backend accept credentials without request? try: inspect.getcallargs(backend.authenticate, **credentials) except TypeError: # This backend doesn't accept these credentials as arguments. Try the next one. return None else: warnings.warn( "Update %s.authenticate() to accept a positional " "`request` argument." % backend_path, RemovedInDjango21Warning ) else: credentials['request'] = request warnings.warn( "In %s.authenticate(), move the `request` keyword argument " "to the first positional argument." % backend_path, RemovedInDjango21Warning ) return backend.authenticate(*args, **credentials)
点进去
class ModelBackend(object): """ Authenticates against settings.AUTH_USER_MODEL. """ def authenticate(self, request, username=None, password=None, **kwargs): if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) try: user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: # Run the default password hasher once to reduce the timing # difference between an existing and a non-existing user (#20760). UserModel().set_password(password) else: if user.check_password(password) and self.user_can_authenticate(user): return user
而上面这段代码中 user = UserModel._default_manager.get_by_natural_key(username) 这句是核心代码 不往下追了
可以理解为 User.objects.get(username=username)
就是 user = UserModel._default_manager.get_by_natural_key(username) 这里写死了只用username 去查询User模型内的user对象是否存在,实际上jwt用的也是django的登录认证方法
而我们要实现多账号登录,则要重写ModelBackend这个类
def get_user_by_account(account): """多账号登录的实现(手机号&用户名)""" try: if re.match(r'^1[3-9]\d{9}$', account): user = User.objects.get(mobile=account) else: user = User.objects.get(username=account) except User.DoesNotExist: return None else: return user class UsernameMobileAuthBackend(ModelBackend): """重写自定义django认证后端""" def authenticate(self, request, username=None, password=None, **kwargs): """ 重写认证方式,使用多账号登录 :param request: 本次登录请求对象 :param username: 用户名/手机号 :param password: 密码 :return: 返回值user/None """ # 1.通过传入的username 获取到user对象(通过手机号或用户名动态查询user) user = get_user_by_account(username) # 2.判断user的密码 if user and user.check_password(password): return user else: return None
那么方法就重写完了,下面就是要让inspect.getcallargs(backend.authenticate, request, **credentials) 中的authenticate方法 去找到我们重写的类方法
而我们之前在配置文件中获知的配置方法
# 修改Django的默认的认证后端类 AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', # 修改django认证后端类 ]
可以从前面的这个代码中提取_get_backends 方法
获取用户对象的关键代码在第50行 user = authenticate(**credentials)
; 而authenticate
到包自$PYTHON_ENVTIONS_PATH/site-packages/django/contrib/auth/init.py`的64行至81行, 代码如下:
def authenticate(request=None, **credentials): """ If the given credentials are valid, return a User object. """ # 获取验证后端的backend对象的关键代码在下面👇这行!!! for backend, backend_path in _get_backends(return_tuples=True): try: user = _authenticate_with_backend(backend, backend_path, request, credentials) except PermissionDenied: # This backend says to stop in our tracks - this user should not be allowed in at all. break if user is None: continue # Annotate the user object with the path of the backend. user.backend = backend_path return user # The credentials supplied are invalid to all backends, fire signal user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
_get_backends 方法:
获取验证后端的backend对象的关键代码在第68行for backend, backend_path in _get_backends(return_tuples=True):
;而_get_backends
对象来当前代码文件的26-36行,代码如下:
def _get_backends(return_tuples=False): backends = [] # 关键代码在下面👇这行!!!! for backend_path in settings.AUTHENTICATION_BACKENDS: backend = load_backend(backend_path) backends.append((backend, backend_path) if return_tuples else backend) if not backends: raise ImproperlyConfigured( 'No authentication backends have been defined. Does ' 'AUTHENTICATION_BACKENDS contain anything?' ) return backends
关键代码在第28行: for backend_path in settings.AUTHENTICATION_BACKENDS
, 而settings
导包自from django.conf import settings
, 那么这里的settings等同于我们项目启动时使用的meiduo_mall.settings.dev
而我们在dev.py中添加了配置代码如下:
# 告知Django使用自定义的认证后端 AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', ]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】