登錄接口設計和實現–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)
- 註冊
- 登錄驗證
認證接口
- 如何獲取瀏覽器提交的token信息?
- 使用Header中的Authorization
- 通過這個header增加token信息。
- 通過header發送數據,方法可以是Post、Get
- 自定義header
- 在Http請求頭中使用X-JWT字段來發送token
- 使用Header中的Authorization
- 本次選擇第二種
認證流程
- 基本上所有的業務都有需要認證用戶的信息。
- 在這裏比較實際戳,如果過期,就直接拋未認證成功401,客戶端收到後就改直接跳轉到登錄頁。
- 如果沒有提交user id,就直接重新登錄。如果用戶查到了,填充user對象。
- request->時間戳比較->user id比較->向後執行
Django的認證
- django.contrilb。auth中提供了許多方法,這裏主要介紹其中的三個:
- authenticate(**credentials)
- 提供了用戶認證,即驗證用戶名以及密碼是否正確
- user = authentica(username=‘someone’,password=‘somepassword’)
- login(HttpRequest,user,backend=None)
- 該函數接受一個HttpRequest對象,以及一個認證了的User對象
- 此函數使用django的session框架給某個已認證的用戶附加上session id等信息。
- logout(request)
- 註銷用戶
- 該函數接受一個HttpRequest對象,無返回值。
- 當調用該函數時,當前請求的session信息會全部清除
- 該用戶即使沒有登錄,使用該函數也不會報錯
- 還提供了一個裝飾器來判斷是否登錄django.contrib.auth.decorators.login_required
- 本項目使用了無session機制,且用戶信息自己建表管理,所以,認證需要自己實現。
- authenticate(**credentials)
中間件技術Middleware
- 官方定義,在Django的request和response處理過程中,由框架提供的hook鉤子
- 中間技術在1.10後發生了改變,我們當前使用1.11版本,可以使用新的方式定義。
- 參考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)
- 流程圖
- 結論
- Django中間件使用的洋蔥式,但有特殊的地方
- 新版本中間件現在
__call__
中get_response(request)之前代碼(相當於老版本中的process_request) - settings中的順序先後執行所有中間件的get_response(request)之前代碼
- 全部執行完解析路徑映射得到view_func
- settings中順序先後執行process_view部分
- return None 繼續向後執行
- return HttpResponse() 就不在執行其他函數的preview函數了,此函數返回值作爲瀏覽器端的響應
- 執行view函數,前提是簽名的所有中間件process_view都返回None
- 逆序執行所有中間件的get_response(request)之後代碼
- 特別注意,如果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中註冊
- 中間件攔截所有視圖函數,但是隻有一部分請求需要提供認證,所以考慮其他方法。
- 如果絕大多數都需要攔截,個別例外,採用中間件比較合適。
- 中間件有很多用戶,適合攔截所有請求和響應。例如瀏覽器端的IP是否禁用、UserAgent分析、異常響應的統一處理。
-
用戶驗證裝飾器
-
在需要認證的view函數上增強認證功能,寫一個裝飾器函數。誰需要認證,就在這個view函數上應用這個裝飾器。
-
定義常量,可以在當前模塊中,也可以定義在settings.py中。本次在
djweb/setting.py
中添加# 在`djweb/setting.py`中添加 #自定義常量 AUTH_EXPIRE = 8* 60 * 60 #8小時過期 AUTH_HEADER = "HTTP_JWT" #瀏覽器端是jwt,服務器端被改寫爲全大寫並加HTTP_前綴
-
用戶驗證裝飾器代碼
# 本次寫在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 ]
- 測試方法(先登錄後測試)
-
-
JWT過期問題(pyjwt過期)
-
在decode的時候,默認開啓過期驗證,如果過期,則拋出異常
-
需要在payload中增加claim exp,也就是exp的鍵值對,記錄過期的時間點
-
exp要求是一個整數int的時間戳,或時間
-
exp鍵值對存在,纔會進行過期校驗
-
測試
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)
-
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)