真香定律!我用這種模式重構了第三方登入

程序员老猫發表於2024-03-04

分享是最有效的學習方式。

部落格:https://blog.ktdaddy.com/

老貓的設計模式專欄已經偷偷發車了。不甘願做crud boy?看了好幾遍的設計模式還記不住?那就不要刻意記了,跟上老貓的步伐,在一個個有趣的職場故事中領悟設計模式的精髓吧。還等什麼?趕緊上車吧。

故事

辦公室裡,小貓託著腮幫對著電腦陷入了思考。就在剛剛,他接到了領導指派的一個任務,業務調整,登入方式要進行擴充。例如需要接入第三方的微信登入,企業微信授權登入等等。

原因大概是這樣,現在大環境不好,原來面向B端企業員工的電商業務並不好做,新客擴充比較困難,業務想要有更好的起色著實比較困難,所以決策層決定要把登入的口子放開,原來支援手機密碼登入以及手機驗證碼進行登入,現在為了更好地推廣,需要支援微信掃碼關注企業公眾號後登入,企業微信,微博等等一些列的第三方登入模式。

說白了未來到底會有多少種登入方式不得而知,那麼面對這樣一個棘手的問題,小貓又該何去何從?

概述

登入問題相信後端小夥伴都有接觸過,最簡單的可能就是做一個許可權系統就會用到登入名+密碼+驗證碼進行登入,繼而稍微複雜一些可能會涉及手機驗證碼登入。現在隨著第三方平臺的層出不窮,我們很多網站其實都提供了聯合登入。使用者掏出手機簡單地一個掃碼動作即可完成初步的註冊登入功能。這種方式一定程度上能夠給當前的網站帶來更多的流量。

關於小貓遇到的問題,咱們嘗試從下面幾個點去解決。

概要

登入演化

聊到登入,我們首先去了解一下整個登入認證的發展階段,以及目前比較常見也相對比較複雜的微信公眾號授權登入流程。

基於Cookie/Session進行驗證登入

在早期,也就是可能是單體系統的時代,亦或者站在java開發者角度來說是jsp時代的時候,我們用的登入方式就是Cookie/Session驗證的方式。關於Cookie以及Session相信很多後端的小夥伴都應該知道,當然若真有不清楚的,大家可以自己查閱一下相關資料。
利用這種方式登入的流程其實還是比較簡單的,如下流程:

session登入

基於上述登入成功後,服務端將使用者的身份資訊儲存在Session裡,並將session ID透過cookie傳遞給客戶端。後續的資料請求都會帶上cookie,服
務端根據cookie中攜帶的session id來得到辨別使用者身份。

簡單的java虛擬碼如下:

...
session.setAtrrbuite("user",user);
...
session.getAttrbuite("user");

當然上述的虛擬碼還是基於最最原始的寫法去寫的,關於這種登入的框架,其實目前市面上也有比較成熟的,例如輕量級的shiro,spring本身自帶的許可權認證框架也有。

隨著業務的發展,系統訪問量級的增大,我們漸漸發現這種方式存在著一些問題:

  1. 由於服務端需要對接大量的客戶端,也就需要存放大量的Seesion ID,這樣就會導致伺服器壓力過大。如果伺服器是個叢集,為了同步登入的狀態,需要將Session ID同步到每一臺伺服器上,無形中增加了伺服器端的維護成本。
  2. 由於Session ID存放在Cookie中,所以無法避免CSRF攻擊(跨站請求偽造)。

當然其他問題也歡迎小夥伴們進行補充。
為了解決這一些列的問題,我們漸漸演化出了另外一種登入認證方式————基於token進行認證登入。

基於TOKEN進行認證登入

現在的系統大部分都是前後端分離開發的。後端大多使用了WEB API,此時token無疑是處理認證的最好方式。

Session 方案中使用者資訊(以Session記錄形式)儲存在服務端。而Token方案中(以Token形式)儲存在客戶端,服務端僅驗證Token合法性即可。基於Token的身份驗證是無狀態的,不將使用者資訊存在伺服器中。這種概念解決了在服務端儲存資訊時的許多問題。NoSession意味著咱們的程式可以根據需要去增減機器,而不用去擔心使用者是否登入。

咱們一起來看一下如果使用TOKEN整個流程。

token機制

