Docker 容器優雅終止方案_包裝設計

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

原文鏈接:Docker 容器優雅終止方案

作為一名系統重啟工程師(SRE),你可能經常需要重啟容器,畢竟 Kubernetes 的優勢就是快速彈性伸縮和故障恢復,遇到問題先重啟容器再說,幾秒鐘即可恢復,實在不行再重啟系統,這就是系統重啟工程師的殺手鐧。然而現實並沒有理論上那麼美好,某些容器需要花費 10s 左右才能停止,這是為啥?有以下幾種可能性:

  1. 容器中的進程沒有收到 SIGTERM 信號。
  2. 容器中的進程收到了信號,但忽略了。
  3. 容器中應用的關閉時間確實就是這麼長。

對於第 3 種可能性我們無能為力,本文主要解決 1 和 2。

如果要構建一個新的 Docker 鏡像,肯定希望鏡像越小越好,這樣它的下載和啟動速度都很快,一般我們都會選擇一個瘦了身的操作系統(例如 AlpineBusybox 等)作為基礎鏡像。

問題就在這裏,這些基礎鏡像的 init 系統也被抹掉了,這就是問題的根源!

init 系統有以下幾個特點:

  • 它是系統的第一個進程,負責產生其他所有用戶進程。
  • init 以守護進程方式存在,是所有其他進程的祖先。
  • 它主要負責:
    • 啟動守護進程
    • 回收孤兒進程
    • 將操作系統信號轉發給子進程

1. Docker 容器停止過程

對於容器來說,init 系統不是必須的,當你通過命令 docker stop mycontainer 來停止容器時,docker CLI 會將 TERM 信號發送給 mycontainer 的 PID 為 1 的進程。

  • 如果 PID 1 是 init 進程 – 那麼 PID 1 會將 TERM 信號轉發給子進程,然後子進程開始關閉,最後容器終止。
  • 如果沒有 init 進程 – 那麼容器中的應用進程(Dockerfile 中的 ENTRYPOINTCMD 指定的應用)就是 PID 1,應用進程直接負責響應 TERM 信號。這時又分為兩種情況:
    • 應用不處理 SIGTERM – 如果應用沒有監聽 SIGTERM 信號,或者應用中沒有實現處理 SIGTERM 信號的邏輯,應用就不會停止,容器也不會終止。
    • 容器停止時間很長 – 運行命令 docker stop mycontainer 之後,Docker 會等待 10s,如果 10s 后容器還沒有終止,Docker 就會繞過容器應用直接向內核發送 SIGKILL,內核會強行殺死應用,從而終止容器。

2. 容器進程收不到 SIGTERM 信號?

如果容器中的進程沒有收到 SIGTERM 信號,很有可能是因為應用進程不是 PID 1,PID 1 是 shell,而應用進程只是 shell 的子進程。而 shell 不具備 init 系統的功能,也就不會將操作系統的信號轉發到子進程上,這也是容器中的應用沒有收到 SIGTERM 信號的常見原因。

問題的根源就來自 Dockerfile,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ./popcorn.sh

ENTRYPOINT 指令使用的是 shell 模式,這樣 Docker 就會把應用放到 shell 中運行,因此 shell 是 PID 1。

解決方案有以下幾種:

方案 1:使用 exec 模式的 ENTRYPOINT 指令

與其使用 shell 模式,不如使用 exec 模式,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ["./popcorn.sh"]

這樣 PID 1 就是 ./popcorn.sh,它將負責響應所有發送到容器的信號,至於 ./popcorn.sh 是否真的能捕捉到系統信號,那是另一回事。

舉個例子,假設使用上面的 Dockerfile 來構建鏡像,popcorn.sh 腳本每過一秒打印一次日期:

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

#!/bin/sh

while true
do
    date
    sleep 1
done

構建鏡像並創建容器:

 → docker build -t truek8s/popcorn .
 → docker run -it --name corny --rm truek8s/popcorn

打開另外一個終端執行停止容器的命令,並計時:

 → time docker stop corny

因為 popcorn.sh 並沒有實現捕獲和處理 SIGTERM 信號的邏輯,所以需要 10s 左右才能停止容器。要想解決這個問題,就要往腳本中添加信號處理代碼,讓它捕獲到 SIGTERM 信號時就終止進程:

#!/bin/sh

# catch the TERM signal and then exit
trap "exit" TERM

while true
do
    date
    sleep 1
done

注意:下面這條指令與 shell 模式的 ENTRYPOINT 指令是等效的:

ENTRYPOINT ["/bin/sh", "./popcorn.sh"]

方案 2:直接使用 exec 命令

如果你就想使用 shell 模式的 ENTRYPOINT 指令,也不是不可以,只需將啟動命令追加到 exec 後面即可,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT exec ./popcorn.sh

這樣 exec 就會將 shell 進程替換為 ./popcorn.sh 進程,PID 1 仍然是 ./popcorn.sh

方案 3:使用 init 系統

如果容器中的應用默認無法處理 SIGTERM 信號,又不能修改代碼,這時候方案 1 和 2 都行不通了,只能在容器中添加一個 init 系統。init 系統有很多種,這裏推薦使用 tini,它是專用於容器的輕量級 init 系統,使用方法也很簡單:

  1. 安裝 tini
  2. tini 設為容器的默認應用
  3. popcorn.sh 作為 tini 的參數

具體的 Dockerfile 如下:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "./popcorn.sh"]

現在 tini 就是 PID 1,它會將收到的系統信號轉發給子進程 popcorn.sh

如果你想直接通過 docker 命令來運行容器,可以直接通過參數 --init 來使用 tini,不需要在鏡像中安裝 tini。如果是 Kubernetes 就不行了,還得老老實實安裝 tini。

3. 使用 tini 后應用還需要處理 SIGTERM 嗎?

最後一個問題:如果移除 popcorn.sh 中對 SIGTERM 信號的處理邏輯,容器會在我們執行停止命令后立即終止嗎?

答案是肯定的。在 Linux 系統中,PID 1 和其他進程不太一樣,準確地說應該是 init 進程和其他進程不一樣,它不會執行與接收到的信號相關的默認動作,必須在代碼中明確實現捕獲處理 SIGTERM 信號的邏輯,方案 1 和 2 乾的就是這個事。

普通進程就簡單多了,只要它收到系統信號,就會執行與該信號相關的默認動作,不需要在代碼中显示實現邏輯,因此可以優雅終止。

