drf-jwt原始碼分析以及自定義token簽發認證、alc和rbac

2023-02-11 21:00:43

1.drf-jwt原始碼執行流程

1.1 簽發(登入)

1.程式碼:
urls.py:
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    path('login/',obtain_jwt_token),
]

2.我們點進obtain_jwt_token原始碼:
drf/views.py:
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()

3.login需要提交使用者名稱和密碼,所以是post請求,我們需要在其父類別中找到post方法:
ObtainJSONWebToken>>>JSONWebTokenAPIView,在JSONWebTokenAPIView中找到了post方法:
    def post(self, request, *args, **kwargs):
        # serializer是序列化類的物件
        serializer = self.get_serializer(data=request.data)
			# 校驗,如果校驗通過:
        if serializer.is_valid():
         # 拿到user和token
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
         # 拿到返回格式,之前我們自定義過token的返回格式。
"""
當我們點選方法:jwt_response_payload_handler(token, user, request),跳轉到了rest_framework_jwt:jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER。說明JWT_RESPONSE_PAYLOAD_HANDLER需要在設定中指定返回格式。因此我們在設定中指定:JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.jwt_response.jwt_response',
}。所以返回格式才能按照我們指定的格式返回。
"""

            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)        
            return response
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
"""
執行if serializer.is_valid():這句話時就會執行序列化類中的程式碼,但是如何得到user和token,在序列化類的全域性勾點中尋找答案:
"""
4.還是回到drf/views.py中:
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()
ObtainJSONWebToken後面跟了as_view()說明這是檢視類,點進去:
    class ObtainJSONWebToken(JSONWebTokenAPIView):
        serializer_class = JSONWebTokenSerializer
說明JSONWebTokenSerializer就是序列化類。

5.JSONWebTokenSerializer程式碼:
class JSONWebTokenSerializer(Serializer):
# 這是一個全域性勾點,因為上面沒有單個欄位的校驗規則,所以此時的addr就是{'username':'max','password':'max123'}
    def validate(self, attrs):
        credentials = {
      # 這一步還是拿到了使用者名稱,只不過是繞了一下
            self.username_field: attrs.get(self.username_field),
      # 拿到密碼
            'password': attrs.get('password')
        }
		# 必須使用者名稱和密碼都幼值才成立
        if all(credentials.values()):
        # auth模組中的,如果使用者存在會拿到使用者物件
            user = authenticate(**credentials)
            if user:
        # 如果能拿到使用者物件,並且使用者被鎖(is_active預設是1,如果使用者被鎖則是0)
                if not user.is_active:
                # 如果被鎖則提示disabled
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)
					# 通過使用者物件拿到荷載
                payload = jwt_payload_handler(user)
					# 通過payload生成token
                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)

1.2 認證 (認證類)

1.認證類需要從JSONWebTokenAuthentication中找到authenticate方法。在其父類別中找到了authenticate方法。
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

    def authenticate(self, request):
        """
        Returns a two-tuple of `User` and token if a valid signature has been 
        """
        # jwt_value就是token字串
        jwt_value = self.get_jwt_value(request)
        # 如果token值沒傳,直接返回None
        if jwt_value is None:
            return None

        try:
        # payload是一個字典:{'user_id': 1, 'username': 'max', 'exp': 1676113688, 'email': ''}
            payload = jwt_decode_handler(jwt_value)
        # 還有幾種可能拿不到,分別是:篡改token、token過期了、未知錯誤
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()
		# 如果沒有錯誤順利能拿到使用者物件
        user = self.authenticate_credentials(payload)
		# 返回當前登入使用者,token
        return (user, jwt_value)
    
2.接下來我們來看剛才的方法get_jwt_value(request)是如何拿到token的,該方法在類JSONWebTokenAuthentication中:
    def get_jwt_value(self, request):
        auth = get_authorization_header(request).split()

