用戶功能設計與實現–Django播客系統(六)
文章目錄
- 提供用戶註冊處理
- 提供用戶登錄處理
- 提供路由配置
用戶註冊接口設計
- 接受用戶通過Post方法提交的註冊信息,提交的數據是JSON格式數據
- 檢查email是否存在與數據庫表中,如果存在返回錯誤狀態碼,例如4xx,如果不存在,將用戶提交的數據存入表中
- 整個過程都採用AJAX異步過程,用戶提交JSON數據,服務端獲取數據後處理,返回JSON。
POST /users/ 創建用戶
請求體 application/json
{
"password":"string",
"name":"string",
"email":"string"
}
響應
201 創建成功
400 請求數據錯誤
路由配置
- 爲了避免項目中的urls.py條目過多,也爲了讓應用自己管理路由,採用多級路由
# djweb/urls.py文件
from django.urls import include
urlpatterns = [
path('admin/', admin.site.urls),
re_path(r'^$',index),
re_path(r'^index$',index),#不同路徑可以指向同一個函數執行
re_path(r'^users/',include("user.urls")),
]
-
include函數參數寫應用.路由模塊,該函數就會動態導入指定的包的模塊,從模塊裏面讀取urlpatterns,返回三元組。
-
url函數第二參數如果不是可調用對象,如果是元組或列表,則會從路徑中除去已匹配的部分,將剩餘部分與應用中的路由模塊的urlpatterns進行匹配。
-
新建
user/uls.py
文件
# user/uls.py
from django.conf.urls import re_path
from .views import reg
urlpatterns = [
re_path(r'^$',reg), #/users/
]
- 在
user/views.py
文件中添加reg方法
# user/views.py
from django.shortcuts import render
from django.http import HttpResponse,HttpRequest
def reg(request:HttpRequest):
return HttpResponse("user.reg")
- 瀏覽器中輸入
http://127.0.0.1:8000/users/
測試(這是GET請求),可以看到響應的數據。下面開始完善視圖函數。 - 在
user/views.py
中編寫視圖函數reg
測試JSON數據
-
使用POST方法,提交數據類型爲application/json,json字符串要使用雙引號。
-
這個數據是註冊用的,由客戶端提交。
- 數據提交模板爲:
{ "password":"abc", "name":"xdd", "email":"[email protected]" }
- 可以使用Postman軟件測試。
CSRF處理
-
在Post數據的時候,發現出現了下面的提示
- 原因:默認Django CsrfViewMiddleware中間件會對所有POST方法提交的信息做CSRF校驗。
-
CSRF或XSRF(Cross-site Request Forgery),即跨站請求僞造。它也被稱爲:one click attack/session riding。是一種對網站的惡意利用。它僞造成來自受信任用戶發起的請求,難以防範。
-
原理:
- 用戶登錄某網站A完成登錄認證,網站返回敏感信息的Cookie,即使是會話級的Cookie
- 用戶沒有關閉瀏覽器,或認證的Cookie一段時間內不過期還持久化了,用戶就訪問攻擊網站B
- 攻擊網站B看似一切正常,但是某些頁面裏面有一些隱藏運行的代碼,或者誘騙用戶操作的按鈕等
- 這些代碼一旦運行就是悄悄地向網站A發起特殊請求,由於網站A的Cookie還有效,且訪問的是網站A,則其Cookie就可以一併發給網站A
- 網站A看到這些Cookie就只能認爲是登錄用戶發起的合理合法請求,就會執行
-
CSRF解決
-
關閉CSRF中間件(不推薦)
#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', ]
-
csrftoken驗證
- 在表單POST提交時,需要發給服務器一個csrf_token
- 模板中的表單Form中增加
{% csrf_token %}
,它返回到了瀏覽器端就會爲cookie增加csrftoken字段,還會在表單中增加一個名爲csrfiddlewaretoken隱藏控件<input type='hidden' name='csrfmiddlewaretoken' value='jZTxU0v5mPoLvugcfLbS1B6vT8COYrKuxMzodWv8oNAr3a4ouWlb5AaYG2tQi3dD' />
- POST提交表單數據時,需要將csrfmiddlewaretoken一併提交,Cookie中的csrf_token也一併會提交,最終在中間件中比較,相符通過,不相符就看到上面的403提示
-
雙cookie驗證
- 訪問本站先獲得csrftoken的cookie
- 如果使用AJAX進行POST,需要在每一次請求Header中增加自定義字段X-CSRFTOKEN,其值來自cookie中獲取的csrftoken值
- 在服務端比較cookie和X-CSRFTOKEN中的csrftoken,相符通過
-
-
現在沒有前端代碼,爲了測試方便,可以選擇第一種方法先禁用中間件,測試完成後開啓。
JSON數據處理
- simplejson標準庫方便好用,功能強大。
pip install simplejson
- 瀏覽器端提交的數據放在了請求對象的body中,需要使用simplejson解析,解析的方式同Json模塊,但是simplejson更方便。
錯誤處理
- Django中有很多異常類,定義在django.http下,這些類都繼承自HttpResponse。
#user/views.py文件
from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse
import simplejson
def reg(request:HttpRequest):
print(request.POST)
print(request.body)
# print("- " * 30)
try:
payload = simplejson.loads(request.body)
email = payload['email']
name = payload['name']
password = payload["password"]
print(email,name,password)
return JsonResponse({},status=201) #創建成功返回201
except Exception as e: #有任何異常,都返回
print(e)
return HttpResponseBadRequest() #這裏返回實例,這不是異常類
- 將上面代碼增加郵箱檢查、用戶信息保存功能,就要用到Django的模型操作。
- 本次採用Restful實踐的設計,採用返回錯誤狀態碼+JSON錯誤信息方式。
註冊代碼 v1
# user/views.py文件
from django.http import HttpResponse,HttpRequest,HttpResponseBadRequest,JsonResponse
import simplejson
from .models import User
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"]
print(email,name,password)
user = User()
user.email = email
user.name = name
user.password = password
user.save()
return JsonResponse({},status=201) #創建成功返回201
except Exception as e: #有任何異常,都返回
print(e)
# 失敗返回錯誤信息和400,所有其他錯誤一律用戶名密碼錯誤
return JsonResponse({"error":"用戶名或密碼錯誤"},status=400)
- 郵箱檢查
- 郵箱檢查需要查user表,需要使用filter方法。
- email=email,前面是字段名email,後面是email變量。查詢後返回結果,如果查詢有結果,則說明該email已經存在,郵箱已經註冊,返回400到前端
- 用戶信息存儲
- 創建User類實例,屬性存儲數據,最後調用save方法。Django默認是在save()、delete()的時候事務自動提交。
- 如果提交拋出任何錯誤,則捕獲此異常做相應處理。
- 如果沒有異常,則返回201,不要返回任何用戶信息。之後可能需要驗證、用戶登錄等操作。
- 異常處理
- 出現獲取輸入框提交信息異常,就返回400
- 查詢郵箱存在,返回400
- save()方法保存數據,有異常,則返回400
- 注意一點,Django的異常類繼承自HttpResponse類,所以不能raise,只能return
- 前端通過狀態碼判斷是否成功
- 由於採用Restful實戰,所有異常全部返回JSON的錯誤信息,所以一律使用了JsonResponse
Django日誌
- Django的日誌配置在settings.py中
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': "DEBUG",
},
},
}
- 配置後,就可以在控制檯看到執行的SQL語句。
- 注意,必須DEBUG=True,同時level是DEBUG,否則從控制檯看不到SQL語句。
模型操作
管理器對象
-
Django會爲模型類提供一個objects對象,它是django.db.models.manager.Manager類型,用於與數據庫交互。當定義模型類的時候沒有指定管理器,則Django會爲模型類提供一個objects的管理器。
-
如果在模型類中手動指定管理器後,Django不再提供默認的objects的管理器了。
-
管理器是Django的模型進行數據庫查詢操作的接口,Django應用的每個模型都至少擁有一個管理器。
-
Django ORM
- 數據的校驗validation是在對象的Save、update方法上
- 對模型對象的CRUD,被Django ORM轉換成相應的SQL語句操作不同的數據源。
- 數據的校驗validation是在對象的Save、update方法上
查詢
- 查詢集
- 查詢會返回結果的集,它是django.db.models.query.QuerySet類型。
- 它是惰性求值,和sqlalchemy一樣。結果就是查詢的集。
- 它是可迭代對象。
- 惰性求值
- 創建查詢集不會帶來任何數據庫的訪問,直到調用方法使用數據時,纔會訪問數據庫。在迭代、序列化、if語句中都會立即求值。
- 緩存:
- 每一個查詢集都包含一個緩存,來最小化對數據庫的訪問。
- 新建查詢集,緩存爲空。首次對查詢集求值時,會發生數據庫查詢,Django會把查詢的結果存在這個緩存中,並返回請求的結果,接下來對查詢集求值將使用緩存的結果。
- 觀察下面的2個例子是要看真正生成的語句了
-
沒有使用緩存,每次都要去查庫,查了2次庫
[user.name for user in User.objects.all()] [user.name for user in User.objects.all()]
-
下面的語句使用緩存,因爲使用同一個結果集
qs = User.objects.all() [user.name for user in qs] [user.name for user in qs]
-
限制查詢集(切片)
- 查詢集對象可以直接使用索引下標的方式(不支持負索引),相當於SQL語句中的limit和offset子句。
- 注意使用索引返回的新的結果集,依然是惰性求值,不會立即查詢。
qs = User.objects.all()[20:40]
# LIMIT 20 OFFSET 20
qs = User.objects.all()[20:30]
# LIMIT 10 OFFSET 20
過濾器
- 返回查詢集的方法,稱爲過濾器,如下:
名稱 | 說明 |
---|---|
all() | |
filter() | 過濾,返回滿足條件的數據 |
exclude() | 排除,排除滿足條件的數據 |
order_by() | 排序,注意參數是字符串 |
values() | 返回一個對象字典的列表,列表的元素是字典,字典內是字段和值的鍵值對 |
filter(k1=v1).filter(k2=v2)
等價於filter(k1=v1,k2=v2)
filter(pk=10)
這裏pk指的就是主鍵,不用關心主鍵字段名,當然也可以使用主鍵名filter(emp_no=10)
mgr = User.objects
print(mgr.all())
print(mgr.values())
print(mgr.filter(pk=1).values())
print(mgr.exclude(pk=4))
print(mgr.exclude(id=1).values())
print(mgr.exclude(id=6).order_by("-id"))
print(mgr.exclude(id=6).order_by("-id").values())
print(mgr.exclude(id=6).values().order_by("-pk"))
- 返回單個值的方法
名稱 | 說明 |
---|---|
get() | 僅返回單個滿足條件的對象 如果未能返回對象則拋出DoesNotExist異常;如果能返回多條,拋出MultipleObjectsReturned異常 |
count() | 返回當前查詢的總條數 |
first() | 返回第一個對象 |
last() | 返回最後一個對象 |
exit() | 判斷查詢集中是否有數據,如果有則返回True |
email = "[email protected]"
mgr = User.objects
print(mgr.get(pk=4)) #使用主鍵查詢
print(mgr.get(email=email)) #只能返回一個結果
print(mgr.filter(id=1).get())
print(mgr.first()) #使用limit 1查詢,返回實例或None
print(mgr.filter(pk=4,email=email).first()) # and條件
字段查詢(Field Lookup)表達式
- 字段查詢表達式可以作爲filter(),exclude(),get()的參數,實現where子句。
- 語法:
屬性名稱__比較運算符=值
- 注意:屬性名和運算符之間使用雙下劃線
- 比較運算符如下
名稱 | 舉例 | 說明 |
---|---|---|
exact |
filter(isdeleted=False) filter(isdeleted__exact=False) |
嚴格等於,可省略不寫 |
contains |
exclude(title__contains="天") |
是否包含,大小寫敏感,等價於like '%天%' |
statswith endswith |
filter(title__startswith='天') |
以什麼開頭或結尾,大小寫敏感 |
isnull isnotnull |
filter(title__isnull=False) |
是否爲null |
iexact icontains istartswith iendswith |
i的意思是忽略大小寫 | |
in |
filter(pk__in=[1,2,3,100]) |
是否在指定範圍數據中 |
gt ,gte ,lt ,lte |
filter(id__get=3) filter(pk__lte=6) filter(pub_date__get=date(2000,1,1)) |
大於,小於等 |
year、month、day week_day hour、minute second |
filter(pub_date__year=2000) |
對日期類型屬性處理 |
mgr = User.objects
print(mgr.filter(id__exact=4))
print(mgr.filter(email__contains='xdd'))
print(mgr.filter(email__istartswith='xdda'))
print(mgr.filter(id__in=[1,4]))
print(mgr.filter(id__gt=3))
Q對象
- 雖然Django提供傳入條件的方式,但是不方便,它還提供了Q對象來解決。
- Q對象是django.db.models.Q,可以使用
&、|
操作符來組成邏輯表達式。~
表示not。
from django.db.models import Q
mgr = User.objects
print(mgr.filter(Q(pk__lt=6))) #不如直接寫filter(pk__lt=6)
print(mgr.filter(pk__lt=6).filter(pk__gt=1)) #與
print(mgr.filter(Q(pk__lt=6) & Q(pk__gt=1))) #與
print(mgr.filter(Q(pk=4) | Q(pk=1))) #或
print(mgr.filter(~Q(pk__lt=6))) #非
- 可使用
&|
和Q對象來構造複雜的邏輯表達式 - 過濾器函數可以使用一個或多個Q對象
- 如果混用關鍵字參數和Q對象,那麼Q對象必須位於關鍵字參數的面前。所有參數都將and和一起
新增、更新、刪除方法
- 更新數據
user = User(email='test3',name='test3') #沒有主鍵
user.save() #會新建一個數據
user = User(id=100,email='test4',name='test4') #有自增主鍵,如果不存在,則是插入
user.save()
user = User(id=100,email='test4',name='test4') # 有自增主鍵,如果存在,則是更新
user.save()
- update在查詢集上同時更新數據
# 更新所有查詢的結果
User.objects.filter(id__get=4).update(password='xyz') #將pk大於4的查詢結果更新,所有用戶的密碼修改
- delete刪除查詢集數據
ret = User.objects.filter(id__gt=4).delete()
print(ret)
# 運行結果
# DELETE FROM `user` WHERE `user`.`id` > 4 ; args=(4,)
# (3,{"user.User":3})
註冊接口設計完善
-
認證:HTTP協議是無狀態協議,爲了解決它產生了cookie和session技術。
-
傳統的session-cookie機制
- 瀏覽器發起第一次請求到服務器,服務器發現瀏覽器沒有提供session id,就認爲這是第一次請求,會返回一個新的session id給瀏覽器,瀏覽器只要不關閉,這個session id就會隨着每一次請求重新發給服務端,服務器端查找這個session id,如果查到,就認爲是同一個會話。如果沒有查到,就認爲是新的請求。
- session是會話級的,服務端還可以在這個會話session中創建很多數據session鍵值對。
- 這個session id有過期的機制,一段時間如果沒有發起請求,認爲用戶已經斷開,服務端就清除本次會話所有session。瀏覽器端也會清除相應的cookie信息。
- 服務端保存着大量session信息,很消耗服務器內存,而且如果多服務器部署,可以考慮session複製集羣,也可以考慮session共享的問題,比如redis、memcached等方案。
-
無ssession方案
- 既然服務端就是需要一個ID來表示身份,那麼不使用session也可以創建一個ID返回給客戶端。但是,要保證客戶端不可篡改該信息。
- 服務端生成一個標識,並使用某種算法對標識簽名。
- 服務端收到客戶端發來的標識,需要檢查簽名。
- 這種方案的缺點是,加密、解密需要消耗CPU計算資源,無法讓瀏覽器自己主動檢查過期的數據以清除。這種技術稱作JWT(json WEB Token)。
-
JWT
- JWT(Json WEB Token)是一種採用Json方式安裝傳輸信息的方式。本次使用PyJWT,它是Python對JWT的實現。
- 安裝
pip install pyjwt
- jwt原理
import jwt import base64 key = "secret" token = jwt.encode({'payload':'abc123'},key,'HS256') print(token) print(jwt.decode(token,key,algorithms=["HS256"])) # b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXlsb2FkIjoiYWJjMTIzIn0.lZc1PBGdbYDKq9k43tNTt1f0MHy4DjwT8NGTnHEIaVE' # token分爲3部分,用.斷開 header,payload,signature = token.split(b'.') print("- "*30) print(header,payload,signature,sep='\n') print("- "*30) def addeq(b:bytes): """ 爲base64編碼補齊等號""" rest = 4 - len(b) % 4 return b + b'='* rest print("header=",base64.urlsafe_b64decode(addeq(header))) print("payload=",base64.urlsafe_b64decode(addeq(payload))) print("signature=",base64.urlsafe_b64decode(addeq(signature))) print(" ="*30) # 根據jwt算法,重新生成簽名 # 1 獲取算法對象 from jwt import algorithms alg = algorithms.get_default_algorithms()["HS256"] newkey = alg.prepare_key(key) # key 爲secret # 2獲取前兩部分 header.payload signing_input,_,_ = token.rpartition(b".") print(signing_input) # 3使用key簽名 signature = alg.sign(signing_input,newkey) print("--------------") print(signature) print(base64.urlsafe_b64encode(signature))
- 由此,可知jwt生成的token分爲三部分
- header,由數據類型、加密算法構成
- payload,負載就是要傳輸的數據,一般來說放入python對象即可,會被json序列化的
- signature,簽名部分。是簽名2部分數據分別base64編碼後使用點號鏈接後,加密算法使用key計算好一個結果,再被base64編碼,得到簽名
- 所有數據都是明文傳輸的,只是做了base64,如果是敏感信息,請不要使用jwt。
- 數據簽名的目的不是爲了隱藏數據,而是保證數據不被篡改。如果數據篡改了,發回到服務器端,服務器使用自己的key再計算一遍,然後進行簽名比對,一定對不上簽名。
- jwt使用場景
- 認證:這是jwt最常用的場景,一旦用戶登錄成功,就會得到wt,然後請求中就可以帶上這個jwt。服務器中jwt驗證通過,就可以被允許訪問資源。甚至可以在不同域名中傳遞,在單點登錄(Single Sign On)中應用廣泛。
- 數據交換:jwt可以防止數據被篡改,它還可以使用公鑰、私鑰加密,確保請求的發送者是可信的
-
密碼
- 使用郵箱+密碼方式登錄。
- 郵箱要求唯一就行了,但是,密碼存儲需要加密。早期,都是用明文的密碼存儲。後來,使用MD5存儲,但是,目前也不安全,網上有很多MD5的網站,使用反查方式找到密碼。
- 加鹽,使用hash(password+salt)的結果存入數據庫中,就算拿到數據庫的密碼反查,也沒有用了。如果是固定加鹽,還是容易被找到規律,或者從源碼中泄露。隨機加鹽,每一次鹽都變,就增加了破解的難度。
- 暴力破解:什麼密碼都不能保證不被暴力破解,例如窮舉。所以要使用慢hash算法,例如bcrypt,就會讓每一次計算都很慢,都是秒即的,這樣窮舉的時間就會很長,爲了一個密碼破解的時間在當前CPU或者GPU的計算能力下可能需要幾十年以上。
-
bcrypt
- 安裝
pip install bcrypt
import bcrypt import datetime password = b'12345xdd' # 每次拿到鹽都不一樣 print(1,bcrypt.gensalt()) print(2,bcrypt.gensalt()) salt = bcrypt.gensalt() # 拿到的鹽不同,計算等到的密文相同 print("===============same salt ===============") x = bcrypt.hashpw(password,salt) y = bcrypt.hashpw(password,salt) print(3,x) print(4,y) # 每次拿到的鹽不同,計算生成的密文也不一樣 print("============different salt===============") xx = bcrypt.hashpw(password,bcrypt.gensalt()) yy = bcrypt.hashpw(password,bcrypt.gensalt()) print(5,x) print(6,y) # 校驗 print(bcrypt.checkpw(password,xx),len(xx)) print(bcrypt.checkpw(password+b' ',xx),len(xx)) # 計算時長 start = datetime.datetime.now() y3 = bcrypt.hashpw(password,bcrypt.gensalt()) delta = (datetime.datetime.now() - start).total_seconds() print(10,'duration={}'.format(delta)) # 檢驗時長 start = datetime.datetime.now() y4 = bcrypt.checkpw(password,xx) delta = (datetime.datetime.now() - start).total_seconds() print(y4) print(11,'duration={}'.format(delta)) start = datetime.datetime.now() y5 = bcrypt.checkpw(b'1',xx) delta = (datetime.datetime.now() - start).total_seconds() print(y5) print(12,'duration={}'.format(delta))
- 從耗時看出,bcrypt加密、驗證非常耗時,所有如果窮舉,非常耗時。而且碰巧攻破一個密碼,由於鹽不一樣,還等窮舉另一個。
salt=b'$2b$12$jwBD7mg9stvIPydF2bqoPO' b'$2b$12$jwBD7mg9stvIPydF2bqoPOodPwWYVvdmZb5uWWuWvlf9iHqNlKSQO' $是分隔符 $2b$,加密算法 12,表示2^12 key expansion rounds 這是鹽b'jwBD7mg9stvIPydF2bqoPO',22個字符,Base64 這是密文b'odPwWYVvdmZb5uWWuWvlf9iHqNlKSQO',31個字符,Base64
- 安裝
註冊代碼 v2
-
全局變量
- 項目的settings.py文件實際上就是全局變量的配置 文件。
- SECRET_KEY一個強KEY
from django.conf import settings print(settings.SECRET_KEY)
- 使用jwt和bcrypt,修改註冊代碼
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
# 對id簽名
def gen_token(user_id):
# 時間戳用來判斷是否過期,以便重發token或重新登錄
return jwt.encode({
"user_id":user_id,
"timestamp":int(datetime.datetime.now().timestamp()) #取整
},settings.SECRET_KEY).decode()
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)
- save方法會自動提交事務
- 數據庫中內容