Kubernetes 1.18.2 1.17.5 1.16.9 1.15.12離線安裝包發布地址http://store.lameleg.com ,歡迎體驗。 使用了最新的sealos v3.3.6版本。 作了主機名解析配置優化,lvscare 掛載/lib/module解決開機啟動ipvs加載問題, 修復lvscare社區netlink與3.10內核不兼容問題,sealos生成百年證書等特性。更多特性 https://github.com/fanux/sealos 。歡迎掃描下方的二維碼加入釘釘群 ,釘釘群已經集成sealos的機器人實時可以看到sealos的動態。

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

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

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

再給福克斯車主一次機會,他會買全新科魯茲嗎?_貨運

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

開起來怎麼樣。運動個性潮流指向。會選擇福克斯的車主,相信對操控性也有非常高的要求。張濤會對全新科魯茲的操控感給出什麼樣的評價也是最為好奇的一點。“全新科魯茲開起來非常輕盈,轉向靈敏,底盤調教得偏運動,但沒有失去舒適性,擁有一個有質感的底盤。

在1個月前曾經招募過幾位不同性格和不同車型的車主朋友,約他們來一場性能3.0時代中級車的試駕活動,當時邀請的朋友中也正好有一位福克斯資深車主,並且算是汽車界的老手,而福克斯作為一代人心目中代表着性能2.0時代的家轎,有着不可撼動的地位。那麼我們找到它的資深車主來試駕到底是no zuo no die還是打臉福克斯?

這次參加試駕的福克斯車主剛好就是大家所熟悉的帥哥張濤,作為一枚資深的福克斯前車主,他試駕完全新科魯茲后,在視頻裏面也表達了他的感受,立刻帶大家解密,請觀看以下視頻:

新外觀能否撼動車主固有審美?

視頻里這位福克斯資深車主張濤給予了全新科魯茲外觀極大的肯定:“全新科魯茲這一代的硬朗造型,從上一代繼承了下來,在這個看臉的時代,全新科魯茲把上一代的造型發揮得更淋漓盡致。”

在外觀這點上,和張濤的想法是一樣的。作為雪佛蘭品牌中最具運動特質的3C家族之一的全新科魯茲,繼承兄輩科爾維特以及科邁羅的力量感,在凌厲的線條為整車帶來肌肉感的同時,也恰如其分地採用了一些曲線讓它看起來更時尚。這樣的全新科魯茲顯然更容易抓住年輕一代的消費者的心。

車主大讚空間寬!

說到內部空間,張濤表示:“全新科魯茲的空間算是比福克斯好一點,橫向空間感很強,不會讓人有局促的感覺。”

本人也坐在後排感受過,確實在A級車裡面,全新科魯茲的空間表現可說是非常優秀,2700mm的軸距為此而貢獻了不少,腿部空間伸展綽綽有餘,所以這是和張濤都大為讚賞的空間設定。

配置上打動人 車主都認可的配置

當談到配置時,張濤也給出了非常正面的評價:“全新科魯茲擁有一些實用的配置,如胎壓監測,全系車型標配的啟停功能,最高配置擁有連福克斯頂配都沒有的座椅加熱和電動調節功能。”

認為胎壓檢測這個功能標配是要為全新科魯茲點贊的,作為在國外標配的一個安全型的配置,胎壓監測已經逐步受到市場和消費者的重視,

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

畢竟安全駕駛的重要性已經逐漸被國人所接受和認同。而座椅加熱和電動調節,自動啟停等功能也都是為了駕駛者貼心設計的,尤其是自動啟停功能更是大大降低了油耗和用車成本。據權威機構測試,此項技術的使用將使一輛普通轎車每年節省10%至15%的燃料。這點只能說雪佛蘭為消費者想得的確非常周到。

開起來怎麼樣?運動個性潮流指向?

會選擇福克斯的車主,相信對操控性也有非常高的要求。張濤會對全新科魯茲的操控感給出什麼樣的評價也是最為好奇的一點。

“全新科魯茲開起來非常輕盈,轉向靈敏,底盤調教得偏運動,但沒有失去舒適性,擁有一個有質感的底盤。”

也開過這台車,要說全新科魯茲標志著性能車3.0時代的來臨,確實沒有誇張,高速段開到120公里車身仍然保持很穩的狀態,變道時方向盤的轉向很精準,與一些虛位較大的車型相比,簡直省心,而這真正給予了駕駛者更多的信心。若要給全新科魯茲打個分,可以給到93以上的高分,怕給滿分會驕傲,所以還是要留一些進步的空間。

總結:

大家看到最後,相信都了解張濤對這台全新科魯茲給出了非常不錯的評價,而也很看好全新科魯茲的市場表現,即使最近全新福克斯也上市了不久,但是在空間上仍然是全新科魯茲佔優,並且綜合工況油耗也是全新科魯茲更省,而且全系標配的胎壓監測裝置等對於一些車主選車來說,都將是決定性因素。

(私底下和張濤深入溝通過,對於他來說,開了汽車那麼多年,從他的角度看,本次全新科魯茲試駕確實給到了他一些駕駛上的新鮮感,對於雪佛蘭造車,也顛覆了他對家轎的一些舊有觀念,感受到了全新科魯茲的一些配置驚喜,是一次全新的駕駛體驗。)

在體驗為先的性能3.0時代里,能否給消費者帶來真正的全方位用戶體驗才是制勝關鍵。對比起上一代還停留在標榜數據的性能2.0時代的福克斯,全新科魯茲已經不單單隻是冰冷的駕駛机械。這一天下來的駕駛,讓這位資深福克斯車主全面接觸這台中級車,雪佛蘭全新科魯茲在這位資深福克斯車主的心目中相信已經留下了不少深刻的印象。回到我們的問題:再給福克斯車主一次機會,他會買全新科魯茲嗎?答案似乎已經不言而喻。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

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

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

豐田86換裝法拉利引擎成功! 先漂兩圈試試吧_網頁設計公司

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

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

而最霸氣的就是那台突出的法拉利發動機,因為體積較大,所以發動機比車頭還要高一點,竟有點美式V8肌肉車的感覺。目前還不知這輛86輸出功率是多少,但經過強化之後,我猜動力絕對不會少於600馬力。因為本身是一位漂移車手,所以改好之後當然要拿來漂一漂活動下啦。

前陣子,我們就推送過一個美國漂移車手Ryan Tuerck把一台法拉利458的法動機塞到一輛豐田86上,現在這輛86也已經到了完工調試階段,今天就讓大家看看吧。