3.我們需要找到方法get_authorization_header(request),在BaseAuthentication中找到了該方法:
def get_authorization_header(request):
    # request.META可以拿到get請求頭當中的值,結果是個字典。在資料傳送到後端時鍵都變成了'HTTP_前端傳入的鍵',如果拿不到就拿一個空字串。此時的auth是jwt dfjkdlsjf...
    auth = request.META.get('HTTP_AUTHORIZATION', b'')
    if isinstance(auth, str):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    # 轉碼然後返回
    return auth

4.繼續回到get_jwt_value(request)方法:
    def get_jwt_value(self, request):
        # auth是個被分割列表:[jwt,dfjkdlsjf]
        auth = get_authorization_header(request).split()
        # JWT_AUTH_HEADER_PREFIX就是'JWT',轉化成小寫'jwt'
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()

        if not auth:
         # 如果請求頭沒帶,就去cookie中取
            if api_settings.JWT_AUTH_COOKIE:
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None
			# 如果列表索引0不為jwt返回None
        if smart_text(auth[0].lower()) != auth_header_prefix:
            return None

        if len(auth) == 1:
            msg = _('Invalid Authorization header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid Authorization header. Credentials string '
                    'should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)
			# 返回列表索引1,也就是token
        return auth[1]

2.自定義使用者表簽發和認證

2.1 簽發

views.py:
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from .models import Userinfo
from rest_framework_jwt.settings import api_settings
# 生成荷載的方法,我們直接呼叫drf的
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
# 生成token的方法,我們也呼叫drf的
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
    
class UserView(ViewSet):
    @action(methods=['POST'],detail=False)
    def login(self,request,*args,**kwargs):
        username = request.data.get('username')
        password = request.data.get('password')
        user = Userinfo.objects.filter(username=username,password=password).first()
        if user:
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            return Response({'code':100,'msg':'登入成功','token':token})
        else:
            return Response({'code':101,'msg':'使用者名稱或密碼錯誤'})
        
urls.py:
router = SimpleRouter()
router.register('user', UserView, 'user')  # 此時路由:http://127.0.0.1:8000/api/v1/user/login/

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(router.urls))
]
通過以上步驟,我們可以自定義出功頒佈token:

2.2 認證

新建一個認證類authentication.py,在其中寫認證類的程式碼:
authentication.py:
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.settings import api_settings
import jwt
from rest_framework.exceptions import AuthenticationFailed
from .models import Userinfo
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER


class JsonWebTokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.META.get('HTTP_TOKEN')  # 前端的格式可以自定義,取的時候在前面加上HTTP_就好,並且鍵要大寫
        if token:
            try:
   				# jwt_decode_handler()方法僅僅是通過token找到payload,內部並沒有切割字串的方法 get_jwt_value(),所以前端在傳的時候不需要加jwt和空格。
                payload = jwt_decode_handler(
                    token)  
                print(payload)  # :{'user_id': 1, 'username': 'max', 'exp': 1676113688, 'email': ''}
                user = Userinfo.objects.filter(pk=payload.get('user_id')).first()
                return user, token
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('token過期')
            except jwt.DecodeError:
                raise AuthenticationFailed('token認證失敗')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('token無效')
            except Exception as e:
                raise AuthenticationFailed('未知異常')
        raise AuthenticationFailed('token沒有傳 認證失敗')
        
views.py:
class BookView(ModelViewSet):
    # 手寫jwt認證只需要寫認證類不用寫許可權類
    authentication_classes = [JsonWebTokenAuthentication]

    def list(self, request, *args, **kwargs):
        return Response('success')

3.auth_user表密碼加密

3.1 手動定義類似token的加密方式:

1.token的加密方式:token由三段構成,第一段宣告加密演演算法和型別,第二段存放有效資訊的地方:過期時間、簽發時間、使用者id、使用者名稱等。第三段是加密後的header和base64加密後的payload。
        
2.我們也可以定義一中類似token的加密方式:改密碼分為三段,用兩個$連線起來,第一段是密碼加密後的密文,第二段是隨機生成的鹽(不加密),第三段是加密後的原密碼和鹽連線在一起(中間不加符號),在通過md5加密。
    
3.程式碼:
views.py:
import uuid
import hashlib

