# JWT多方式登录及自定义验证

{% hint style="info" %}
RBAC(基于用户权限访问控制的认证) 　　(自己了解：基于auth的认证规则) 　　\
Django框架采用的是RBAC认证规则，\
RBAC认证规则通常会分为 三表规则、五表规则，\
Django采用的是六表规则 　　\
三表：用户表、角色表、权限表 　　\
五表：用户表、角色表、权限表、用户角色关系表、角色权限关系表 　　\
六表：用户表、角色表、权限表、用户角色关系表、角色权限关系表、用户权限关系表
{% endhint %}

## JWT 多方面登录

#### 用户可以以用户名、邮箱、手机号登录

* 手机号+密码
* 用户名+密码
* 邮箱名+密码

#### 流程分析(post请求):

* 路由 -- 自动生成
* 视图类: -- ViewSet（ViewSetMixin, views.APIView）
* 序列化类: -- 重写validate方法，在这里面对用户名和密码进行校验

> 之前写的逻辑处理一般都是在视图类中，现在把逻辑拿到了序列化类的`validate全局钩子`中，在视图函数中 就无需写逻辑了，执行到`.is_valid`就会执行`validate`方法

* 重点：视图类和序列化类之间交互数据的桥梁：`context={}`（是1个字典）
* 自定义用户表，登录手动签发`token`，自定义的认证类
* 认证通过，正常情况下查询得到当前登录用户

### 效果

用户名登录

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2FcNHrDbloUip9ZU4gGoCy%2Fv2-d5cf3a6443ce98fda055baeb2e3d7047_720w.jpg?alt=media\&token=acd52720-f691-4478-8733-7fc17b4389e2)

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2FJGhNILnrvgsKqZUbNS1Q%2Fv2-762a77ec3a6fbc1445c7552eb2df4a02_720w.jpg?alt=media\&token=91747fea-3304-4004-a288-095d5bdc29e5)

```python
pip install djangorestframework-jwt djangorestframework
```

### 代码

* settings

```python
INSTALLED_APPS = [
....
    'app01.apps.App01Config',
    'rest_framework'
]

AUTH_USER_MODEL = 'app01.UserInfo'  # 扩写auth的user表,必须配置

import datetime
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'app01.utils.common_exception', # 配置全局异常
    'JWT_EXPIRATION_DELTA': datetime.timedelta(weeks=1), # 过期时间1周
}
```

* urls

```python
from django.contrib import admin
from django.urls import path
from app01.views import LoginView
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/',views.LoginView.as_view({'post': 'post'}))
]
```

* views

```python
from rest_framework.viewsets import ViewSet
from app01.serializer import LoginSerializer
from app01.utils import APIResponse


class LoginView(ViewSet):
    def post(self, request, *args, **kwargs):
        # 实例化得到一个序列化类的对象
        ser = LoginSerializer(data=request.data)
        # 序列化类的对象的校验方法
        ser.is_valid(raise_exception=True)
        token = ser.context.get('token')
        # 如果通过,表示登录成功,返回手动签发的token
        # 如果失败,抛异常,就不用管了
        username = ser.context.get('username')
        return APIResponse(token=token, username=username)
```

* models

```python
from django.db import models
from django.contrib.auth.models import AbstractUser

# 扩展 django 自带的 User, 为的添加一个字段
class UserInfo(AbstractUser):
    phone = models.CharField(max_length=32, unique=True)
```

* utils

```python
from rest_framework.response import Response
from rest_framework.views import exception_handler

# 自己封装的response
class APIResponse(Response):
    def __init__(self, code=200, msg='成功', data=None, status=None,
                 headers=None, content_type=None, **kwargs):
        dic = {'code': code, 'msg': msg}
        if data:
            dic['data'] = data
        dic.update(kwargs)
        super().__init__(data=dic, status=status,
                         headers=headers, content_type=content_type)


# 自己封装的全局异常处理
def common_exception(exc, context):
    # 先调用REST framework默认的异常处理方法获得标准错误响应对象
    response = exception_handler(exc, context)
    # 在此处补充自定义的异常处理
    if response is None:
        response = Response(data={'code': 999, 'msg': str(exc)})

    return response
```

* serializer

```python
from rest_framework import serializers

from app01.models import UserInfo
import re
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler


class LoginSerializer(serializers.ModelSerializer):  # 只要跟表有关系,就继承modelSerializer
    username = serializers.CharField()    # 重写 username , 否则会它会认为你想存数据
    class Meta:
        model = UserInfo
        fields = ['username', 'password']

    def validate(self, attrs):
        # username phone email  都可能是登录账户
        username = attrs.get('username')
        password = attrs.get('password')
        if re.match('^1[0-9]\d{9}$', username):  # 手机号正则
            user = UserInfo.objects.filter(phone=username).first()
            
        elif re.match('^.+@.+$', username):  # 邮箱登录正则
            user = UserInfo.objects.filter(email=username).first()
            
        else:  # 用户名登录
            user = UserInfo.objects.filter(username=username).first()
            
        if user and user.check_password(password):  # 如果登录成功,生成token
            payload = jwt_payload_handler(user)  # 通过user拿到payload
            token = jwt_encode_handler(payload)  # 通过payload拿到token
            #  token是要在视图类种使用,现在我们在序列化类中
            # 视图类和序列化类之间通过context这个字典来传递数据
            self.context['token'] = token
            self.context['username'] = user.username
            return attrs

        else:
            raise ValidationError('账号或密码错误')
```

## JWT 自定义验证

* authentications.py （创建认证文件）

```python
from rest_framework_jwt.utils import jwt_decode_handler
import jwt
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework.exceptions import AuthenticationFailed


# 自定义的认证类（验证请求数据需要的认证）
class JwtAuthentication(BaseJSONWebTokenAuthentication):
    # 自定义认证类，重写authenticate方法
    def authenticate(self, request):
        '''
         认证通过，返回user，auth
         认证失败，返回None
        '''
        token = request.META.get('HTTP_Authorization'.upper())
        if not token:
            return None

        try:
            payload = jwt_decode_handler(token)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('过期了')
        except jwt.DecodeError:
            raise AuthenticationFailed('解码错误')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('不合法的token')
        user = self.authenticate_credentials(payload)
        return (user, token)
```

* settings.py

```python
# rest_framework 设置
REST_FRAMEWORK = {
    # '我们自定义认证函数的路径',  (app.py文件.类)
    'DEFAULT_AUTHENTICATION_CLASSES': [
        "test_jwt.authentications.JwtAuthentication",
    ],
    # 全局权限配置：一站式网站（所有操作都需要登录后才能访问）
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],

    # 配置全局异常
    'EXCEPTION_HANDLER': 'test_jwt.utils.common_exception',

}

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=30000),   # token有效期
    # 'JWT_AUTH_HEADER_PREFIX': 'TOKEN',  # token前缀，默认是 jwt
}
```

由于设置了所有操作都需要登录后才能访问，那登录接口无法访问得到token数据。解决如下:

```python
from rest_framework.permissions import AllowAny

class LoginView(ViewSet):
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        '''略'''
```

携带 token 访问另一个接口：

![](https://2134947750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvILwbD2PrkBCkSitM3m4%2Fuploads%2F14F3EQxqsG50xLiPnRbl%2Fimage-20210903194138204.png?alt=media\&token=59412e99-f060-42c0-997c-be794ae30b67)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://close.gitbook.io/yun-wei-bi-ji/python/drf/jwt-duo-fang-shi-deng-lu-ji-zi-ding-yi-yan-zheng.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