這輛86基本就只剩個車身,塞進法拉利458的4.5L V8發動機之後,整輛車也根據實際需要進行了很大程度的改造,

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

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

比如說冷卻系統後置,全車輕量化和車身強化,進口方式也改了,同時也用上了不少改裝品牌的套件。加上這亮眼的姨媽紅,只要看一眼你就會記住它。

而最霸氣的就是那台突出的法拉利發動機,因為體積較大,所以發動機比車頭還要高一點,竟有點美式V8肌肉車的感覺 。目前還不知這輛86輸出功率是多少,但經過強化之後,我猜動力絕對不會少於600馬力。

因為本身是一位漂移車手,所以改好之後當然要拿來漂一漂活動下啦。這輛4586的聲浪還真是不錯,那台458的發動機依然是最晃眼的,不知以後這輛車會不會拉去比賽,這樣就可以看到更多這輛4586的視頻啦。

另附之前還在施工的4586視頻

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

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

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

16萬買GS8低配 還是高配博越和RX5_包裝設計

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

但正當我們以為十六萬就可以充值中型SUV信仰時,作為實用主義者的,不得不告訴你,這個價格已經可以買到頂配的博越和次頂配的RX5了。同樣是自主品牌SUV,拿着十六萬元的你,到底該買哪一輛呢。

具有里程碑式意義的中型SUV傳祺GS8自推出后就受到不少人的關注,

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

而在其上市公布價格后,十六萬的入門價格的確讓不少人垂涎欲滴,畢竟以往這個尺寸的SUV售價都是動輒二十五萬的。但正當我們以為十六萬就可以充值中型SUV信仰時,作為實用主義者的,不得不告訴你,這個價格已經可以買到頂配的博越和次頂配的RX5了。同樣是自主品牌SUV,拿着十六萬元的你,到底該買哪一輛呢?

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

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

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

一帶一路又惹議 中國在克什米爾建水電廠引大規模示威_貨運

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

摘錄自2020年7月7日中央社報導

國際亞洲新聞社(ANI)等印度媒體今(7日)報導,巴基斯坦控制克什米爾首府穆薩法拉巴德(Muzaffarabad)居民昨天舉行示威活動,譴責中國財團在尼肋姆河和吉魯姆河興建尼肋姆-吉魯姆(Neelum-Jhelum)水力發電廠和柯哈拉(Kohala)水力發電廠工程是非法建設,且嚴重破壞當地環境生態。

當地居民指控,水力發電廠營運後,會把尼肋姆河90%的河水轉移到發電廠,導致養活穆薩法拉巴德50萬人口的尼肋姆河流量和水位下降,影響當地居民生計,且引發當地氣溫升高。

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

尼肋姆-吉魯姆水力發電廠和柯哈特水力發電廠興建項目,都是中國「一帶一路」下「中巴經濟走廊」的項目。其中,尼肋姆-吉魯姆水力發電廠從2008年開始興建,2018年8月完成;柯哈特水力發電廠工程則於2015年由中國長江三峽集團得標,於2018年起也引發當地居民抗議迄今。

生物多樣性
生態保育
國際新聞
中國新聞
巴基斯坦
水力發電廠
一帶一路
集水區

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

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

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

北韓暴雨成災糧倉泡湯 金正恩到場視察_網頁設計公司

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

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

摘錄自2020年8月7日中央社報導

北韓中央通信社(KCNA)報導,北韓領導人金正恩今天(7日)視察黃海北道(North Hwanghae)水患災情,並下令提供糧食和補給品給災民。

法新社報導,北韓部分地區已經連日降下傾盆大雨。北韓許多山地和丘陵地區植被長期以來遭到破壞,使得水流恣意順著山勢往下流。

平壤以南的黃海北道是北韓重要糧倉。

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

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

北韓中央通信社說,銀波郡(Unpha County)大青里(Taechong-ri)一帶的堤防潰堤,「導致730餘棟平房和600餘公頃稻田泡湯,179棟住宅被摧毀」。

北韓中央通信社指出,金正恩表示,儘速供應糧食、藥物和基本民生必需品給水患地區災民是當務之急。

國際新聞
北韓
暴雨

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

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

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

真相難辨 日本政府涉放射線不實教材 15萬公民連署要求撤回_包裝設計

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

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

SpringSecurity(1)—認證+授權代碼實現_貨運

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

認證+授權代碼實現

Spring Security是 一種基於 Spring AOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時在 Web 請求級和方法調用級處理身份確認和授權。

有關認證和授權的理論知識,之前有寫過相關博客。了解權限管理

一、SpringSceurity工作流程

網上找一張圖,覺得畫的挺好的,比較容易理解。不然換的是源碼流程圖很難去理解。

圖片地址 : 地址 可以單機放大看更加清楚

要想理解這張圖建議看下這篇博客,因為這張圖中需要自定義的My…類,在文章中都有說明,所以更好理解點。

Spring Boot Security 詳解

二、認證+授權代碼

這裏只展示一些核心代碼,具體完整代碼放在github上。

1、UserDetails接口

Security 中的用戶接口,我們自定義用戶類要實現該接口, 用於向security中注入當前用戶的姓名密碼,和擁有的角色。同時也包含一些其它信息,比如當前用戶是否過期,

賬號是否鎖定等等。

自己定義User實現這個接口

public class User implements UserDetails {
    private String username;
    private String password;
    private List<Role> roles;
    /**
     * 獲取用戶名
     */
    @Override
    public String getUsername() {
        return username;
    }
    /**
     * 獲取密碼
     */
    @Override
    public String getPassword() {
        return password;
    }
    /**
     * 用戶的權限集, 默認需要添加ROLE_ 前綴
     */
    @Override
    @JsonIgnore
    public List<GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        }
        return authorities;
    }
    /**
     * 賬戶是否過期
     */
    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }
    /**
     * 賬戶是否鎖定
     */
    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }
    /**
     * 憑證是否過期
     */
    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 用戶是否可用
     */
    @Override
    public boolean isEnabled() {
        return true;
    }  
}

2、UserDetailsService

Security 中的用戶 Service,自定義用戶服務類需要實現該接口。這個接口只有一個方法需要我們去實現,那就是通過用戶名去獲取用戶信息。這裏也是和數據庫交互獲取

用戶認證和授權信息的地方。