def register_hash(request):
    """
    password:原密碼
    res:原密碼加密之後的密文
    salt:隨機生成的鹽(不加密)
    pwd1:res$salt
    pwd_part3:password+salt
    res2:給pwd_part3加密之後的密文
    pwd2:最終密碼:pwd1+res2
    """
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        # print(password)  # 123
        md51 = hashlib.md5()
        md51.update(password.encode('utf8'))
        res = md51.hexdigest()
        # print('res',res)  # 202cb962ac59075b964b07152d234b70
        # 隨機生成一個鹽(不加密)
        salt = str(uuid.uuid4())
        print('salt',salt)
        # 將加密的原密碼和不加密的鹽組合起來,組成密碼的前兩部分
        pwd1 = res + '$' + salt  # 202cb962ac59075b964b07152d234b70$44590a73-2602-4f96-a718-972d83fb7ae6 
        # 將不加密的密碼和鹽組合起來,組成明文
        pwd_part3 = res + salt
        md52 = hashlib.md5()
        md52.update(pwd_part3.encode('utf8'))
        # 原密碼和鹽組成的明文加密,組成密碼的第三部分
        res2 = md52.hexdigest()
			# 最終的密碼
        pwd2 = pwd1 + '$' + res2
        print(pwd2)
        User_hash.objects.create(username=username,hash_pwd=pwd2)
        return HttpResponse('註冊成功')

    return render(request, 'pwd.html', locals())


