登錄接口設計和實現--Django播客系統(七)

登錄接口設計和實現–Django播客系統(七)

  • 提供用戶註冊處理
  • 提供用戶登錄處理
  • 提供路由配置

用戶登錄接口設計

POST /users/login 用戶登錄
請求體 application/json
{
    "password":"string",
    "email":"string"
}

響應
200 登錄成功
400 用戶名密碼錯誤
  • 接收用戶通過POST方法提交的登錄信息,提交的數據是JSON格式數據
{
    "password":"abc",
    "email":"[email protected]"
}
  • 從user表中email找出匹配的一條記錄,驗證密碼是否正確
  • 驗證通過說明是合法用戶登錄,顯示歡迎頁面。
  • 驗證失敗返回錯誤狀態碼,例如4xx
  • 整個過程都採用AJAX異步過程,用戶提交JSON數據,服務端獲取數據後處理,返回JSON。

路由配置

# 修改user/urls.py文件
from django.conf.urls import re_path
from .views import reg,login

urlpatterns = [
    re_path(r'^$',reg), #/users/
    re_path(r'^login$',login),
]
  • 在user/views.py文件中實現登錄代碼
# user/views.py文件中增加如下代碼

import jwt
import datetime
import bcrypt
from django.conf import settings

# 篩選所需要的字段
def jsonify(instance,allow=None,exclude=[]):
    # allow優先,如果有,就使用allow指定的字段,這時候exclude無效
    # allow如果爲空,就全體,但要看看有exclude中的要排除
    modelcls = type(instance)
    if allow:
        fn = (lambda x:x.name in allow)
    else:
        fn = (lambda x:x.name not in exclude)
    # from django.db.models.options import Options
    # m:Options = modelcls._meta
    # print(m.fields,m.pk)
    return {k.name:getattr(instance,k.name) for k in filter(fn,modelcls._meta.fields)}