@Service
@Slf4j
public class UserService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //TODO 正常應該查詢數據庫獲取用戶和用戶的權限
//        User user = userMapper.loadUserByUsername(userName);
//        List<Role> roles = rolesMapper.getRolesByUid(user.getId());
//        user.setRoles(roles);
        log.info("登陸用戶名: {}", userName);
        //通過用戶名查詢到的密碼 密碼肯定是加密過的 這裏明文密碼是 123456
        String password = "e10adc3949ba59abbe56e057f20f883e";
        //用戶對應權限
        List<Role> roles = Lists.newArrayList(new Role(1L, "教師"), new Role(2L, "學生"));
        User user = new User(userName, password, roles);
        return user;
    }
}

注意 這裏的明文密碼是 123456,也就是用戶輸入這個才能完成認證。授權的話當前用戶有兩個角色 教師學生。在下面測試的時候會用到。

3、WebSecurityConfigurerAdapter

它是Spring Security的Java 配置類。創建類SecurityConfiguration繼承 WebSecurityConfigurerAdapter,來對我們應用中所有的安全相關的事項(

所有url,驗證用戶名密碼,表單重定向等)進行控制。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 1、配置的是認證信息, AuthenticationManagerBuilder 這個類,就是AuthenticationManager的建造者, 我們只需要向這個類中, 配置用戶信息,
     *    就能生成對應的AuthenticationManager, 這個類也提過,是用戶身份的管理者, 是認證的入口, 因此,我們需要通過這個配置,想security提供真實的用戶身份。
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    }
    /**
     * 2、配置Security的認證策略, 每個模塊配置使用and結尾。這個也是最複雜的
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    }
    /**
     * 3、這個配置方法用於配置靜態資源的處理方式,可使用 Ant 匹配規則。就是可以不用認證就可以直接訪問的接口
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
    }
}

完整示例

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    /**
     * 密碼驗證器
     */
    @Autowired
    private PassWordEncorder passWordEncorder;
    /**
     * 成功處理器
     */
    @Autowired
    private AuthenctiationSuccessHandler authenctiationSuccessHandler;

    /**
     * 失敗處理器
     */
   @Autowired
   private AuthenctiationFailHandler authenctiationFailHandler;
   /**
    * 向Security注入用戶信息
    */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passWordEncorder);
    }
    /**
     * 配置規則
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //開啟登陸配置
        http.authorizeRequests()
                // 登錄之後就能訪問
                .antMatchers("/no-authorize").authenticated()
                // 登陸后 需要校長角色權限
                .antMatchers("/need-authorize").hasRole("校長")
                // 其他的路徑都是登錄后即可訪問
                .anyRequest().authenticated()
                .and().formLogin()
                // 定義登錄頁面,未登錄時,訪問一個需要登錄之後才能訪問的接口,會自動跳轉到該頁面
                .loginPage("/login_page")
                //登錄成功的處理器
                .successHandler(authenctiationSuccessHandler)
                //登錄失敗的處理器
                .failureHandler(authenctiationFailHandler)
                // 登錄處理接口
                .loginProcessingUrl("/login")
                // 定義登錄時,用戶名的 key,默認為 username
                .usernameParameter("username")
                //定義登錄時,用戶密碼的 key,默認為 password
                .passwordParameter("password").permitAll()
                .and().logout()
                ////和表單登錄相關的接口統統都直接通過
                .permitAll()
                .and().csrf().disable().exceptionHandling().accessDeniedHandler(getAccessDeniedHandler());
    }

    /**
     * 對於/static/  下的路徑都不用認證
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/no-login");
    }

    /**
     * 用戶未認證異常攔截
     */
    @Bean
    AccessDeniedHandler getAccessDeniedHandler() {
        return new AuthenticationAccessDeniedHandler();
    }
}

注意 這裏一共配置了三個路徑用於測試。

1、/no-login 接口不需要認證就可以直接訪問
2、/no-authorize 需要認證 但不需要授權就可以訪問
3、/need-authorize 首先需要認證 認證通過還需要授權 這裏需要校長的角色才可以訪問該接口 但是我們測試用戶只有教師和學生所以沒有權限訪問該接口

下面會針對這個個接口分別進行測試。

三、測試

1、接口提供

@RestController
public class TestController {

    /**
     * 1、不需要登陸就可以訪問
     */
    @RequestMapping(value = "/no-login")
    public ServiceResponse noLogin() {
        return ServiceResponse.success("歡迎訪問不需要登陸接口");
    }
    /**
     * 2、只登陸,不許認證接口
     */
    @RequestMapping(value = "/no-authorize")
    public ServiceResponse needAuthorize(){
        return ServiceResponse.success("登陸了 不用授權");
    }
    /**
     * 3、登陸 + 相關認證接口
     */
    @RequestMapping(value = "/need-authorize")
    public ServiceResponse noAuthorize() {
        return ServiceResponse.success("登陸+授權成功");
    }
    /**
     * @Description: 如果自動跳轉到這個頁面,說明用戶未登錄,返回相應的提示即可
     */
    @RequestMapping("/login_page")
    public ServiceResponse loginPage() {
        return  ServiceResponse.failure("001", "尚未登錄,請登錄!");
    }
}

2、未登錄訪問 no-login 和 no-authorize 接口

no-login接口

很明顯沒有登陸 請求該接口成功!

no-authorize接口

沒有登陸訪問失敗,在上面配置了如果用戶沒有認證的話跳轉到login_page接口,所以這裏返回 ‘尚未登錄,請登錄!’

3、登陸后訪問 no-authorize 和 need-authorize 接口

先登陸

根據上面配置登陸的路徑為 /login 請求參數包括 usernamepassword

注意 這裏需要post請求。

no-authorize 接口

登陸就可以訪問了。

need-authorize 接口

雖然登陸成功了,但是因為該接口需要校長角色,之前給該用戶只配置了教師和學生的角色所以訪問失敗。

參考

1、SpringSide 3 中的安全框架

2、Spring Security 工作原理概覽

3、Spring Boot Security 詳解 很贊

別人罵我胖,我會生氣,因為我心裏承認了我胖。別人說我矮,我就會覺得好笑,因為我心裏知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(17)

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

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

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

SpringBoot學習筆記(十五:OAuth2 )_網頁設計公司

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

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

@