def login_view(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        md51 = hashlib.md5()
        md51.update(password.encode('utf8'))
        res = md51.hexdigest()
        user_obj = User_hash.objects.filter(username=username).first()
        if not user_obj:
            return HttpResponse('使用者未註冊')
        if not res == user_obj.hash_pwd.split('$')[0]:
            return HttpResponse('密碼錯誤')
        salt = user_obj.hash_pwd.split('$')[1]
        pwd_part3 = res + salt
        md52 = hashlib.md5()
        md52.update(pwd_part3.encode('utf8'))
        res2 = md52.hexdigest()
        if not res2 == user_obj.hash_pwd.split('$')[2]:
            return HttpResponse('密碼錯誤')
        return HttpResponse('登陸成功')
    return render(request,'login.html',locals())
	
urls.py:
urlpatterns = [
    path('register/',views.register_hash),
    path('login1/',views.login_view)
]

3.2 利用django自帶的方法make_password()和check_password()來編寫登入註冊

1.make_password()只有一個引數,就是原密碼。返回值是加密後的密碼,也是django的auth_user表中的使用者密碼的加密方式:

from django.contrib.auth.hashers import make_password, check_password
from .models import User1
# 用djanngo的make_password方法註冊
def register2(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        # 密碼加密:
        pwd = make_password(password)
        User1.objects.create(username=username,password=pwd)
        return HttpResponse('註冊成功')
    return render(request,'register2.html',locals())

2.check_password()方法用來校驗密碼,裡面有兩個引數,第一個是明文密碼,第二個引數是密文密碼,如果這兩個密碼匹配那麼結果是True,不匹配返回結果是False。
# 用django的check_password方法登陸
def login2(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        real_pwd = User1.objects.filter(username=username).first().password
        is_correct = check_password(password,real_pwd)
        if is_correct:
            return HttpResponse('登陸成功')
    return render(request,'login2.html',locals())
"""
如果超級管理員密碼忘記了,可以再建立一個超級管理員,然後將新建立的管理員密碼(密文)複製到前一個超級管理員的密碼處,這兩個管理員就會使用同一個密碼。
"""

4.simpleui使用

1.之前公司裡,做專案,要使用許可權,要快速搭建後臺管理,使用djagno的admin直接搭建,django的admin介面不好。所以採用第三方軟體。

2.第三方的美化:
	xadmin:作者棄坑了,bootstrap+jq 
	simpleui: vue,介面更好看
    
3.現在階段,一般前後端分離比較多:django+vue

4.1 使用步驟

1.安裝:pip install simpleui
    
2.在app中註冊
要註冊在最上面
INSTALLED_APPS = [
    'simpleui'
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01',
    'rest_framework',
]
然後當我們登入到admin後臺管理就變成了這樣:

3.然後我們在models.py中構造以下幾張表,並且在admin.py中註冊:
models.py:
class Book(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish_date = models.DateField()
    publish = models.ForeignKey(to='Publish',to_field='nid',on_delete=models.CASCADE)
    authors=models.ManyToManyField(to='Author')
    def __str__(self):
        return self.name

class Author(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    age = models.IntegerField()
    author_detail = models.OneToOneField(to='AuthorDetail',to_field='nid',unique=True,on_delete=models.CASCADE)

class AuthorDetail(models.Model):
    nid = models.AutoField(primary_key=True)
    telephone = models.BigIntegerField()
    birthday = models.DateField()
    addr = models.CharField(max_length=64)

class Publish(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    city = models.CharField(max_length=32)
    email = models.EmailField()
   
admin.py:
from .models import Book,Publish,AuthorDetail,Author
admin.site.register(Book)
admin.site.register(Publish)
admin.site.register(AuthorDetail)
admin.site.register(Author)
然後我們就可以在admin後臺管理頁看到這幾張表:

4.在apps.py中加入verbose_name = '圖書管理系統',就可以將左側列表中的app名改成自定義的名字:
class App01Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'app01'
    verbose_name = '圖書管理系統'

5.在models.py中每張表下面加入:
    class Meta:
        verbose_name_plural = '作者表'
在後臺管理就可以將表名顯示成中文:

然後再資料庫加一些資料(可以在admin後臺管理加,也可以在pycharm中加),新增好之後可以直接點進去修改,這就完成了對一個圖書管理系統增刪改查的建立。
"""
DataTimeField欄位剛開始預設是英文,如果我們想要把它設定成中文,需要在settings.py中設定:LANGUAGE_CODE = 'zh-hans'。
"""

6.當我們在admin.py中註冊好之後,我們在頁面上只能看到書名。註冊還有一種方式,在admin.py中寫一個類,定義哪張表選擇顯示的欄位就繼承哪張表,用list_play=('欄位名')來定義顯示的欄位名,但是不能上傳多對多的外來鍵欄位:

7.還可以在頁面上增加按鈕:在剛才定義的BookAdmin中繼續加內容:
    actions = ['custom_button']  # custom_button不能更改

    def custom_button(self, request, queryset):
        print(queryset)  # queryset就是選中物件的queryset,可以額外做一些操作

    custom_button.short_description = '額外操作'  # 按鈕的中文名
    custom_button.type = 'success'  # 設定按鈕顏色

8.側邊欄設定,需要在settings.py中進行如下設定:
SIMPLEUI_CONFIG = {
    'system_keep': False,
    'menu_display': ['圖書管理', '許可權認證', '外連'],  # 開啟排序和過濾功能, 不填此欄位為預設排序和全部顯示, 空列表[] 為全部不顯示.
    'dynamic': True,  # 設定是否開啟動態選單, 預設為False. 如果開啟, 則會在每次使用者登陸時動態展示選單內容
    'menus': [
        # name要和menu_display中註冊的名字保持一致
        {  
            'name': '圖書管理',
            'app': 'app01',
            'icon': 'fas fa-code',
            # models繼續往下寫下面的子目
            'models': [
                {
                    'name': '圖書',
                    'icon': 'fa fa-user',
                    'url': '/admin/app01/book/'
                },
                #url只能是自己在urls.py中設定的路由或者是自動生成的路由
                {
                    'name': '出版社',
                    'icon': 'fa fa-user',
                    'url': 'app01/publish/'
                },
                {
                    'name': '作者',
                    'icon': 'fa fa-user',
                    'url': 'app01/author/'
                },
                {
                    'name': '作者詳情',
                    'icon': 'fa fa-user',
                    'url': 'app01/authordetail/'
                },
            ]
        },
        {
            'app': 'auth',
            'name': '許可權認證',
            'icon': 'fas fa-user-shield',  # 圖示
            'models': [
                {
                    'name': '使用者',
                    'icon': 'fa fa-user',
                    'url': 'auth/user/'
                },
                {
                    'name': '組',
                    'icon': 'fa fa-user',
                    'url': 'auth/group/'
                },
            ]
        },
        {

            'name': '外連',
            'icon': 'fa fa-file',
            'models': [
                {
                    'name': 'Baidu',
                    'icon': 'far fa-surprise',
                    # 第三級選單 ,
                    'models': [
                        {
                            'name': '愛奇藝',
                            'url': 'https://www.iqiyi.com/dianshiju/'
                            # 第四級就不支援了,element只支援了3級
                        }, {
                            'name': '百度問答',
                            'icon': 'far fa-surprise',
                            'url': 'https://zhidao.baidu.com/'
                        }
                    ]
                },
 # 我們自己定義的頁面也可以直接寫路由:               
                {
                    'name': '大屏展示',
                    'url': '/show/',
                    'icon': 'fab fa-github'
                }]
        }
    ]
}

9.其他設定項:
SIMPLEUI_LOGIN_PARTICLES = False  #登入頁面動態效果
SIMPLEUI_LOGO = 'https://avatars2.githubusercontent.com/u/13655483?s=60&v=4'#圖示替換
SIMPLEUI_HOME_INFO = False  #取消首頁右側github提示
SIMPLEUI_HOME_QUICK = False #快捷操作
SIMPLEUI_HOME_ACTION = False # 動作

5.許可權控制

5.1 網際網路專案:

	alc:存取控制列表,許可權放在列表中
	使用者表:儲存使用者資訊,和許可權表是一對多的關係
	許可權表:每個使用者擁有的許可權
     
比如:
	許可權列表:[發視訊,發評論,開直播]
	max擁有的許可權:[發視訊,發評論,開直播]
	jerry擁有的許可權:[發視訊]

5.2 公司內部專案(python寫公司內部專案居多):

1.rbac:是基於角色的存取控制(Role-Based Access Control )在 RBAC  中,許可權與角色相關聯,使用者通過成為適當角色的成員而得到這些角色的許可權。這就極大地簡化了許可權的管理。這樣管理都是層級相互依賴的,許可權賦予給角色,而把角色又賦予使用者,這樣的許可權設計很清楚,管理起來很方便。
        
2表關係:
	使用者表:使用者和角色是多對多關係(一個使用者可以對應多個角色)
	角色表:類似於公司中的崗位
	許可權表:使用者表不直接和使用者表建立聯絡,而是和角色表建立聯絡(某個使用者成為了某個角色之後才擁有某項許可權)。角色表和許可權表是多對多關係。
        
所以描述以上三者關係需要建立5張表:
	使用者表、角色表、許可權表、使用者角色表、角色許可權表
    
3.使用者和許可權不直接建立聯絡是為了簡化流程方便管理,但是也有特殊情況:比如公司人資想要獲取拉取程式碼的許可權,但是開發角色擁有的許可權不僅僅是拉取程式碼而且還能操作程式碼。
如果將開發的角色賦給人資就會導致人資的許可權過大。所以角色和許可權直接監理聯絡,產生第6張表:角色許可權中間表。
    
4.以圖書管理系統為例,目前設定2個使用者,一個是root(超級管理員),一個是max(普通使用者)。目前想要設定max的許可權為檢視書籍列表和作者列表,需要首先建立一個組(角色),該組中規定了檢視書籍列表和作者列表的許可權。

在進入到使用者設定列表中:首先取消該使用者超級管理員身份(公司內超級管理員數量很有限)。

再登陸使用者max,發現系統中只有作者和圖書兩個選項,並且只能檢視:

5.管理員也可以直接設定使用者和許可權的對應關係:

6.在表中許可權、組以及它們的對應關係都在以下6張表中:
auth_user:使用者表
auth_group:角色表,組表
auth_permission:許可權表
auth_user_groups:使用者和角色中間表
auth_group_permissions:角色和許可權中間表
auth_user_user_permissions:使用者和許可權中間表

7.用管理員給使用者max通過使用者和許可權對應關係給max新增了一個許可權(不通過角色),該功能也可以疊加到max的許可權中,使用者max現在有三個功能:檢檢視書和作者(通過角色新增許可權)、檢視出版社(通過使用者新增許可權):