真香定律!我用這種模式重構(gòu)了第三方登錄
老貓的設(shè)計(jì)模式專欄已經(jīng)偷偷發(fā)車了。不甘愿做crud boy?看了好幾遍的設(shè)計(jì)模式還記不?。磕蔷筒灰桃庥浟?,跟上老貓的步伐,在一個(gè)個(gè)有趣的職場(chǎng)故事中領(lǐng)悟設(shè)計(jì)模式的精髓吧。還等什么?趕緊上車吧。
一、故事
辦公室里,小貓托著腮幫對(duì)著電腦陷入了思考。就在剛剛,他接到了領(lǐng)導(dǎo)指派的一個(gè)任務(wù),業(yè)務(wù)調(diào)整,登錄方式要進(jìn)行拓展。例如需要接入第三方的微信登錄,企業(yè)微信授權(quán)登錄等等。
原因大概是這樣,現(xiàn)在大環(huán)境不好,原來(lái)面向B端企業(yè)員工的電商業(yè)務(wù)并不好做,新客拓展比較困難,業(yè)務(wù)想要有更好的起色著實(shí)比較困難,所以決策層決定要把登錄的口子放開,原來(lái)支持手機(jī)密碼登錄以及手機(jī)驗(yàn)證碼進(jìn)行登錄,現(xiàn)在為了更好地推廣,需要支持微信掃碼關(guān)注企業(yè)公眾號(hào)后登錄,企業(yè)微信,微博等等一些列的第三方登錄模式。
說(shuō)白了未來(lái)到底會(huì)有多少種登錄方式不得而知,那么面對(duì)這樣一個(gè)棘手的問(wèn)題,小貓又該何去何從?
二、概述
登錄問(wèn)題相信后端小伙伴都有接觸過(guò),最簡(jiǎn)單的可能就是做一個(gè)權(quán)限系統(tǒng)就會(huì)用到登錄名+密碼+驗(yàn)證碼進(jìn)行登錄,繼而稍微復(fù)雜一些可能會(huì)涉及手機(jī)驗(yàn)證碼登錄?,F(xiàn)在隨著第三方平臺(tái)的層出不窮,我們很多網(wǎng)站其實(shí)都提供了聯(lián)合登錄。用戶掏出手機(jī)簡(jiǎn)單地一個(gè)掃碼動(dòng)作即可完成初步的注冊(cè)登錄功能。這種方式一定程度上能夠給當(dāng)前的網(wǎng)站帶來(lái)更多的流量。
關(guān)于小貓遇到的問(wèn)題,咱們嘗試從下面幾個(gè)點(diǎn)去解決。
概要
三、登錄演化
聊到登錄,我們首先去了解一下整個(gè)登錄認(rèn)證的發(fā)展階段,以及目前比較常見也相對(duì)比較復(fù)雜的微信公眾號(hào)授權(quán)登錄流程。
1.基于Cookie/Session進(jìn)行驗(yàn)證登錄
在早期,也就是可能是單體系統(tǒng)的時(shí)代,亦或者站在java開發(fā)者角度來(lái)說(shuō)是jsp時(shí)代的時(shí)候,我們用的登錄方式就是Cookie/Session驗(yàn)證的方式。關(guān)于Cookie以及Session相信很多后端的小伙伴都應(yīng)該知道,當(dāng)然若真有不清楚的,大家可以自己查閱一下相關(guān)資料。利用這種方式登錄的流程其實(shí)還是比較簡(jiǎn)單的,如下流程:
session登入
基于上述登錄成功后,服務(wù)端將用戶的身份信息存儲(chǔ)在Session里,并將session ID通過(guò)cookie傳遞給客戶端。后續(xù)的數(shù)據(jù)請(qǐng)求都會(huì)帶上cookie,服 務(wù)端根據(jù)cookie中攜帶的session id來(lái)得到辨別用戶身份。
簡(jiǎn)單的java偽代碼如下:
...
session.setAtrrbuite("user",user);
...
session.getAttrbuite("user");
當(dāng)然上述的偽代碼還是基于最最原始的寫法去寫的,關(guān)于這種登錄的框架,其實(shí)目前市面上也有比較成熟的,例如輕量級(jí)的shiro,spring本身自帶的權(quán)限認(rèn)證框架也有。
隨著業(yè)務(wù)的發(fā)展,系統(tǒng)訪問(wèn)量級(jí)的增大,我們漸漸發(fā)現(xiàn)這種方式存在著一些問(wèn)題:
- 由于服務(wù)端需要對(duì)接大量的客戶端,也就需要存放大量的Seesion ID,這樣就會(huì)導(dǎo)致服務(wù)器壓力過(guò)大。如果服務(wù)器是個(gè)集群,為了同步登錄的狀態(tài),需要將Session ID同步到每一臺(tái)服務(wù)器上,無(wú)形中增加了服務(wù)器端的維護(hù)成本。
- 由于Session ID存放在Cookie中,所以無(wú)法避免CSRF攻擊(跨站請(qǐng)求偽造)。
當(dāng)然其他問(wèn)題也歡迎小伙伴們進(jìn)行補(bǔ)充。為了解決這一系列的問(wèn)題,我們漸漸演化出了另外一種登錄認(rèn)證方式————基于token進(jìn)行認(rèn)證登錄。
2.基于TOKEN進(jìn)行認(rèn)證登錄
現(xiàn)在的系統(tǒng)大部分都是前后端分離開發(fā)的。后端大多使用了WEB API,此時(shí)token無(wú)疑是處理認(rèn)證的最好方式。
Session 方案中用戶信息(以Session記錄形式)存儲(chǔ)在服務(wù)端。而Token方案中(以Token形式)存儲(chǔ)在客戶端,服務(wù)端僅驗(yàn)證Token合法性即可。基于Token的身份驗(yàn)證是無(wú)狀態(tài)的,不將用戶信息存在服務(wù)器中。這種概念解決了在服務(wù)端存儲(chǔ)信息時(shí)的許多問(wèn)題。NoSession意味著咱們的程序可以根據(jù)需要去增減機(jī)器,而不用去擔(dān)心用戶是否登錄。
咱們一起來(lái)看一下如果使用TOKEN整個(gè)流程。
token機(jī)制
關(guān)于上述token機(jī)制的特點(diǎn)有以下幾點(diǎn):
- 無(wú)狀態(tài)、可擴(kuò)展:在客戶端存儲(chǔ)的Token是無(wú)狀態(tài)的,并且能夠被擴(kuò)展?;谶@種無(wú)狀態(tài)的和不存儲(chǔ)Session信息,所以不會(huì)對(duì)服務(wù)器端造成壓力,負(fù)載均衡器能夠?qū)⒂脩粜畔囊粋€(gè)服務(wù)器傳到其他服務(wù)器上,即使是服務(wù)器集群,也不需要增加維護(hù)成本。
- 可擴(kuò)展性:Tokens能夠創(chuàng)建與其它程序共享權(quán)限的程序。(即,我們所說(shuō)的第三方平臺(tái)聯(lián)合登錄的時(shí)候,token的生成機(jī)制以及驗(yàn)證可以由第三方系統(tǒng)進(jìn)行聯(lián)合驗(yàn)證登錄)
- 安全性:請(qǐng)求中發(fā)送Token而不是發(fā)送Cookie,能夠防止CSRF(跨站請(qǐng)求偽造)。即客戶端使用Cookie存儲(chǔ)了Tooken,Cookie也僅僅是一個(gè)存儲(chǔ)機(jī)制而不是用于認(rèn)證。不將信息存儲(chǔ)在Session中,讓我們少了對(duì)Session的操作。Token也可以存放在前端任何地方,可以不用保存在Cookie中,提升了頁(yè)面的安全性。Token是會(huì)失效的,一段時(shí)間之后用戶需要重新驗(yàn)證。
- 多平臺(tái)跨域:對(duì)應(yīng)用程序和服務(wù)進(jìn)行擴(kuò)展的時(shí)候,需要介入各種各種的設(shè)備和應(yīng)用程序。只要用戶有一個(gè)通過(guò)了驗(yàn)證的token,數(shù)據(jù)和資源就能夠在任何域上被請(qǐng)求到。
3.微信掃碼跳轉(zhuǎn)公眾號(hào)認(rèn)證登錄
這也是后續(xù)小貓遇到的問(wèn)題,以及需要和其他第三方Api主要對(duì)接的。其實(shí)關(guān)于掃碼認(rèn)證登錄也是基于token機(jī)制的一種拓展。只不過(guò)第三方的平臺(tái)在token機(jī)制上新增了獲取二維碼進(jìn)行二次確認(rèn)的過(guò)程。咱們以微信掃碼跳轉(zhuǎn)公眾號(hào)登錄為例來(lái)看一下整個(gè)流程。其他的第三方登錄流程其實(shí)也是大同小異,咱們了解一個(gè)流程即可,不同的平臺(tái)只是對(duì)接不同的api而已。
流程圖如下:
ticket機(jī)制
從上面這幅圖看到,掃碼登錄其實(shí)復(fù)雜就復(fù)雜在獲取token這個(gè)步驟上,當(dāng)獲取完畢token之后,其后續(xù)的業(yè)務(wù)邏輯其實(shí)基本也是一樣的。
其實(shí)其他第三方的登錄其實(shí)也是大同小異,最主要的難點(diǎn)是在如何獲取token上,我們只要認(rèn)真看完對(duì)接的api,其實(shí)問(wèn)題也基本都能迎刃而解。
說(shuō)明一下,老貓這里繪圖用了drawio工具,如果想要知道老貓的繪圖思路,大家可以看看這里《繪圖思路》
四、如何兼容多套?
看完上述之后,相信大家會(huì)對(duì)認(rèn)證登錄心里有桿秤了。細(xì)節(jié)方面其實(shí)只要去查詢相關(guān)平臺(tái)的api,然后去擼代碼就好了。但是實(shí)現(xiàn)一套倒是還好,但是現(xiàn)在小貓遇到的問(wèn)題是需要在原邏輯上去豐富登錄的代碼。如果在老的代碼上通過(guò)if else的方式去實(shí)現(xiàn)多套登錄邏輯,那估計(jì)后面又是屎山。
這里,其實(shí)我們可以引入“適配器設(shè)計(jì)模式”去解決這樣的問(wèn)題。
1.什么是適配器模式?
適配器模式(英文名:Adapter Pattern)是指將一個(gè)類的接口轉(zhuǎn)換成用戶期望的另一個(gè)接口,使得原本接口不兼容的類可以一起工作。
適配器模式可以分為兩類:對(duì)象適配器模式和類適配器模式。對(duì)象適配器模式通過(guò)組合實(shí)現(xiàn)適配,而類適配器模式則通過(guò)繼承實(shí)現(xiàn)適配。
此外,還有一種特殊的適配器模式——缺省適配器模式它由一個(gè)抽象類實(shí)現(xiàn),并在其中實(shí)現(xiàn)目標(biāo)接口中所規(guī)定的所有方法,但這些方法的實(shí)現(xiàn)通常是空方法,由具體的子類來(lái)實(shí)現(xiàn)具體的功能。適配器模式的應(yīng)用可以提高代碼的復(fù)用性和可維護(hù)性,同時(shí)幫助解決不同接口之間的兼容性問(wèn)題。
上面的概念比較抽象,其實(shí)在咱們的日常生活中也有這樣的例子,例如手機(jī)充電轉(zhuǎn)換頭,顯示器轉(zhuǎn)接頭等等。
2.適配器模式重構(gòu)第三方登錄
話不多說(shuō),直接開干,我們就針對(duì)小貓的遇到這個(gè)第三方登錄的場(chǎng)景,咱們用代碼重構(gòu)一把。(當(dāng)然,這里我們側(cè)重的還是偽代碼)。跟著老貓,咱們一步步走好代碼的演化。
咱們先看一下老的業(yè)務(wù)代碼,如下:
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;
}
}
接下來(lái)由于小貓的業(yè)務(wù)會(huì)發(fā)生變更,新的登錄方式會(huì)層出不窮,所以,我們得遵循之前提到的軟件設(shè)計(jì)原則去更好地寫一下業(yè)務(wù)代碼。我們遵循之前提到的開閉原則,于是我們邁出了重構(gòu)代碼的第一步,我們將創(chuàng)建一個(gè)新的第三方登錄的類來(lái)專門處理第三方的登錄對(duì)接。如下:
public class ThirdPartyUserLoginService extends UserLoginService {
public ApiResponse loginForQQ(String openId) {
/**
* openid 全局唯一,咱們直接作為用戶名
* 默認(rèn)密碼QQ_EMPTY
* 注冊(cè)(原來(lái)父類中有注冊(cè)實(shí)現(xiàn))
* 調(diào)用原來(lái)的登錄
*/
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);
}
}
寫到這里,其實(shí)咱們已經(jīng)集成了多種登錄方式的代碼兼容,但是這種實(shí)現(xiàn)方式顯然是不太優(yōu)雅的,看起來(lái)比較死板,在登錄的時(shí)候我們甚至還得去判斷客戶到底是用什么去做登錄的,然后去分別調(diào)用不同第三方平臺(tái)的認(rèn)證方式。
我們接下來(lái)演化開始用適配器。如下代碼:首先我們定義出一個(gè)標(biāo)準(zhǔn)的適配接口:
public interface LoginAdapter {
boolean support(Object adapter);
ApiResponse login(String id,Object adapter);
}
根據(jù)上面我們看到,我們有QQ方式登錄,有微信方式登錄,有電話驗(yàn)證碼方式登錄。所以我們對(duì)應(yīng)的就應(yīng)該有相關(guān)的這些方式的適配器的實(shí)現(xiàn)。由于代碼重復(fù),所以在此老貓就寫QQ和微信這兩種偽代碼,其他的暫時(shí)先偷個(gè)懶。
/**
* @author 公眾號(hào):程序員老貓
* @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;
}
}
有了這些適配器之后,我們就統(tǒng)一對(duì)外給出去接口:
public interface IPassportForThird {
ApiResponse loginForQQ(String openId);
ApiResponse loginForWechat(String openId);
ApiResponse<String> loginForRegist(String userName, String password);
}
最后創(chuàng)建統(tǒng)一適配器。
@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);
}
//用到簡(jiǎn)單工廠模式以及策略模式
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;
}
}
最終我們看一下實(shí)現(xiàn)的類圖:
適配器結(jié)構(gòu)圖
上述我們就用了適配器的模式簡(jiǎn)單重構(gòu)了現(xiàn)有的第三方登錄的代碼,當(dāng)然上述可能還存在一些代碼的缺陷,大家也不要太過(guò)較真,在此給大家在日常開發(fā)中多點(diǎn)思路。
大家可能會(huì)對(duì)每個(gè)適配器的support()方法有點(diǎn)疑問(wèn),用來(lái)決斷兼容。這里support()方法的參數(shù)也是Object類型的,而support()方法來(lái)自接口。適配器的實(shí)現(xiàn)并不依賴接口,其實(shí)我們也可以直接將LoginAdapter移除。
在上述重構(gòu)的例子中,其實(shí)咱們不僅僅用到了適配器模式,其實(shí)還用到了簡(jiǎn)單工廠模式的特性。
五、總結(jié)
其實(shí)在我們?nèi)粘5拈_發(fā)中,適配器模式是比較常用的一種設(shè)計(jì)模式,不僅僅使用上述場(chǎng)景,其實(shí)在很多其他api的對(duì)接的場(chǎng)景也有適用。例如,在電商業(yè)務(wù)場(chǎng)景中會(huì)涉及到各種對(duì)接,說(shuō)到買賣就會(huì)牽扯到供應(yīng)商的對(duì)接,第三方分銷渠道客戶的對(duì)接,其中必然涉及模型不一致需要適配轉(zhuǎn)換的場(chǎng)景,比如供應(yīng)商商品信息和標(biāo)準(zhǔn)商城商品信息等等。當(dāng)然老貓?jiān)诖艘仓皇亲隽艘幌潞?jiǎn)單羅列。希望大家在后面的工作中可以參考用到。