關於上述token機制的特點有以下幾點:

  • 無狀態、可擴充套件:在客戶端儲存的Token是無狀態的,並且能夠被擴充套件。基於這種無狀態的和不儲存Session資訊,所以不會對伺服器端造成壓力,負載均衡器能夠將使用者資訊從一個伺服器傳到其他伺服器上,即使是伺服器叢集,也不需要增加維護成本。

  • 可擴充套件性:Tokens能夠建立與其它程式共享許可權的程式。(即,我們所說的第三方平臺聯合登入的時候,token的生成機制以及驗證可以由第三方系統進行聯合驗證登入)

  • 安全性:請求中傳送Token而不是傳送Cookie,能夠防止CSRF(跨站請求偽造)。即客戶端使用Cookie儲存了Tooken,Cookie也僅僅是一個儲存機制而不是用於認證。不將資訊儲存在Session中,讓我們少了對Session的操作。Token也可以存放在前端任何地方,可以不用儲存在Cookie中,提升了頁面的安全性。Token是會失效的,一段時間之後使用者需要重新驗證。

  • 多平臺跨域:對應用程式和服務進行擴充套件的時候,需要介入各種各種的裝置和應用程式。只要使用者有一個透過了驗證的token,資料和資源就能夠在任何域上被請求到。

微信掃碼跳轉公眾號認證登入

這也是後續小貓遇到的問題,以及需要和其他第三方Api主要對接的。其實關於掃碼認證登入也是基於token機制的一種擴充。只不過第三方的平臺在token機制上新增了獲取二維碼進行二次確認的過程。咱們以微信掃碼跳轉公眾號登入為例來看一下整個流程。其他的第三方登入流程其實也是大同小異,咱們瞭解一個流程即可,不同的平臺只是對接不同的api而已。流程圖如下:

ticket機制

從上面這幅圖看到,掃碼登入其實複雜就複雜在獲取token這個步驟上,當獲取完畢token之後,其後續的業務邏輯其實基本也是一樣的。

其實其他第三方的登入其實也是大同小異,最主要的難點是在如何獲取token上,我們只要認真看完對接的api,其實問題也基本都能迎刃而解。

說明一下,老貓這裡繪圖用了drawio工具,如果想要知道老貓的繪圖思路,大家可以看看這裡《繪圖思路

如何相容多套?

看完上述之後,相信大家會對認證登入心裡有桿秤了。細節方面其實只要去查詢相關平臺的api,然後去擼程式碼就好了。但是實現一套倒是還好,但是現在小貓遇到的問題是需要在原邏輯上去豐富登入的程式碼。如果在老的程式碼上透過if else的方式去實現多套登入邏輯,那估計後面又是屎山。

這裡,其實我們可以引入“介面卡設計模式”去解決這樣的問題。

什麼是介面卡模式?

介面卡模式(英文名:Adapter Pattern)是指將一個類的介面轉換成使用者期望的另一個介面,使得原本介面不相容的類可以一起工作。

介面卡模式可以分為兩類:物件介面卡模式和類介面卡模式。物件介面卡模式透過組合實現適配,而類介面卡模式則透過繼承實現適配。

此外,還有一種特殊的介面卡模式——預設介面卡模式它由一個抽象類實現,並在其中實現目標介面中所規定的所有方法,但這些方法的實現通常是空方法,由具體的子類來實現具體的功能。介面卡模式的應用可以提高程式碼的複用性和可維護性,同時幫助解決不同介面之間的相容性問題。

上面的概念比較抽象,其實在咱們的日常生活中也有這樣的例子,例如手機充電轉換頭,顯示器轉接頭等等。

介面卡模式重構第三方登入

話不多說,直接開幹,我們就針對小貓的遇到這個第三方登入的場景,咱們用程式碼重構一把。(當然,這裡我們側重的還是虛擬碼)。跟著老貓,咱們一步步走好程式碼的演化。

咱們先看一下老的業務程式碼,如下:

public class UserLoginService {
    public ApiResponse<String> regist(String userName,String password) {
        //...dosomething
        return ApiResponse.success("success");
    }
    public ApiResponse login(String userName, String password) {
        return null;
    }
}

接下來由於小貓的業務會發生變更,新的登入方式會層出不窮,所以,我們得遵循之前提到的軟體設計原則去更好地寫一下業務程式碼。我們遵循之前提到的開閉原則,於是我們邁出了重構程式碼的第一步,我們將建立一個新的第三方登入的類來專門處理第三方的登入對接。如下:

public class ThirdPartyUserLoginService extends UserLoginService {