目錄

  • 一、OAuth 簡介
    • 1、什麼是OAuth
    • 2、OAuth 角色
    • 3、OAuth 授權流程
    • 4、OAuth授權模式
      • 4.1、授權碼
      • 4.2、隱藏式
      • 4.3、密碼式
      • 4.4、憑證式
  • 二、實踐
    • 1、密碼模式
      • 1.1、授權服務器
        • 1.1.1、依賴
        • 1.1.2、授權服務器配置
        • 1.1.3、Spring Security配置
      • 1.2、資源服務器
        • 1.2.1、資源服務器配置
        • 1.2.2、資源服務接口
      • 1.3、測試
        • 1.3.1、獲取token
        • 1.3.2、使用獲取到的token訪問資源接口
    • 2、授權碼模式
      • 2.1、應用註冊
      • 2.2、具體代碼
      • 2.3、測試

一、OAuth 簡介

1、什麼是OAuth

開放授權(Open Authorization,OAuth)是一種資源提供商用於授權第三方應用代表資源所有者獲取有限訪問權限的授權機制。由於在整個授權過程中,第三方應用都無須觸及用戶的密碼就可以取得部分資源的使用權限,所以OAuth是安全開放的。

例如,用戶想通過 QQ 登錄csdn,這時csdn就是一個第三方應用,csdn要訪問用戶的一些基本信息就需要得到用戶的授權,如果用戶把自己的 QQ 用戶名和密碼告訴csdn,那麼csdn就能訪問用戶的所有數據,井且只有用戶修改密碼才能收回授權,這種授權方式安全隱患很大,如果使用 OAuth ,就能很好地解決這一問題。

OAuth第一個版本誕生於2007年12月,並於2010年4月正式被IETF作為標準發布(編號RFC 5849)。由於OAuth1.0複雜的簽名邏輯以及單一的授權流程存在較大缺陷,隨後標準工作組又推出了 OAuth2.0草案,並在2012年10月正式發布其標準(編號RFC 6749)。OAuth2.0放棄了OAuth1.0中讓開發者感到痛苦的数字簽名和加密方案,使用已經得到驗證並廣泛使用的HTTPS技術作為安全保障手 段。OAuth2.0與OAuth1.0互不兼容,由於OAuth1.0已經基本退出歷史舞台,所以下面提到的OAuth都是指OAuth2.0。

2、OAuth 角色

想要理解OAuth的運行流程,則必須要認識4個重要的角色。

  • Resource Owner:資源所有者,通常指用戶,例如每一個QQ用戶。
  • Resource Server:資源服務器,指存放用戶受保護資源的服務器,通常需要通過Access Token(訪問令牌)才能進行訪問。例如,存儲QQ用戶基本信息的服務器,充當的便是資源服務器的 角色。
  • Client:客戶端,指需要獲取用戶資源的第三方應用,如CSDN網站。
  • Authorization Server:授權服務器,用於驗證資源所有者,並在驗證成功之後向客戶端發放相關訪問令牌。

3、OAuth 授權流程

這是 個大致的流程,因為 OAuth2 中有 種不同的授權模式,每種授權模式的授權流程又會有差異,基本流程如下:

  • 客戶端(第三方應用)向資源所有者請求授權。
  • 服務端返回一個授權許可憑證給客戶端。
  • 客戶端拿着授權許可憑證去授權服務器申請令牌。
  • 授權服務器驗證信息無誤后,發放令牌給客戶端。
  • 客戶端拿着令牌去資源服務器訪問資源。
  • 資源服務器驗證令牌無誤后開放資源。

4、OAuth授權模式

OAuth 協議的授權模式共分為4種。

4.1、授權碼

授權碼(authorization code)方式,指的是第三方應用先申請一個授權碼,然後再用該碼獲取令牌。

這種方式是最常用的流程,安全性也最高,它適用於那些有後端的 Web 應用。授權碼通過前端傳送,令牌則是儲存在後端,而且所有與資源服務器的通信都在後端完成。這樣的前後端分離,可以避免令牌泄漏。

  • 第一步,A 網站提供一個鏈接,用戶點擊后就會跳轉到 B 網站,授權用戶數據給 A 網站使用。下面就是 A 網站跳轉 B 網站的一個示意鏈接。
https://b.com/oauth/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read

上面 URL 中,response_type參數表示要求返回授權碼(code),client_id參數讓 B 知道是誰在請求,redirect_uri參數是 B 接受或拒絕請求后的跳轉網址,scope參數表示要求的授權範圍(這裡是只讀)。

  • 第二步,用戶跳轉后,B 網站會要求用戶登錄,然後詢問是否同意給予 A 網站授權。用戶表示同意,這時 B 網站就會跳回redirect_uri參數指定的網址。跳轉時,會傳回一個授權碼,就像下面這樣。
https://a.com/callback?code=AUTHORIZATION_CODE

上面 URL 中,code參數就是授權碼。

  • 第三步,A 網站拿到授權碼以後,就可以在後端,向 B 網站請求令牌。
https://b.com/oauth/token?
 client_id=CLIENT_ID&
 client_secret=CLIENT_SECRET&
 grant_type=authorization_code&
 code=AUTHORIZATION_CODE&
 redirect_uri=CALLBACK_URL

上面 URL 中,client_id 參數和 client_secret 參數用來讓 B 確認 A 的身份(client_secret參數是保密的,因此只能在後端發請求),grant_type參數的值是 AUTHORIZATION_CODE,表示採用的授權方式是授權碼,code參數是上一步拿到的授權碼,redirect_uri 參數是令牌頒發后的回調網址。

  • 第四步,B 網站收到請求以後,就會頒發令牌。具體做法是向redirect_uri指定的網址,發送一段 JSON 數據。

    {    
      "access_token":"ACCESS_TOKEN",
      "token_type":"bearer",
      "expires_in":2592000,
      "refresh_token":"REFRESH_TOKEN",
      "scope":"read",
      "uid":100101,
      "info":{...}
    }

上面 JSON 數據中,access_token字段就是令牌,A 網站在後端拿到了。

4.2、隱藏式

有些 Web 應用是純前端應用,沒有後端。這時就不能用上面的方式了,必須將令牌儲存在前端。RFC 6749 就規定了第二種方式,允許直接向前端頒發令牌。這種方式沒有授權碼這个中間步驟,所以稱為(授權碼)”隱藏式”(implicit)。

  • 第一步,A 網站提供一個鏈接,要求用戶跳轉到 B 網站,授權用戶數據給 A 網站使用。
https://b.com/oauth/authorize?
  response_type=token&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read

上面 URL 中,response_type參數為token,表示要求直接返回令牌。

  • 第二步,用戶跳轉到 B 網站,登錄后同意給予 A 網站授權。這時,B 網站就會跳回redirect_uri參數指定的跳轉網址,並且把令牌作為 URL 參數,傳給 A 網站。
