李毓麗 陳泰宏

摘要:為了提供給Web應用以及Web開發者一種即時消息通知的解決方案,該設計主要以當下流行的vuejs+Laravel+Mysql的技術組合方式,將前后端完全分離,降低開發的耦合性,有利于多階段的部署。并在此基礎上,白定義了一套令牌校驗機制以及利用swoole下的websocket接口實現消息通知功能,并且使用redis提高性能。
關鍵詞:redis;websocket;自定義token;消息通知
中圖分類號:TP311 文獻標識碼:A
文章編號:1009-3044(2020)20-0084-03
1背景
一個成熟的Web應用必定少不了消息通知。例如商城中的到貨提醒功能,又如社交類型的網站,當用戶A關注了用戶B,或者用戶C收藏了用戶D的話題,系統會定時發送更新的消息,去提醒對應的用戶,所以,消息通知在當下的WEB應用中是非常重要的組成部分。
2消息通知系統的設計
消息通知系統是以用戶為單位來進行實現,主要的技術棧為前端采用vue2.x,利用vue-cli創建項目,是一個單頁Web應用,也就是我們說的SPA應用。SPA應用有許多的優點,良好的交互體驗,用戶不需要頻繁的刷新頁面,通過Ajax從后臺瀆取數據,利用vuex對數據進行管理。良好的前后端分離開發方式,讓單頁Web應用可以和RESTful規約一起使用,通過RESTAPI提供接口數據,并使用Ajax獲取數據,這樣有助于分離客戶端和服務器端工作。在客戶端也可以分解為靜態頁面和頁面數據響應兩個部分。最重要的是可以減輕服務器壓力,服務器只需要傳輸數據以及對一些存在安全隱患的數據進行驗證,不用管展示邏輯和頁面合成,吞吐能力會提高幾倍。但SPA單頁應用也有一個比較大的缺點,就是SEO難度較高,所以在優化期間會將頁面修改成服務器渲染的方式。
后端采用的是PHP的框架Laravel,Laravel框架提供了許多軟件包,Laravel有一個好處就是倉庫非常多,加上有compos-er這一包管理器,使得其拓展性非常大。包括前端的一些組件包以及后臺的一些插件。由于我們采用的是前后端完全分離這一模式,會存在跨域問題,因此使用了laravel-cors這一插件包,解決跨域問題。
根據需求,主要分為三部分,第一部分是用戶的登錄機制的實現。登錄采用前后分離的方式,這就需要令牌token的驗證,利用自定義實現的令牌機制,在登錄注冊這一功能上,根據OAUTH2.0協議,對用戶的登錄進行加密傳遞,通過用戶提供的用戶名密碼認證成功后,傳遞回來的access_token去獲取用戶數據。同時,還集成了短信登錄(阿里云短信接口)和微博登錄(微博開放平臺),方便用戶登錄,更好的拓展用戶量。
第二部分是基于用戶登錄完成后的消息通知系統,當用戶登錄成功后,系統會自動連接上消息系統,本部分的功能基于PHP異步框架swoole的實現,通過swoole底層的封裝,達成對websocket協議的支持,從而使得用戶能夠作為長鏈接穩定的接受系統通知消息。
第三部分是基于Redis的以用戶為單位的域分離隊列消息推送。消息通知系統中少不了訂閱推送的功能,當系統中的管理員,想給所有的用戶推送一條消息,假設平臺有10萬個用戶,如果按照原來的思路來實現,那么就意味著每向所有用戶推送一條消息,則會通過消息表,向消息表中增加10萬條數據,這樣做無疑會對數據庫的性能造成非常大的影響,會對服務器造成很大的壓力。而利用redis,則可以實現只插入一條數據,通知所有用戶,并對用戶的其他消息不造成影響。
3基于JWT標準與HMACSHA256+base64加密規則實現的自定義token機制
Laravel框架白帶了一個實現OAUTH2.0協議和JWT標準的擴展包,名為Laravel-passport,該擴展包將token和Refresh-Token存于數據庫。并且安全性等各方面也已經做得比較完善,但是缺點也很明顯,復雜的實現邏輯,繁重的trait,還有麻煩的將token記錄到數據庫(這樣無異于session)這些特點,都使得這個擴展包性能變得非常緩慢,并且很明顯,擴展性以及對外友好性不強,耦合性太高(只適用于Laravel框架)。因此實現一個自定義的token機制。
3.1基于JWT標準的token定義
根據JWT標準,token被拆分為三部分:頭部、載荷、簽名。頭部中存著實現簽名的算法規則,以及實現方式,如下面代碼所示:
private statiC $header= array(
'alg'=>'HS256',//生成signature的算法
'typ'=>'JWT'//類型
);
payload主要存著我們需要的數據內容,叫作jwt載荷,需要幾個基本的字段,用于在后期的解析中使用,其中包括用戶idtoken的頒發時間和過期時間,以及jti(該token的唯一標識)等。payload是token中最主要的部分。下面是定義access_token和refresh_token的payload的實現。
accesstoken:
$data= array(
'iss'=>self::loadConf($iss)['secretuser'],//該JWT的簽發者
'iat'=> time(),//簽發時間
'exp'=> time()+ $exp,//過期時間
'nbf'=> time0+ 10,//該時間之前不接收處理該Token
'uid'=>$uid.
'type'=>'access',
'refresh_id'=>$refresh_id,
'jti'=> $jti//該token的id);
refresh_token:
$data= array(
'iss'=>self::loadConf($iss)['secret_user'],//該JWT的簽發者
'iat'=> time(),//簽發時間
'exp'=> time()+ $exp,//過期時間
'nbf'=> time()+ 60,//該時間之前不接收處理該Token
'uid'=>$uid.
'type'=>'refresh',
'jti'=>$jti);
第三部分是簽名部分,簽名部分是由一個白定義的隨機字符串組成的key,由這個key,對前面兩部分進行base64編碼和HMACSHA256加密形成簽名。這一部分是確保token安全性的必須內容。生成token代碼:
public static function getToken(array $payload, string $key)
{if(is_array($payload)){
$base64header=self:: base64UrIEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
$base64payload=self:: base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
retum $base64header.'.'.$base64payload.'.'.self::signature($base64header.'.'.$base64payload, $key, self'::$header['alg']);
}else{
return false;
}}
當生成token之后,再通過指定的key,將token解析,當校對無誤后,解析出完整的載荷內容,當解析正確,中間件將認為這是一個允許訪問的請求,則可以進入控制器,執行相應的功能邏輯。為了給不同的用戶定制不同的配置,例如管理員to-ken和用戶token的secret key肯定不能相同,或者過期時間等配置,所以會獨立出來,以使用者為單位進行配置,配置文件如下所示:
/*用戶*/
'user'=>[
'secret_key'=>'vhjcUExrBL5q6kWW"',//Str:: random()生成
'secret user'=>'Jasper_User',
'access_token_expire_time'=>7200.//access_token過期時間
'ref'resh_token_expire_time'=>86400.//refresh_token過期時間
],
/*管理員*/
'admin'=>[
'seCret_key'=>'IlzzMTZAX6tycbkM',//Str::random()生成
'seCret user'=>'Jasper_Admin',
'accesstoken_expiretime'=>84600,//access_token過期時間
'refresh_token_expire_time'=>86400.//refresh_token過期時間
],
對前臺用戶的中間件攔截請求如下面代碼所示,對于管理員的攔截也是類似的寫法,開發者只需要簡單的修改一下使用者的身份并且在配置文件配置好,則可以實現不同的秘鑰。這樣很好的解決了耦合性的問題,使開發者很方便的就能夠使用不同的機制開發其他不同類型的功能。
$token= SerAuth::getFinalToken($Auth);
if (!$token) return response(retumAPI(6000));
$result=SerAuth::verifyToken($token);
if(! $result['token'])
retum resp.nse(returnAPl($result['code']));
$info= $result['payload']['uid'];
$request->payload= $info;
return $next($request);
4基于websocket協議和swoole的消息通知系統實現
系統的實現較為復雜,采用點對多的方式。管理員給用戶發送一條消息,消息將會通過某個控制器的方法,調用封裝好的發送消息的service方法,將消息發送websocket服務器。該ServiCe作為一個短暫的客戶端,將信息轉發給webs。cket服務器之后,立即斷開,因為該連接不需要進行長連接占用資源,它只負責講消息發送到服務器。再由已經連接上websocket的前臺客戶端,接收服務器所發送的消息。
獲取信息的內容,并將信息入庫。然后利用composer中的websocket建立一個短鏈接客戶端,鏈接到服務器,將消息通知給對應的用戶。當管理員發送消息請求時候,需要將消息的主體入庫,以此來達到同步數據的目的。同步數據是為了準確的提高用戶了解未讀信息的數量。當管理員發起一條消息,消息表入庫,未讀消息則加l,在客戶端的store倉庫中,會將用戶顯示的未讀消息數量增1,所以當用戶刷新或者退出登錄狀態時,即使收不到及時信息,但是消息表中依然有記錄,等待用戶登錄的時候,依舊能夠看到所有的信息。這就完成了一整套的消息通知流程。具體如下圖1所示。
5基于Redis緩存的數據讀取,減少直接讀取數據庫以及對數據庫的操作
本系統是基于websocket下的swoole框架來完成業務,配合Mysql數據庫的消息表進行消息的存取。其中的一個業務是管理員需要向平臺的所有用戶推送某一消息,例如商品上線通知等系統消息。那么,如果平臺有十萬甚至百萬個用戶,推送的數據量存入消息表就非常大。如果每發送一條消息,消息表都需要增加10萬條數據,那么這樣對數據庫性能和服務器要求無疑是非常大的。因此,我們可以通過緩存來實現全部推送。管理員向所有用戶推送信息,只需要在數據庫中加入一個字段“is_all",來判斷該消息是否是所有用戶的消息即可。在查詢某個用戶的消息時,會有兩部分,一是擁有自己的消息的數據,二是帶有“is_all”字段的消息。當用戶已讀消息時,正常消息會修改“is_read”字段標識為已讀,而對于“is_all,類型的消息,為了避免其他用戶的消息受到影響,則會需要用緩存將每個用戶的已讀消息存人,在數據查詢時忽略這些內容,即可完成已讀未讀區分。刪除功能也是如此。
經過多次的測試發現,這種消息通知入庫的方式可行,并可以極大程度的避免了數據庫中數據的冗余性,有利于系統高效的運行,減少了頻繁入庫的操作。
6結束語
論文主要論述了基于白定義token以用戶為單位的消息通知系統的設計與實現。重點闡述自定義token機制的實現,包括頭部、載荷、簽名。實現基于websocket協議和swoole的消息通知流程,以及消息通知入庫的方式,提高系統的性能和運行效率,
參考文獻:
[1] Vue CLI[EB/OLl.[2019-12-20l.https://cli.vuejs.org/zh/guitle/.
[2] larave16.0中文文檔[EB/OL].[2019-12-20].https://leamku.com/docs/laraveU6.0.
[3] Vanessa Wang.HTML5 WebSocket指南2018[M].北京:機械工業出版社,2014: 21-250.
【通聯編輯:謝媛媛】
收稿日期:2020-05-08
作者簡介:李毓麗( 1980-),女,廣東普寧人,副教授,碩士,主要從事網絡技術、網絡編程方向的教學研究。