    public ApiResponse loginForQQ(String openId) {
        /**
         * openid 全域性唯一,咱們直接作為使用者名稱
         * 預設密碼QQ_EMPTY
         * 註冊(原來父類中有註冊實現)
         * 呼叫原來的登入
         */
        return loginForRegist(openId, null);
    }

    public ApiResponse loginForWechat(String openId) {
        return null;
    }

    public ApiResponse loginForToken(String token) {
        return null;
    }

    public ApiResponse loginForTel(String tel, String code) {
        return null;
    }

    public ApiResponse<String> loginForRegist(String userName, String password) {
        super.login(userName, password);
        return super.login(userName, password);
    }
}

寫到這裡,其實咱們已經整合了多種登入方式的程式碼相容,但是這種實現方式顯然是不太優雅的,看起來比較死板,在登入的時候我們甚至還得去判斷客戶到底是用什麼去做登入的,然後去分別呼叫不同第三方平臺的認證方式。

我們接下來演化開始用介面卡。如下程式碼:
首先我們定義出一個標準的適配介面:

public interface LoginAdapter {
    boolean support(Object adapter);
    ApiResponse login(String id,Object adapter);
}

根據上面我們看到,我們有QQ方式登入,有微信方式登入,有電話驗證碼方式登入。所以我們對應的就應該有相關的這些方式的介面卡的實現。由於程式碼重複,所以在此老貓就寫QQ和微信這兩種虛擬碼,其他的暫時先偷個懶。

/**
 * @author 公眾號:程式設計師老貓
 * @date 2024/3/3 22:47
 */
public class LoginForQQAdapter implements LoginAdapter {
    @Override
    public boolean support(Object adapter) {
        return adapter instanceof LoginForQQAdapter;
    }

    @Override
    public ApiResponse login(String id, Object adapter) {
        return null;
    }
}

public class LoginForWeChatAdapter implements LoginAdapter {
    @Override
    public boolean support(Object adapter) {
        return adapter instanceof LoginForWeChatAdapter;
    }

    @Override
    public ApiResponse login(String id, Object adapter) {
        return null;
    }
}

有了這些介面卡之後,我們就統一對外給出去介面:


public interface IPassportForThird {
    ApiResponse loginForQQ(String openId);

    ApiResponse loginForWechat(String openId);

    ApiResponse<String> loginForRegist(String userName, String password);
}

最後建立統一介面卡。

@Slf4j
public class PassportForThirdAdapter extends UserLoginService implements IPassportForThird{
    @Override
    public ApiResponse loginForQQ(String openId) {
        return doLogin(openId,LoginForQQAdapter.class);
    }

    @Override
    public ApiResponse loginForWechat(String openId) {
        return doLogin(openId,LoginForWeChatAdapter.class);
    }

  
    @Override
    public ApiResponse<String> loginForRegist(String userName, String password) {
        super.login(userName, password);
        return super.login(userName, password);
    }
    
    //用到簡單工廠模式以及策略模式
    private ApiResponse doLogin(String openId,Class<? extends LoginAdapter> clazz) {
        try {
            LoginAdapter adapter = clazz.newInstance();
            if(adapter.support(adapter)){
                return adapter.login(openId,adapter);
            }
        }catch (Exception e) {
            log.error("exception is",e);
        }
        return null;
    }
}

最終我們看一下實現的類圖:

介面卡結構圖

上述我們就用了介面卡的模式簡單重構了現有的第三方登入的程式碼,當然上述可能還存在一些程式碼的缺陷,大家也不要太過較真,在此給大家在日常開發中多點思路。

大家可能會對每個介面卡的support()方法有點疑問,用來決斷相容。這裡support()方法的引數也是Object型別的,而support()方法來自介面。介面卡的實現並不依賴介面,其實我們也可以直接將LoginAdapter移除。

在上述重構的例子中,其實咱們不僅僅用到了介面卡模式,其實還用到了簡單工廠模式的特性。

總結

其實在我們日常的開發中,介面卡模式是比較常用的一種設計模式,不僅僅使用上述場景,其實在很多其他api的對接的場景也有適用。例如,在電商業務場景中會涉及到各種對接,說到買賣就會牽扯到供應商的對接,第三方分銷渠道客戶的對接,其中必然涉及模型不一致需要適配轉換的場景,比如供應商商品資訊和標準商城商品資訊等等。當然老貓在此也只是做了一下簡單羅列。希望大家在後面的工作中可以參考用到。

相關文章