https://a.com/callback#token=ACCESS_TOKEN

上面 URL 中,token參數就是令牌,A 網站因此直接在前端拿到令牌。

注意,令牌的位置是 URL 錨點(fragment),而不是查詢字符串(querystring),這是因為 OAuth 2.0 允許跳轉網址是 HTTP 協議,因此存在”中間人攻擊”的風險,而瀏覽器跳轉時,錨點不會發到服務器,就減少了泄漏令牌的風險。

這種方式把令牌直接傳給前端,是很不安全的。因此,只能用於一些安全要求不高的場景,並且令牌的有效期必須非常短,通常就是會話期間(session)有效,瀏覽器關掉,令牌就失效了。

4.3、密碼式

如果你高度信任某個應用,RFC 6749 也允許用戶把用戶名和密碼,直接告訴該應用。該應用就使用你的密碼,申請令牌,這種方式稱為”密碼式”(password)。

  • 第一步,A 網站要求用戶提供 B 網站的用戶名和密碼。拿到以後,A 就直接向 B 請求令牌。
https://oauth.b.com/token?
  grant_type=password&
  username=USERNAME&
  password=PASSWORD&
  client_id=CLIENT_ID

上面 URL 中,grant_type參數是授權方式,這裏的password表示”密碼式”,username和password是 B 的用戶名和密碼。

  • 第二步,B 網站驗證身份通過後,直接給出令牌。注意,這時不需要跳轉,而是把令牌放在 JSON 數據裏面,作為 HTTP 回應,A 因此拿到令牌。

4.4、憑證式

最後一種方式是憑證式(client credentials),適用於沒有前端的命令行應用,即在命令行下請求令牌。

  • 第一步,A 應用在命令行向 B 發出請求。
https://oauth.b.com/token?
  grant_type=client_credentials&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

上面 URL 中,grant_type參數等於client_credentials表示採用憑證式,client_id和client_secret用來讓 B 確認 A 的身份。

  • 第二步,B 網站驗證通過以後,直接返回令牌。

這種方式給出的令牌,是針對第三方應用的,而不是針對用戶的,即有可能多個用戶共享同一個令牌。

二、實踐

1、密碼模式

如果是自建單點服務,一般都會使用密碼模式。資源服務器和授權服務器
可以是同一台服務器,也可以分開。這裏我們學習分佈式的情況。

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

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

授權服務器和資源服務器分開,項目結構如下:

1.1、授權服務器

授權服務器的職責:

  • 管理客戶端及其授權信息
    * 管理用戶及其授權信息
    * 管理Token的生成及其存儲
    * 管理Token的校驗及校驗Key

1.1.1、依賴

        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--oauth2-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.6.RELEASE</version>
        </dependency>

1.1.2、授權服務器配置

授權服務器配置通過繼承AuthorizationServerConfigurerAdapter的配置類實現:

/**
 * @Author 三分惡
 * @Date 2020/5/20
 * @Description  授權服務器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;//密碼模式需要注入認證管理器

    @Autowired
    public PasswordEncoder passwordEncoder;

    //配置客戶端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client-demo")
                .secret(passwordEncoder.encode("123"))
                .authorizedGrantTypes("password") //這裏配置為密碼模式
                .scopes("read_scope");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);//密碼模式必須添加authenticationManager
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("isAuthenticated()");
    }
}

  • 客戶端的註冊:這裏通過inMemory的方式在內存中註冊客戶端相關信息;實際項目中可以通過一些管理接口及界面動態實現客戶端的註冊
  • 校驗Token權限控制:資源服務器如果需要調用授權服務器的/oauth/check_token接口校驗token有效性,那麼需要配置checkTokenAccess(“isAuthenticated()”)
  • authenticationManager配置:需要通過endpoints.authenticationManager(authenticationManager)將Security中的authenticationManager配置到Endpoints中,否則,在Spring Security中配置的權限控制將不會在進行OAuth2相關權限控制的校驗時生效。

1.1.3、Spring Security配置

通過Spring Security來完成用戶及密碼加解密等配置:

/**
 * @Author 三分惡
 * @Date 2020/5/20
 * @Description SpringSecurity 配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("fighter")
                .password(passwordEncoder().encode("123"))
                .authorities(new ArrayList<>(0));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //所有請求必須認證
        http.authorizeRequests().anyRequest().authenticated();
    }
}

1.2、資源服務器

資源服務器的職責:

  • token的校驗
  • 給與資源

1.2.1、資源服務器配置

資源服務器依賴一樣,而配置則通過繼承自ResourceServerConfigurerAdapter的配置類來實現:

/**
 * @Author 三分惡
 * @Date 2020/5/20
 * @Description
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Bean
    public RemoteTokenServices remoteTokenServices() {
        final RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId("client-demo");
        tokenServices.setClientSecret("123");
        tokenServices.setCheckTokenEndpointUrl("http://localhost:8090/oauth/check_token");
        return tokenServices;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //session創建策略
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        //所有請求需要認證
        http.authorizeRequests().anyRequest().authenticated();
    }
}

主要進行了如下配置:

  • TokenService配置:在不採用JWT的情況下,需要配置RemoteTokenServices來充當tokenServices,它主要完成Token的校驗等工作。因此需要指定校驗Token的授權服務器接口地址
  • 同時,由於在授權服務器中配置了/oauth/check_token需要客戶端登錄后才能訪問,因此也需要配置客戶端編號及Secret;在校驗之前先進行登錄
  • 通過ResourceServerSecurityConfigurer來配置需要訪問的資源編號及使用的TokenServices

1.2.2、資源服務接口

接口比較簡單:

/**
 * @Author 三分惡
 * @Date 2020/5/20
 * @Description
 */
@RestController
public class ResourceController {

    @GetMapping("/user/{username}")
    public String user(@PathVariable String username){
        return "Hello !"+username;
    }
}

1.3、測試

授權服務器使用8090端口啟動,資源服務器使用默認端口。

1.3.1、獲取token

訪問/oauth/token端點,獲取token:

  • url:   http://localhost:8090/oauth/token?username=fighter&password=123&scope=read_scope&grant_type=password
  • 請求頭:
  • 返回的token

1.3.2、使用獲取到的token訪問資源接口

  • 使用token調用資源,訪問http://localhost:8080/user/fighter,注意使用token添加Bearer請求頭

相當於在Headers中添加 Authorization:Bearer 4a3c351d-770d-42aa-af39-3f54b50152e9。

