詳解從Django Allauth中進行登入改造小結
大概來介紹一下 Django Allauth 改造的期間遇到的一些問題和改造方法,在此之前我只想說——Django Allauth 是屑。
為什麼我說 Django Allauth 是屑
入職之初我就接到了一些第三方登入的任務,然而 Django Allauth 將內部封裝的太好,暴露的 API 不足,更新又慢,issue 和 PR 很少有人處理,當你需要擴充套件時,很多情況下你只能用一些 hack 的手段去解決問題,非常蛋疼,所以當時就決定慢慢的切到自己的一套 Auth 體系中。
目前已經做的是第三方登入的部分,賬號管理的部分還沒有遷移,之前稍微看了一下,要遷移的成本還是比較麻煩的。
遷移成本在哪裡
Django 中的賬號密碼登入一般是由本身提供的 auth 表進行擴充套件的結果,而 allauth 在此基礎上擴充了第三方登入的幾個表,再和本身的 auth user 表關聯。而這一部分是構建在 Allauth 內部的 model 內,且沒有暴露任何的方法來修改結構(當然可能也是因為真的不好改),導致一旦不滿足需求就很難搞,因為資料已經放在那裡了,刷資料同步的方案對於大流量網站來說也並不是很友好的選擇。
此外,在路由上,由於我們需要儘可能的無痛遷移和在漸進式切換時的平穩降級,因此只能通過簡單粗暴的路由覆蓋操作,這極度依賴路由的解析順序。
資料庫擴充套件與 provider 變更
說了這麼多,其實關鍵點並不在於「問題在哪裡」,而在於「我是怎麼解決這些問題的」。
Allauth 一個平臺的註冊是一個 provider,比如 「wechat」、「weibo」、「qq」,整張表是一對一的關係,那麼問題來了,我們知道,國內的平臺往往並不是一個 appid 和 key 能搞定的事情,對於 web 和移動端的平臺來說,其實是兩個 appid 共享一套 unionid,儘管官方提供了一套增加 Provider 的擴充套件方式,但實際上是沒有必要的,因為 Web 和移動端來說,獲得使用者資訊的介面是共享的,而移動端並不用通過後端獲取 access_token。在繫結上,實際上也是同一個平臺。
因此我們擴充了一張表來解決這個問題,將我們額外的資訊放在了額外新增的表中。
之後要解決的就是 admin 的 provider select 問題,它會進行一次校驗,所以我們必須要取消這些校驗並把 select 改成 input。
首先,我們要取消 Model 層的校驗,Proxy 可以對錶進行一些覆蓋式的操作(但不能改變表結構):
class CustomSocialApp(SocialApp): class Meta: proxy = True def clean_fields(self,exclude=None): # 別校驗了 pass def full_clean(self,exclude=None,validate_unique=True): # 別校驗了 pass def clean(self,validate_unique=True): # 別校驗了 pass
這裡我們在原來的SocialApp 的基礎上新建一個屬於自己的新的 Admin,他本質上還是操作 SocialApp 表,只是挪出來方便我們自定義而已:
class CustomSocialAppAdmin(SocialAppAdmin): list_display = ('provider_text','name') form = CustomAppAdminForm def get_form(self,request,obj=None,**kwargs): kwargs['widgets'] = {'provider': forms.TextInput} return super().get_form(request,obj,**kwargs) def provider_text(self,obj): return obj.provider
但是這樣就會遇到一個 provider 的校驗問題,這也就是上面我們還沒有寫完的CustomAppAdminForm 的部分,我們將校驗的部分用自定義的 form 完全取消:
class CustomSocialAppAdminForm(forms.ModelForm): class Meta: model = CustomSocialApp fields = '__all__' widgets = {'provider': forms.TextInput()} def clean(self): # 別校驗了 if self.has_error('provider'): del self._errors['provider'] self.cleaned_data['provider'] = self.data['provider'] return self.cleaned_data
這樣就完成了校驗的修改,成了一個完全體的 input 覆蓋了原來的 select。
第三方登入與繫結流程
上面可以任意在表中拓展 provider 了 ,但重頭戲其實是:搞清楚 allauth 原本的登入和繫結流程,完美的 copy 一份流程,這樣才能實現平穩降級和無痛遷移。
查詢賬號
- 獲取使用者授權資訊中的uid
- 在AllauthSocialAccount 表中獲取到對應的資料,如果沒有則返回None
登入流程
- 確保使用者是匿名使用者:request.user.is_anouymous 且已經存在對應的賬號
- 更新 AllauthSocialAccount 表中的資料到最新
- 根據 social account 更新 social token
- 寫入 session(Django 中自帶 login 函式)
註冊流程
- 確保使用者是匿名使用者且不存在對應賬號
- 建立新使用者(要點是生成使用者名稱和暱稱),在 Django 中有create_user 可以直接建立
- 寫入AllatuhSocialAccount 和AllauthSocialToken
- 寫入 session 登入
繫結流程
- 使用者不是匿名使用者
- 查詢對應的第三方賬號是否已經被繫結
- 更新 AllauthSocialAccount 表
- 更新 social token
只要按照這個流程實現下來就可以了,而同一平臺多 provider(appid)的差異功能與核心部分無關,可以在各社交媒體對應的檔案中單獨實現。
構建新的賬號系統
現在我們徹底將第三方登入抽離了出來,接下來需要抽出賬號的部分,賬號登入和註冊本質上還是 Django 提供的那些東西,因此比較好抽,需要相容的部分主要在於「忘記密碼」和「重置密碼」。
我們來思考一下為什麼這部分需要做相容:
一般來說我們都是在重置密碼時在手機或者郵箱裡收到一個驗證郵件,裡面會附上一個隨機字串用來保證連線的唯一性。而在我們替換過程中,我們不能讓一群使用者已經發送過但還沒有使用的隨機字串不可用,從可讀的角度來看,生成的內容也應該和原來差不多(同時也是避免衝突),因此需要抄一下它的忘記密碼。
在account/forms 中表明瞭 token 的生成演算法:
from django.contrib.auth.tokens import PasswordResetTokenGenerator token_generator = PasswordResetTokenGenerator() # 生成 token key = token_generator.make_token(user) # 檢查 token token_generator.check_token(user,key)
Allauth 中將 user 用 base36 加密了,相容 Python2,所以 utils 中的語句略長,由於我們直接是 Python3,所以只剩下這些句子:
from django.utils.http import base36_to_int from django.utils.http import int_to_base36 def user_pk_to_url_str(user): return int_to_base36(user.pk) def url_str_to_user_pk(s): return base36_to_int(s)
所有內容將會被儲存在account_emailconfirmation 表中,這樣就能保證對應的關係了。
總結
在賬號的部分由於還沒有改完,所以可以說的不多,只是做了一些微小的工作,對於這種可能需要根據國情定製的需求,建議大家還是小心使用。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。