# 登錄接口
def login(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        print(payload)
        email = payload["email"]
        password = payload["password"]

        user = User.objects.get(email=email) # only one
        print(user.password)

        if bcrypt.checkpw(password,user.password.encode()):
            # 驗證成功
            token = gen_token(user.id)

            res = JsonResponse({
                "user":jsonify(user,exclude=["password"]),
                "token":token
            }) #返回200
            res.set_cookie("jwt",token)
            return res
        else:
            return JsonResponse({"error":"用戶名或密碼錯誤"},status=400)
    except Exception as e:
        print(e)
        #失敗返回錯誤信息和400,所有其他錯誤一律用戶名密碼錯誤
        return JsonResponse({"error":"用戶名或密碼錯誤"},status=400)
  • 註冊
    django6_001
  • 登錄驗證
    django_002

認證接口

  • 如何獲取瀏覽器提交的token信息?
    1. 使用Header中的Authorization
      • 通過這個header增加token信息。
      • 通過header發送數據,方法可以是Post、Get
    2. 自定義header
      • 在Http請求頭中使用X-JWT字段來發送token
  • 本次選擇第二種

認證流程

  • 基本上所有的業務都有需要認證用戶的信息。
  • 在這裏比較實際戳,如果過期,就直接拋未認證成功401,客戶端收到後就改直接跳轉到登錄頁。
  • 如果沒有提交user id,就直接重新登錄。如果用戶查到了,填充user對象。
  • request->時間戳比較->user id比較->向後執行

Django的認證

  • django.contrilb。auth中提供了許多方法,這裏主要介紹其中的三個:
    1. authenticate(**credentials)
      • 提供了用戶認證,即驗證用戶名以及密碼是否正確
      • user = authentica(username=‘someone’,password=‘somepassword’)
    2. login(HttpRequest,user,backend=None)
      • 該函數接受一個HttpRequest對象,以及一個認證了的User對象
      • 此函數使用django的session框架給某個已認證的用戶附加上session id等信息。
    3. logout(request)
      • 註銷用戶
      • 該函數接受一個HttpRequest對象,無返回值。
      • 當調用該函數時,當前請求的session信息會全部清除
      • 該用戶即使沒有登錄,使用該函數也不會報錯
      • 還提供了一個裝飾器來判斷是否登錄django.contrib.auth.decorators.login_required
      • 本項目使用了無session機制,且用戶信息自己建表管理,所以,認證需要自己實現。

中間件技術Middleware

  1. 官方定義,在Django的request和response處理過程中,由框架提供的hook鉤子
  2. 中間技術在1.10後發生了改變,我們當前使用1.11版本,可以使用新的方式定義。
  3. 參考https://docs.djangoproject.com/en/1.11/topics/http/middleware/#writing-your-own-middleware
  • 原理
# 測試代碼添加在user/views.py

class SimpleMiddleware1:
    def __init__(self,get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self,request):
        # Conde to be executed for each request before
        # the view (and later middleware) are called.
        print(1,'- '*30)
        print(isinstance(request,HttpRequest))
        print(request.GET)
        print(request.POST)
        print(request.body)

        # 之前相當於老闆本的process_request
        #return HttpResponse(b'',status = 404)

        response = self.get_response(request)

        #Code to be executed for each request/response after
        #the view is called.
        print(101,'- '* 30)
        return  response

    def process_view(self,request,view_func,view_args,view_kwargs):
        print(2,"-" * 30)
        print(view_func.__name__,view_args,view_kwargs)
        # 觀察view_func名字,說明在process_request之後,process_view之前已經做好了路徑映射
        return  None # 繼續執行其他的process_view或view
        # return HttpResponse("111",status=201)

class SimpleMiddleware2:
    def __init__(self,get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # Conde to be executed for each request before
        # the view (and later middleware) are called.
        print(3,"-" * 30)
        # return HttpResponse(b'',status=404)
        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.
        print(102,"- "* 30)
        return response

    def process_view(self,request,view_func,view_args,view_kwargs):
        print(4,"- "* 30)
        print(view_func,__name__,view_args,view_kwargs)
        # return None #繼續執行其他的process_view或view
        return HttpResponse("2222",status=201)
  • 修改djweb/settings.py文件,添加消息中間件
# 修改djweb/settings.py文件,添加消息中間件

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware', #註釋掉
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'user.views.SimpleMiddleware1',
    'user.views.SimpleMiddleware2',
]
  • 運行結果(使用瀏覽器訪問web)
    django6_003
  • 流程圖
    django6_004
  • 結論
    1. Django中間件使用的洋蔥式,但有特殊的地方
    2. 新版本中間件現在__call__中get_response(request)之前代碼(相當於老版本中的process_request)
    3. settings中的順序先後執行所有中間件的get_response(request)之前代碼
    4. 全部執行完解析路徑映射得到view_func
    5. settings中順序先後執行process_view部分
      • return None 繼續向後執行
      • return HttpResponse() 就不在執行其他函數的preview函數了,此函數返回值作爲瀏覽器端的響應
    6. 執行view函數,前提是簽名的所有中間件process_view都返回None
    7. 逆序執行所有中間件的get_response(request)之後代碼
    8. 特別注意,如果get_response(request)之前代碼中return HttpResponse(),將從當前中間件立即返回給瀏覽器端,從洋蔥中依次反彈

自定義中間件用戶驗證

class BlogAuthMiddleware(object):
    """自定義中間件"""
    def __init__(self,get_response):
        self.get_response = get_response
        # 初始化執行一次

    def __call__(self,request):
        # 視圖函數之前執行
        # 認證
        print(type(request),"++++++++++++++")
        print(request.GET)
        print(request.POST)
        print(request.body) #json數據
        print("- "* 30)
        response = self.get_response(request)
        #視圖函數之後執行

        return response

# 要在setting的MIDDLEWARE中註冊
  1. 中間件攔截所有視圖函數,但是隻有一部分請求需要提供認證,所以考慮其他方法。
  2. 如果絕大多數都需要攔截,個別例外,採用中間件比較合適。
  3. 中間件有很多用戶,適合攔截所有請求和響應。例如瀏覽器端的IP是否禁用、UserAgent分析、異常響應的統一處理。
  • 用戶驗證裝飾器

    1. 在需要認證的view函數上增強認證功能,寫一個裝飾器函數。誰需要認證,就在這個view函數上應用這個裝飾器。

    2. 定義常量,可以在當前模塊中,也可以定義在settings.py中。本次在djweb/setting.py中添加

      # 在`djweb/setting.py`中添加
      #自定義常量
      AUTH_EXPIRE = 8* 60 * 60 #8小時過期
      AUTH_HEADER = "HTTP_JWT" #瀏覽器端是jwt,服務器端被改寫爲全大寫並加HTTP_前綴
      
    3. 用戶驗證裝飾器代碼

      # 本次寫在user、views.py
      # 登錄驗證裝飾器
      def authenticate(viewfunc):
          def wrapper(request:HttpRequest):
              # 認證越早越好
              jwtheader  = request.META.get(settings.AUTH_HEADER,"")
              # print(request.META.keys())
              # print(request.META["HTTP_COOKIE"].get(settings.AUTH_HEADER,""))
              # print(request.META["HTTP_COOKIE"])
              print("-   ------------")
              if not jwtheader:
                  return HttpResponse(status=401)
              print(jwtheader)
              # 解碼
              try:
                  payload = jwt.decode(jwtheader,settings.SECRET_KEY,algorithms=["HS256"])
                  # payload = "aa"
                  print(payload)
              except Exception as e: #解碼有任何異常,都不能通過認證
                  print(e)
                  return HttpResponse(status=401)
      
              # 是否過期ToDO
              print("- "*30)
              try:
                  user_id = payload.get("user_id",0)
                  if user_id == 0:
                      return HttpResponse(status=401)
                  user = User.objects.get(pk=user_id)
                  request.user = user
              except Exception as e:
                  print(e)
                  return HttpResponse(status=401)
              response = viewfunc(request)
              return response
          return wrapper
      
      @authenticate #在有需要的視圖函數上加上此裝飾器
      def test(request):
          print("- "*30,"test")
          print(request.user)
          return JsonResponse({},status=200)
      
      # 修改原先gen_token(user_id)函數
      # 對id簽名
      def gen_token(user_id):
          # 時間戳用來判斷是否過期,以便重發token或重新登錄
          return  jwt.encode({
              "user_id":user_id,
              "exp":int(datetime.datetime.now().timestamp()) + settings.AUTH_EXPIRE #取整
          },settings.SECRET_KEY,algorithm="HS256").decode()
      
      • 註冊函數
      from django.conf.urls import re_path
      from .views import reg,login,test
      
      urlpatterns = [
          re_path(r'^$',reg), #/users/
          re_path(r'^login$',login), #/users/login
          re_path(r"^test$",test), #/users/test
      ]
      
      • 測試方法(先登錄後測試)
        django6_005
  • JWT過期問題(pyjwt過期)

    1. 在decode的時候,默認開啓過期驗證,如果過期,則拋出異常

    2. 需要在payload中增加claim exp,也就是exp的鍵值對,記錄過期的時間點

    3. exp要求是一個整數int的時間戳,或時間

    4. exp鍵值對存在,纔會進行過期校驗

    5. 測試

      import jwt
      import datetime
      import threading
      
      event = threading.Event()
      key = "xdd"
      
      #在jwt的payload中增加exp claim
      exp = int(datetime.datetime.now().timestamp()+10)
      data = jwt.encode({"name":'tom',"age":20,'exp':exp},key)
      print(jwt.get_unverified_header(data)) #不校驗簽名提取header
      
      try:
          while not event.wait(1):
              print(jwt.decode(data,key)) #過期校驗就會拋出異常
              print(datetime.datetime.now().timestamp())
      except jwt.ExpiredSignatureError as e:
          print(e)
      

      django6_006

view裝飾器

  • 註冊、登錄函數都是隻支持POST方法,可以在試圖函數內部自己判斷,也可以使用官方提供的裝飾器指定方法。
from django.views.decorators.http import require_http_methods,require_POST,require_GET
@require_http_methods(["POST"])

代碼參考

  • user/views.py
from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse
import simplejson
from .models import User
import jwt
import datetime
import bcrypt
from django.conf import settings
from django.views.decorators.http import require_http_methods,require_POST,require_GET

# 對id簽名
def gen_token(user_id):
    # 時間戳用來判斷是否過期,以便重發token或重新登錄
    return  jwt.encode({
        "user_id":user_id,
        "exp":int(datetime.datetime.now().timestamp()) + settings.AUTH_EXPIRE #取整
    },settings.SECRET_KEY,algorithm="HS256").decode()

# 註冊接口
@require_http_methods(["POST"])
def reg(request:HttpRequest):

    try:
        payload = simplejson.loads(request.body)
        email = payload['email']
        query = User.objects.filter(email=email)
        print(query)
        print(query.query) #查看sQL語句
        if query.first():
            return JsonResponse({"error":"用戶已存在"},status=400)

        name = payload['name']
        password = payload["password"].encode()
        print(email,name,password)

        # 密碼加密
        password = bcrypt.hashpw(password,bcrypt.gensalt()).decode()
        print(password)

        user = User()
        user.email = email
        user.name = name
        user.password = password
        user.save()

        return JsonResponse({"token":gen_token(user.id)},status=201) #創建成功返回201
    except Exception as e: #有任何異常,都返回
        print(e)
        # 失敗返回錯誤信息和400,所有其他錯誤一律用戶名密碼錯誤
        return JsonResponse({"error":"用戶名或密碼錯誤"},status=400)

# 篩選所需要的字段
def jsonify(instance,allow=None,exclude=[]):
    # allow優先,如果有,就使用allow指定的字段,這時候exclude無效
    # allow如果爲空,就全體,但要看看有exclude中的要排除
    modelcls = type(instance)
    if allow:
        fn = (lambda x:x.name in allow)
    else:
        fn = (lambda x:x.name not in exclude)
    # from django.db.models.options import Options
    # m:Options = modelcls._meta
    # print(m.fields,m.pk)
    # print("----------")
    return {k.name:getattr(instance,k.name) for k in filter(fn,modelcls._meta.fields)}

# 登錄接口
@require_POST
def login(request:HttpRequest):
    try:
        payload = simplejson.loads(request.body)
        print(payload)
        email = payload["email"]
        password = payload["password"].encode()

        user = User.objects.get(email=email) # only one
        print(user.password)

        if bcrypt.checkpw(password,user.password.encode()):
            # 驗證成功
            token = gen_token(user.id)

            res = JsonResponse({
                "user":jsonify(user,exclude=["password"]),
                "token":token
            }) #返回200
            res.set_cookie("jwt",token)
            return res
        else:
            return JsonResponse({"error":"用戶名或密碼錯誤"},status=400)
    except Exception as e:
        print(e)
        #失敗返回錯誤信息和400,所有其他錯誤一律用戶名密碼錯誤
        return JsonResponse({"error":"用戶名或密碼錯誤"},status=400)

# 登錄驗證裝飾器
def authenticate(viewfunc):
    def wrapper(request:HttpRequest):
        # 認證越早越好
        jwtheader  = request.META.get(settings.AUTH_HEADER,"")
        # print(request.META.keys())
        # print(request.META["HTTP_COOKIE"].get(settings.AUTH_HEADER,""))
        # print(request.META["HTTP_COOKIE"])
        print("-   ------------")
        if not jwtheader:
            return HttpResponse(status=401)
        print(jwtheader)
        # 解碼
        try:
            payload = jwt.decode(jwtheader,settings.SECRET_KEY,algorithms=["HS256"])
            # payload = "aa"
            print(payload)
        except Exception as e: #解碼有任何異常,都不能通過認證
            print(e)
            return HttpResponse(status=401)

        # 是否過期ToDO
        print("- "*30)
        try:
            user_id = payload.get("user_id",0)
            if user_id == 0:
                return HttpResponse(status=401)
            user = User.objects.get(pk=user_id)
            request.user = user
        except Exception as e:
            print(e)
            return HttpResponse(status=401)
        response = viewfunc(request)
        return response
    return wrapper

@require_POST
@authenticate #在有需要的視圖函數上加上此裝飾器
def test(request):
    print("- "*30,"test")
    print(request.user)
    return JsonResponse({},status=200)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章