OK,可以看到資源正確返回。

這裏僅僅是密碼模式的精簡化配置,在實際項目中,某些部分如:

  • 資源服務訪問授權服務去校驗token這部分可能會換成Jwt、Redis等tokenStore實現,
  • 授權服務器中的用戶信息與客戶端信息生產環境從數據庫中讀取,對應Spring Security的UserDetailsService實現類或用戶信息的Provider

2、授權碼模式

很多網站登錄時,允許使用第三方網站的身份,這稱為”第三方登錄”。所謂第三方登錄,實質就是 OAuth 授權。

例如用戶想要登錄 A 網站,A 網站讓用戶提供第三方網站的數據,證明自己的身份。獲取第三方網站的身份數據,就需要 OAuth 授權。

以A網站使用GitHub第三方登錄為例,流程示意如下:

接下來,簡單地實現GitHub登錄流程。

2.1、應用註冊

在使用之前需要先註冊一個應用,讓GitHub可以識別。

  • 訪問地址:https://github.com/settings/applications/new,填寫註冊表

應用的名稱隨便填,主頁 URL 填寫http://localhost:8080,回調地址填寫 http://localhost:8080/oauth/redirect。

  • 提交表單以後,GitHub 應該會返回客戶端 ID(client ID)和客戶端密鑰(client secret),這就是應用的身份識別碼

2.2、具體代碼

  • 只需要引入web依賴:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
  • GitHub相關配置
github.client.clientId=29d127aa0753c12263d7
github.client.clientSecret=f3cb9222961efe4c2adccd6d3e0df706972fa5eb
github.client.authorizeUrl=https://github.com/login/oauth/authorize
github.client.accessTokenUrl=https://github.com/login/oauth/access_token
github.client.redirectUrl=http://localhost:8080/oauth/redirect
github.client.userInfoUrl=https://api.github.com/user

  • 對應的配置類
@Component
@ConfigurationProperties(prefix = "github.client")
public class GithubProperties {
    private String clientId;
    private String clientSecret;
    private String authorizeUrl;
    private String redirectUrl;
    private String accessTokenUrl;
    private String userInfoUrl;
    //省略getter、setter
}    
  • index.html:首頁比較簡單,一個鏈接向後端發起登錄請求
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>網站首頁</title>
</head>
<body>
    <div style="text-align: center">
        <a href="http://localhost:8080/authorize">Login in with GitHub</a>
    </div>
</body>
</html>
  • GithubLoginController.java:
     * 使用RestTemplate發送http請求
     * 使用Jackson解析返回的json,不用引入更多依賴
     * 快捷起見,發送http請求的方法直接寫在控制器中,實際上應該將工具方法分離出去
     * 同樣是快捷起見,返回的用戶信息沒有做任何解析
@Controller
public class GithubLoginController {
    @Autowired
    GithubProperties githubProperties;


    /**
     * 登錄接口,重定向至github
     *
     * @return 跳轉url
     */
    @GetMapping("/authorize")
    public String authorize() {
        String url =githubProperties.getAuthorizeUrl() +
                "?client_id=" + githubProperties.getClientId() +
                "&redirect_uri=" + githubProperties.getRedirectUrl();
        return "redirect:" + url;
    }

    /**
     * 回調接口,用戶同意授權后,GitHub會將授權碼傳遞給此接口
     * @param code GitHub重定向時附加的授權碼,只能用一次
     * @return
     */
    @GetMapping("/oauth/redirect")
    @ResponseBody
    public String redirect(@RequestParam("code") String code) throws JsonProcessingException {
        System.out.println("code:"+code);
        // 使用code獲取token
        String accessToken = this.getAccessToken(code);
        // 使用token獲取userInfo
        String userInfo = this.getUserInfo(accessToken);
        return userInfo;
    }


    /**
     * 使用授權碼獲取token
     * @param code
     * @return
     */
    private String getAccessToken(String code) throws JsonProcessingException {
        String url = githubProperties.getAccessTokenUrl() +
                "?client_id=" + githubProperties.getClientId() +
                "&client_secret=" + githubProperties.getClientSecret() +
                "&code=" + code +
                "&grant_type=authorization_code";
        // 構建請求頭
        HttpHeaders requestHeaders = new HttpHeaders();
        // 指定響應返回json格式
        requestHeaders.add("accept", "application/json");
        // 構建請求實體
        HttpEntity<String> requestEntity = new HttpEntity<>(requestHeaders);
        RestTemplate restTemplate = new RestTemplate();
        // post 請求方式
        ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
        String responseStr = response.getBody();
        // 解析響應json字符串
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(responseStr);
        String accessToken = jsonNode.get("access_token").asText();
        System.out.println("accessToken:"+accessToken);
        return accessToken;
    }

    /**
     *
     * @param accessToken 使用token獲取userInfo
     * @return
     */
    private String getUserInfo(String accessToken) {
        String url = githubProperties.getUserInfoUrl();
        // 構建請求頭
        HttpHeaders requestHeaders = new HttpHeaders();
        // 指定響應返回json格式
        requestHeaders.add("accept", "application/json");
        // AccessToken放在請求頭中
        requestHeaders.add("Authorization", "token " + accessToken);
        // 構建請求實體
        HttpEntity<String> requestEntity = new HttpEntity<>(requestHeaders);
        RestTemplate restTemplate = new RestTemplate();
        // get請求方式
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
        String userInfo = response.getBody();
        System.out.println("userInfo:"+userInfo);
        return userInfo;
    }

}

2.3、測試

  • 訪問localhost:8080,點擊鏈接,重定向至GitHub
  • 在GitHub中輸入賬號密碼,登錄
  • 登錄成功后,GitHub 就會跳轉到redirect_uri指定的跳轉網址,並且帶上授權碼
http://localhost:8080/oauth/redirect?code=d45683eded3ac7d4e6ed

OK,用戶信息也一併返回了。

本文為學習筆記類博客,學習資料見參考!

參考:

【1】:《SpringSecurity 實戰》
【2】:《SpringBoot Vue全棧開發實戰》
【3】:理解OAuth 2.0
【4】:OAuth 2.0 的一個簡單解釋
【5】:OAuth 2.0 的四種方式
【6】:這個案例寫出來,還怕跟面試官扯不明白 OAuth2 登錄流程?
【7】:做微服務繞不過的 OAuth2,松哥也來和大家扯一扯
【8】:GitHub OAuth 第三方登錄示例教程
【9】:OAuth 2.0 認證的原理與實踐
【10】:Spring Security OAuth2 Demo —— 密碼模式(Password)
【11】:Spring Security OAuth專題學習-密碼模式及客戶端模式實例
【12】:Spring Boot and OAuth2
【13】:Spring Boot+OAuth2使用GitHub登錄自己的服務

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

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

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

結合 AOP 輕鬆處理事件發布處理日誌_包裝設計

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

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

結合 AOP 輕鬆處理事件發布處理日誌

Intro

前段時間,實現了 EventBus 以及 EventQueue 基於 Event 的事件處理,但是沒有做日誌(EventLog)相關的部分,原本想增加兩個接口, 處理事件發布日誌和事件處理日誌,最近用了 AOP 的思想處理了 EntityFramework 的數據變更自動審計,於是想着事件日誌也用 AOP 的思想來實現,而且可能用 AOP 來處理可能會更好一些,最近自己造了一個 AOP 的輪子 —— FluentAspects,下面的示例就以它來演示了,你也可以換成自己喜歡的 AOP 組件,思想是類似的

事件日誌示例

事件發布日誌

事件發布日誌只需要攔截事件發布的方法調用即可,在發布事件時進行攔截,在攔截器中根據需要進行日誌記錄即可

事件發布者接口定義:

public interface IEventPublisher
{
    /// <summary>
    /// publish an event
    /// </summary>
    /// <typeparam name="TEvent">event type</typeparam>
    /// <param name="event">event data</param>
    /// <returns>whether the operation succeed</returns>
    bool Publish<TEvent>(TEvent @event) where TEvent : class, IEventBase;

    /// <summary>
    /// publish an event async
    /// </summary>
    /// <typeparam name="TEvent">event type</typeparam>
    /// <param name="event">event data</param>
    /// <returns>whether the operation succeed</returns>
    Task<bool> PublishAsync<TEvent>(TEvent @event) where TEvent : class, IEventBase;
}

事件發布日誌攔截器:

public class EventPublishLogInterceptor : AbstractInterceptor
{
    public override async Task Invoke(IInvocation invocation, Func<Task> next)
    {
        Console.WriteLine("-------------------------------");
        Console.WriteLine($"Event publish begin, eventData:{invocation.Arguments.ToJson()}");
        var watch = Stopwatch.StartNew();
        try
        {
            await next();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Event publish exception({ex})");
        }
        finally
        {
            watch.Stop();
            Console.WriteLine($"Event publish complete, elasped:{watch.ElapsedMilliseconds} ms");
        }
        Console.WriteLine("-------------------------------");
    }
}

事件處理日誌

事件處理器接口定義:

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

public interface IEventHandler
{
    Task Handle(object eventData);
}

事件處理日誌攔截器定義:

public class EventHandleLogInterceptor : IInterceptor
{
    public async Task Invoke(IInvocation invocation, Func<Task> next)
    {
        Console.WriteLine("-------------------------------");
        Console.WriteLine($"Event handle begin, eventData:{invocation.Arguments.ToJson()}");
        var watch = Stopwatch.StartNew();
        try
        {
            await next();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Event handle exception({ex})");
        }
        finally
        {
            watch.Stop();
            Console.WriteLine($"Event handle complete, elasped:{watch.ElapsedMilliseconds} ms");
        }
        Console.WriteLine("-------------------------------");
    }
}

AOP 配置

Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(builder =>
    {
        builder.UseStartup<Startup>();
    })
    .UseFluentAspectsServiceProviderFactory(options =>
    {
        // 攔截器配置
        
        // 攔截 `IEventPublisher` 日誌,註冊事件發布日誌攔截器
        options
            .InterceptType<IEventPublisher>()
            .With<EventPublishLogInterceptor>();

        // 攔截 `IEventHandler`,註冊事件處理日誌攔截器
        options.InterceptType<IEventHandler>()
            .With<EventHandleLogInterceptor>();
    }, builder =>
    {
        // 默認使用默認實現來生成代理,現在提供了 Castle 和 AspectCore 的擴展,也可以自己擴展實現自定義代理生成方式
        // 取消註釋使用 Castle 來生成代理
        //builder.UseCastleProxy();
    }, t => t.Namespace?.StartsWith("WeihanLi") == false // 要忽略的類型斷言
    )
    .Build()
    .Run();

More

事件發布示例,定義了一個發布事件的中間件:

// pageView middleware
app.Use((context, next) =>
{
    var eventPublisher = context.RequestServices
        .GetRequiredService<IEventPublisher>();
    eventPublisher.Publish(new PageViewEvent()
    {
        Path = context.Request.Path.Value,
    });

    return next();
});

事件處理示例是用一個消息隊列的模式來處理的,示例和前面的事件的文章類似,EventConsumer 是一個後台任務,完整代碼示例如下:

public class EventConsumer : BackgroundService
{
    private readonly IEventQueue _eventQueue;
    private readonly IEventHandlerFactory _eventHandlerFactory;

    public EventConsumer(IEventQueue eventQueue, IEventHandlerFactory eventHandlerFactory)
    {
        _eventQueue = eventQueue;
        _eventHandlerFactory = eventHandlerFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var queues = await _eventQueue.GetQueuesAsync();
            if (queues.Count > 0)
            {
                await queues.Select(async q =>
                        {
                            var @event = await _eventQueue.DequeueAsync(q);
                            if (null != @event)
                            {
                                var handlers = _eventHandlerFactory.GetHandlers(@event.GetType());
                                if (handlers.Count > 0)
                                {
                                    await handlers
                                            .Select(h => h.Handle(@event))
                                            .WhenAll()
                                        ;
                                }
                            }
                        })
                        .WhenAll()
                    ;
            }

            await Task.Delay(1000, stoppingToken);
        }
    }
}

完整的示例代碼可以從https://github.com/WeihanLi/WeihanLi.Common/blob/dev/samples/AspNetCoreSample 獲取

OverMore

之前在微軟的 EShopOnContainers 項目里又看到類似下面這樣的代碼,在發布事件的時候包裝一層 try … catch 來記錄事件發布日誌,相比之下,本文示例中的這種方式更為簡潔,代碼更清爽

Reference

  • https://www.cnblogs.com/weihanli/p/12941919.html
  • https://www.cnblogs.com/weihanli/p/implement-event-queue.html
  • https://github.com/WeihanLi/WeihanLi.Common
  • https://github.com/WeihanLi/WeihanLi.Common/blob/dev/samples/AspNetCoreSample/Startup.cs

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

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務