oauth2實(shí)現(xiàn)單點(diǎn)登錄

oauth2實(shí)現(xiàn)單點(diǎn)登錄

電子書資源見
https://www.yuque.com/docs/share/37bd8227-eece-435a-9805-87254f4b34e9?# 《oauth2授權(quán)》

blog 地址 https://youngboy.vip

oauth2是什么?

OAuth2: OAuth2(開放授權(quán))是一個(gè)開放標(biāo)準(zhǔn),允許用戶授權(quán)第三方網(wǎng)站訪問他們存儲(chǔ)在另外的服務(wù)提供者上的信息,而不需要將用戶名和密碼提供給第三方網(wǎng)站或分享他們數(shù)據(jù)的所有內(nèi)容。

舉個(gè)栗子:第三方網(wǎng)站登錄,oschina通過gitee登錄

訪問oschina,點(diǎn)擊登錄可以選擇使用gitee登錄,gitee登錄之后,oschina就能獲取到登錄的gitee賬號(hào)相關(guān)信息<br />根據(jù)以上示例,可以將OAuth2分為四個(gè)角色:

  • Resource Owner:資源所有者 即上述中的gitee用戶
  • Resource Server:資源服務(wù)器 即上述中的gitee服務(wù)器,提供gitee用戶基本信息給到第三方應(yīng)用
  • Client:第三方應(yīng)用客戶端 即上述中oschina網(wǎng)站
  • Authorication Server:授權(quán)服務(wù)器 該角色可以理解為管理其余三者關(guān)系的中間層
    <a name="WlZNm"></a>

四種授權(quán)模式的使用場景

模式 適用場景
授權(quán)碼模式 適用于網(wǎng)頁第三方授權(quán)
密碼模式 適用于手機(jī)等客戶端使用
客戶端模式 適用于授權(quán)接口給第三方客戶端
簡化模式 適用于web端應(yīng)用

簡化模式<br />
<br />

image.png
image.png
<br />客戶端模式<br />
image.png
image.png
<br />密碼模式<br />
image.png
image.png
<br /> <br />授權(quán)碼模式<br />
image.png
image.png
<br /> <br />四種模式的選擇流程<br />
image.png
image.png
<br /> <br />參考文章 http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
<a name="4HUbK"></a>

常見安全問題

<a name="BODdj"></a>

授權(quán)碼安全

open redirect 攻擊<br />Referer 攻擊<br />redirect_url 控制攻擊<br />

<a name="CIGnM"></a>

憑證安全

保證client_secret的保密性,不能在日志中輸出client_secret或者用戶的密碼或token
<a name="GQYyl"></a>

csrf攻擊

cross-site request forgery (CSRF)

授權(quán)碼模式和簡化模式都應(yīng)該帶上state參數(shù),防止可能的csrf攻擊

官方原文6

An opaque value used by the client to maintain state between the request and callback.
The authorization server includes this value when redirecting the user-agent back to the
client. The parameter SHOULD be used for preventing cross-site request forgery (CSRF).

<a name="rxyGm"></a>

安全建議

  • 客戶端帶上state參數(shù),登陸頁面接口請(qǐng)求都要加上 CSRF token校驗(yàn)
  • 日志脫敏,避免泄露憑證或token
  • 全站https并開啟HSTS防止請(qǐng)求劫持
  • 禁止登陸頁面被嵌入iframe
  • 避免在url參數(shù)中傳遞token,游覽器的referrer頭可能會(huì)泄露token·
  • 選擇正確的授權(quán)模式,手機(jī)端游覽器禁止使用簡化模式,前端頁面禁止使用密碼模式(前端不能保存client_secret)
  • 前端后端開啟xss過濾器
  • access_token有效期不應(yīng)該大于通常使用時(shí)間
  • 使用成熟的oauth2框架

<br /> <br />Tips:如果您想對(duì)OAuth2.0開放標(biāo)準(zhǔn)進(jìn)行擴(kuò)展閱讀,請(qǐng)參看:OAuth標(biāo)準(zhǔn)(英文) | OAuth維基百科(中文)<br />
<a name="v5NXd"></a>

SSO單點(diǎn)登錄

<a name="dh3KD"></a>

sso 是什么?

SSO的定義是在多個(gè)應(yīng)用系統(tǒng)中,用戶只需要登錄一次就可以訪問所有相互信任的應(yīng)用系統(tǒng)。<br />權(quán)限控制主要分兩步

  • Authentication 認(rèn)證 (證明自己是自己)
  • Authorization 授權(quán)(證明自己是自己后能夠做什么)

sso 屬于 Authentication 范疇
<a name="6tiZK"></a>

登錄系統(tǒng)

首先,我們要為“登錄”做一個(gè)簡要的定義,令后續(xù)的講述更準(zhǔn)確。之前的兩篇文章有意無意地混淆了“登錄”與“身份驗(yàn)證”的說法,因?yàn)樵诒酒埃簧佟皞鹘y(tǒng)Web應(yīng)用”都將對(duì)身份的識(shí)別看作整個(gè)登錄的過程,很少出現(xiàn)像企業(yè)應(yīng)用環(huán)境中那樣復(fù)雜的情景和需求。但從之前的文章中我們看到,現(xiàn)代Web應(yīng)用對(duì)身份驗(yàn)證相關(guān)的需求已經(jīng)向復(fù)雜化發(fā)展了。<br />我們有必要重新認(rèn)識(shí)一下登錄系統(tǒng)。 登錄指的是從識(shí)別用戶身份,到允許用戶訪問其權(quán)限相應(yīng)的資源的過程。 舉個(gè)例子,在網(wǎng)上買好了票之后去影院觀影的過程就是一個(gè)典型的登錄過程:我們先去取票機(jī),輸入驗(yàn)證碼取票;接著拿到票去影廳檢票進(jìn)入。取票的過程即身份驗(yàn)證,它能夠證明我們擁有這張票;而后面檢票的過程,則是授權(quán)訪問的過程。之所以要分成這兩個(gè)過程,最直接的原因還是業(yè)務(wù)形態(tài)本身具有復(fù)雜性——如果觀景過程是免費(fèi)匿名的,也就免去了這些過程。<br />在登錄的過程中,“鑒權(quán)”與“授權(quán)”是兩個(gè)最關(guān)鍵的過程。接下來要介紹的一些技術(shù)和實(shí)踐,也包含在這兩個(gè)方面中。雖然現(xiàn)代Web應(yīng)用的登錄需求比較復(fù)雜,但只要處理好了鑒權(quán)和授權(quán)兩個(gè)方面,其余各個(gè)方面的問題也將迎刃而解。在現(xiàn)代Web應(yīng)用的登錄工程實(shí)踐中,需要結(jié)合傳統(tǒng)Web應(yīng)用的典型實(shí)踐,以及一些新的思路,才能既解決好登錄需求,又能符合Web的輕量級(jí)架構(gòu)思路。

解析常見的登錄場景

在簡單的Web系統(tǒng)中,典型的鑒權(quán)也就是要求用戶輸入并比對(duì)用戶名和密碼的過程,而授權(quán)則是確保會(huì)話Cookie存在。而在稍微復(fù)雜的Web系統(tǒng)中,則需要考慮多種鑒權(quán)方式,以及多種授權(quán)場景。上一篇文章中所述的“多種登錄方式”和“雙因子鑒權(quán)”就是多種鑒權(quán)方式的例子。有經(jīng)驗(yàn)的人經(jīng)常調(diào)侃說,只要理解了鑒權(quán)與授權(quán),就能清晰地理解登錄系統(tǒng)了。不光如此,這也是安全登錄系統(tǒng)的基礎(chǔ)所在。<br />鑒權(quán)的形式豐富多彩,有傳統(tǒng)的用戶名密碼對(duì)、客戶端證書,有人們?cè)絹碓绞煜さ牡谌降卿洝⑹謾C(jī)驗(yàn)證,以及新興的掃碼和指紋等方式,它們都能用于對(duì)用戶的身份進(jìn)行識(shí)別。在成功識(shí)別用戶之后,在用戶訪問資源或執(zhí)行操作之前,我們還需要對(duì)用戶的操作進(jìn)行授權(quán)。<br />在一些特別簡單的情形中——用戶一經(jīng)識(shí)別,就可以無限制地訪問資源、執(zhí)行所有操作——系統(tǒng)直接對(duì)所有“已登錄的人”放行。比如高速公路收費(fèi)站,只要車輛有合法的號(hào)牌即可放行,不需要給駕駛員發(fā)一張用于指示“允許行駛的方向或時(shí)間”的票據(jù)。除了這類特別簡單的情形之外,授權(quán)更多時(shí)候是比較復(fù)雜的工作。<br />在單一的傳統(tǒng)Web應(yīng)用中,授權(quán)的過程通常由會(huì)話Cookie來完成——只要服務(wù)器發(fā)現(xiàn)瀏覽器攜帶了對(duì)應(yīng)的Cookie,即允許用戶訪問資源、執(zhí)行操作。而在瀏覽器之外,例如在Web API調(diào)用、移動(dòng)應(yīng)用和富 Web 應(yīng)用等場景中,要提供安全又不失靈活的授權(quán)方式,就需要借助令牌技術(shù)。

令牌

令牌是一個(gè)在各種介紹登錄技術(shù)的文章中常被提及的概念,也是現(xiàn)代Web應(yīng)用系統(tǒng)中非常關(guān)鍵的技術(shù)。令牌是一個(gè)非常簡單的概念,它指的是在用戶通過身份驗(yàn)證之后,為用戶分配的一個(gè)臨時(shí)憑證。在系統(tǒng)內(nèi)部,各個(gè)子系統(tǒng)只需要以統(tǒng)一的方式正確識(shí)別和處理這個(gè)憑證即可完成對(duì)用戶的訪問和操作進(jìn)行授權(quán)。在上文所提到的例子中,電影票就是一個(gè)典型的令牌。影廳門口的工作人員只需要確認(rèn)來客手持印有對(duì)應(yīng)場次的電影票即視為合法訪問,而不需要理會(huì)客戶是從何種渠道取得了電影票(比如自行購買、朋友贈(zèng)予等),電影票在本場次范圍內(nèi)可以持續(xù)使用(比如可以中場出去休息等)、過期作廢。通過電影票這樣一個(gè)簡單的令牌機(jī)制,電影票的出售渠道可以豐富多樣,檢票人員的工作卻仍然簡單輕松。

OAuth 2、Open ID Connect

令牌在廣為使用的OAuth技術(shù)中被采用來完成授權(quán)的過程。OAuth是一種開放的授權(quán)模型,它規(guī)定了一種供資源擁有方與消費(fèi)方之間簡單又直觀的交互方法,即從消費(fèi)方向資源擁有方發(fā)起使用AccessToken(訪問令牌)簽名的HTTP請(qǐng)求。這種方式讓消費(fèi)方應(yīng)用在無需(也無法)獲得用戶憑據(jù)的情況下,只要用戶完成鑒權(quán)過程并同意消費(fèi)方以自己的身份調(diào)用數(shù)據(jù)和操作,消費(fèi)方就可以獲得能夠完成功能的訪問令牌。OAuth簡單的流程和自由的編程模型讓它很好地滿足了開放平臺(tái)場景中授權(quán)第三方應(yīng)用使用用戶數(shù)據(jù)的需求。不少互聯(lián)網(wǎng)公司建設(shè)開放平臺(tái),將它們的用戶在其平臺(tái)上的數(shù)據(jù)以 API 的形式開放給第三方應(yīng)用來使用,從而讓用戶享受更豐富的服務(wù)。<br />OAuth在各個(gè)開放平臺(tái)的成功使用,令更多開發(fā)者了解到它,并被它簡單明確的流程所吸引。此外,OAuth協(xié)議規(guī)定的是授權(quán)模型,并不規(guī)定訪問令牌的數(shù)據(jù)格式,也不限制在整個(gè)登錄過程中需要使用的鑒權(quán)方法。人們很快發(fā)現(xiàn),只要對(duì)OAuth進(jìn)行合適的利用即可將其用于各種自有系統(tǒng)中的場景。例如,將 Web 服務(wù)視作資源擁有方,而將富Web應(yīng)用或者移動(dòng)應(yīng)用視作消費(fèi)方應(yīng)用,就與開放平臺(tái)的場景完全吻合。<br />另一個(gè)大量實(shí)踐的場景是基于OAuth的單點(diǎn)登錄。OAuth并沒有對(duì)鑒權(quán)的部分做規(guī)定,也不要求在握手交互過程中包含用戶的身份信息,因此它并不適合作為單點(diǎn)登錄系統(tǒng)來使用。然而,由于OAuth的流程中隱含了鑒權(quán)的步驟,因而仍然有不少開發(fā)者將這一鑒權(quán)的步驟用作單點(diǎn)登錄系統(tǒng),這也儼然衍生成為一種實(shí)踐模式。更有人將這個(gè)實(shí)踐進(jìn)行了標(biāo)準(zhǔn)化,它就是Open ID Connect——基于OAuth的身份上下文協(xié)議,通過它即可以JWT的形式安全地在多個(gè)應(yīng)用中共享用戶身份。接下來,只要讓鑒權(quán)服務(wù)器支持較長的會(huì)話時(shí)間,就可以利用OAuth為多個(gè)業(yè)務(wù)系統(tǒng)提供單點(diǎn)登錄功能了。<br />我們還沒有討論OAuth對(duì)鑒權(quán)系統(tǒng)的影響。實(shí)際上,OAuth對(duì)鑒權(quán)系統(tǒng)沒有影響,在它的框架內(nèi),只是假設(shè)已經(jīng)存在了一種可用于識(shí)別用戶的有效機(jī)制,而這種機(jī)制具體是怎么工作的,OAuth并不關(guān)心。因此我們既可以使用用戶名密碼(大多數(shù)開放平臺(tái)提供商都是這種方式),也可以使用掃碼登錄來識(shí)別用戶,更可以提供諸如“記住密碼”,或者雙因子驗(yàn)證等其他功能。<br />

匯總

上面羅列了大量術(shù)語和解釋,那么具體到一個(gè)典型的Web系統(tǒng)中,又應(yīng)該如何對(duì)安全系統(tǒng)進(jìn)行設(shè)計(jì)呢?綜合這些技術(shù),從端到云,從Web門戶到內(nèi)部服務(wù),本文給出如下架構(gòu)方案建議:<br />推薦為整個(gè)應(yīng)用的所有系統(tǒng)、子系統(tǒng)都部署全程的HTTPS,如果出于性能和成本考慮做不到,那么至少要保證在用戶或設(shè)備直接訪問的Web應(yīng)用中全程使用HTTPS。<br />用不同的系統(tǒng)分別用作身份和登錄,以及業(yè)務(wù)服務(wù)。當(dāng)用戶登錄成功之后,使用OpenID Connect向業(yè)務(wù)系統(tǒng)頒發(fā)JWT格式的訪問令牌和身份信息。如果需要,登錄系統(tǒng)可以提供多種登錄方式,或者雙因子登錄等增強(qiáng)功能。作為安全令牌服務(wù)(STS),它還負(fù)責(zé)頒發(fā)、刷新、驗(yàn)證和取消令牌的操作。在身份驗(yàn)證的整個(gè)流程的每一個(gè)步驟,都使用OAuth及JWT中內(nèi)置的機(jī)制來驗(yàn)證數(shù)據(jù)的來源方是可信的:登錄系統(tǒng)要確保登錄請(qǐng)求來自受認(rèn)可的業(yè)務(wù)應(yīng)用,而業(yè)務(wù)在獲得令牌之后也需要驗(yàn)證令牌的有效性。<br />在Web頁面應(yīng)用中,應(yīng)該申請(qǐng)時(shí)效較短的令牌。將獲取到的令牌向客戶端頁面中以httponly的方式寫入會(huì)話Cookie,以用于后續(xù)請(qǐng)求的授權(quán);在后緒請(qǐng)求到達(dá)時(shí),驗(yàn)證請(qǐng)求中所攜帶的令牌,并延長其時(shí)效。基于JWT自包含的特性,輔以完備的簽名認(rèn)證,Web 應(yīng)用無需額外地維護(hù)會(huì)話狀態(tài)。<br />在富客戶端Web應(yīng)用(單頁應(yīng)用),或者移動(dòng)端、客戶端應(yīng)用中,可按照應(yīng)用業(yè)務(wù)形態(tài)申請(qǐng)時(shí)效較長的令牌,或者用較短時(shí)效的令牌、配合專用的刷新令牌使用。<br />在Web應(yīng)用的子系統(tǒng)之間,調(diào)用其他子服務(wù)時(shí),可靈活使用“應(yīng)用程序身份”(如果該服務(wù)完全不直接對(duì)用戶提供調(diào)用),或者將用戶傳入的令牌直接傳遞到受調(diào)用的服務(wù),以這種方式進(jìn)行授權(quán)。各個(gè)業(yè)務(wù)系統(tǒng)可結(jié)合基于角色的訪問控制(RBAC)開發(fā)自有專用權(quán)限系統(tǒng)。<br />http://insights.thoughtworkers.org/traditional-web-app-authentication/
<a name="axw0U"></a>

各大開發(fā)平臺(tái)oauth2的使用

cas 解決方案

cas 是一種協(xié)議,cas server 是開源的實(shí)現(xiàn)

官網(wǎng)地址 https://www.apereo.org/projects/cas
<a name="Y6Vq0"></a>

cas 協(xié)議

CAS協(xié)議是一種簡單且功能強(qiáng)大的基于票證(ticket)的協(xié)議。它涉及一個(gè)或多個(gè)客戶端和一臺(tái)服務(wù)器。中央身份驗(yàn)證服務(wù)(CAS)是Web的單點(diǎn)登錄/單點(diǎn)退出協(xié)議。用戶向中央CAS Server應(yīng)用程序提供一次憑據(jù)(例如用戶ID和密碼),就可以訪問多個(gè)應(yīng)用程序。客戶端嵌入在CASified應(yīng)用程序中(稱為“ CAS服務(wù)”),而CAS服務(wù)器是獨(dú)立組件:

  • CAS服務(wù)器負(fù)責(zé)驗(yàn)證用戶并授予訪問應(yīng)用程序
  • CAS客戶保護(hù)CAS應(yīng)用程序和檢索CAS服務(wù)器的授權(quán)用戶的身份。

關(guān)鍵概念:

  • TGT存儲(chǔ)在TGCcookie中的(“票證授予票證”)代表用戶的SSO會(huì)話。
  • ST(服務(wù)票據(jù)),作為傳輸GET參數(shù)的URL,代表由CAS服務(wù)器授予訪問CASified應(yīng)用程序?qū)μ囟ㄓ脩簟?/li>

官方地址 https://www.apereo.org/projects/cas<br />流程參考 https://blog.csdn.net/isyoungboy/article/details/103242009[圖片上傳失敗...(image-d12fde-1602203995839)]
<a name="Sstz5"></a>

cas server

cas server 不僅僅是對(duì)cas協(xié)議的實(shí)現(xiàn),cas server 還實(shí)現(xiàn)了 oidc smal 等協(xié)議
<a name="c0P7Q"></a>

部署要求

  • java版本要求jdk11以上
    <a name="eubQf"></a>

優(yōu)點(diǎn)

  • 功能豐富,支持多種認(rèn)證協(xié)議實(shí)現(xiàn) cas oidc smal
  • 支持協(xié)議代理使用pac4j實(shí)現(xiàn)
  • 客戶端支持齊全,支持spring security spring boot
  • 官方文檔比較全
    <a name="gWuaN"></a>

缺點(diǎn)

  • 如果使用cas協(xié)議,對(duì)已有系統(tǒng)改動(dòng)比較大,改動(dòng)成本高
  • 框架比較復(fù)雜
  • java版本要求高
    <a name="OziOJ"></a>

keycloak 解決方案

Keycloak 是一個(gè)為瀏覽器和 RESTful Web 服務(wù)提供 SSO 的集成。基于OIDC規(guī)范。最開始是面向 JBoss 和 Wildfly 通訊<br />源碼地址是:https://github.com/keycloak/keycloak/
<a name="6MWhJ"></a>

部署要求

  • java8
    <a name="ZPzFD"></a>

優(yōu)點(diǎn)

  • 支持ladp等中間件
  • 功能豐富,有多種認(rèn)證協(xié)議實(shí)現(xiàn) cas oidc smal
  • 自帶有完善的用戶角色組織管理系統(tǒng),同時(shí)還有自帶的web控制臺(tái)管理用戶角色組織客戶端等信息
  • 客戶端支持齊全,支持spring security spring boot
    <a name="2czN9"></a>

缺點(diǎn)

  • 改動(dòng)成本高
  • 中間件比較復(fù)雜
  • 已有用戶體系的系統(tǒng)不好改造
  • 文檔不多,沒有很多人使用
    <a name="BwavC"></a>

spring security oauth2 解決方案

oauth2 只是授權(quán)協(xié)議并不包含完整的認(rèn)證協(xié)議,需要在oauth2的基礎(chǔ)上進(jìn)行改造才能使用,需要寫一些代碼,亦可以使用oidc協(xié)議可以簡單理解為(openid + oauth2),改動(dòng)后以前的前端登陸實(shí)現(xiàn)都需要從項(xiàng)目中遷移出來到用戶認(rèn)證中心,用戶統(tǒng)一到認(rèn)證中心登陸(和qq github 等平臺(tái)一樣,上面的解決方案也差不多)
<a name="33Elk"></a>

優(yōu)點(diǎn)

可以靈活添加功能改動(dòng)成本較小如果使用oidc協(xié)議,接入方可以選擇標(biāo)準(zhǔn)的開源成熟的 oidc sdk,不需要再自己寫sdk
<a name="8pe7C"></a>

缺點(diǎn)

需要拓展一些功能(可以參考cas中的實(shí)現(xiàn))
<a name="HIGF0"></a>

oauth2 sso 簡單認(rèn)證流程

代號(hào) 描述
UAA 認(rèn)證中心
A 系統(tǒng)A
B 系統(tǒng)B
USER 用戶

第一次訪問A系統(tǒng)

[圖片上傳失敗...(image-586f67-1602203995839)]> 訪問A系統(tǒng)之后再訪問B系統(tǒng)

[圖片上傳失敗...(image-68bba7-1602203995839)]
<br />

<a name="i34Go"></a>

現(xiàn)有用戶體系問題

Q: 接入方系統(tǒng)以有現(xiàn)成的用戶怎么解決?

A:接入方自己解決(接入方可以通過綁定已有用戶,也可以新建用戶)
<a name="adTlL"></a>

權(quán)限控制問題

Q: 接入方系統(tǒng)有現(xiàn)成的用戶權(quán)限怎么控制?

A: 單點(diǎn)登陸只管認(rèn)證的問題,不需要考慮接入方的權(quán)限怎么控制

Q: 接入方系統(tǒng)怎么控制權(quán)限?

A: 使用用戶詳情接口獲取用戶詳細(xì)信息,使用client的scope 或者 authorities

Q: 接入方系統(tǒng)與接入方系統(tǒng)間需要相互調(diào)用嗎?

A: 不需要相互調(diào)用

<a name="XBFaU"></a>

單系統(tǒng)登出功能,全局登出功能

  • “SSO” 是指單點(diǎn)登錄。
  • “SLO” 是指單一注銷。

<br />

目前系統(tǒng)只實(shí)現(xiàn)了全局登出,參考了oidc使用前端游覽器通知第三方退出的方法

登出流程<br />
<a name="DQZe2"></a>

參考鏈接

open id 官網(wǎng) https://openid.net/connect/<br />oidc 文檔 https://openid.net/specs/openid-connect-core-1_0.html<br />oidc core 協(xié)議 https://www.cnblogs.com/linianhui/p/openid-connect-core.html<br />oidc 認(rèn)證流程 http://www.sohu.com/a/206818578_468635<br />
<a name="rtbHT"></a>

基于spring security oauth2單點(diǎn)登錄的實(shí)現(xiàn)

<a name="mkcW7"></a>

實(shí)現(xiàn)功能點(diǎn)

  • 用戶認(rèn)證(用戶名密碼,釘釘掃碼)
  • 跨域名單點(diǎn)登錄
  • 統(tǒng)一登出(參考了oidc實(shí)現(xiàn))
  • 客戶端token獲取
  • password模式token獲取
  • 簡化模式token獲取

<a name="2NpzV"></a>

spring security oauth2配置項(xiàng)

<a name="I3qAH"></a>

配置授權(quán)服務(wù)

@EnableAuthorizationServer<br />@Configuration<br />public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter {<br /> @Autowired<br /> private UaaProperties uaaProperties;<br /> @Autowired<br /> private ApplicationContext applicationContext;<br /> @Autowired<br /> private RemoteClientDetailsService remoteClientDetailsService;<br /> @Autowired<br /> private RedisTokenStoreEnhance redisTokenStoreEnhance;<br /> @Autowired<br /> private AuthorizationCodeServices authorizationCodeServices;<br /> @Override<br /> public void configure(ClientDetailsServiceConfigurer clients) throws Exception {<br /> clients.withClientDetails(remoteClientDetailsService);//配置客戶端詳情服務(wù)<br /> }<br /> @Override<br /> public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {<br /> //設(shè)置token的訪問權(quán)限為permitAll 檢查token 的權(quán)限為 isAuthenticated() 在httpSecurity 中配置權(quán)限是沒用的<br /> oauthServer<br /> .tokenKeyAccess("permitAll()")//設(shè)置oauth/token_key 的訪問權(quán)限<br /> .checkTokenAccess("isAuthenticated()") //設(shè)置checkToken 接口的訪問權(quán)限<br /> .allowFormAuthenticationForClients();//設(shè)置允許Client通過Form認(rèn)證,否則就只能使用basic認(rèn)證,from認(rèn)證相關(guān)過濾器為 ClientCredentialsTokenEndpointFilter<br /> }<br /> @Override<br /> public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {<br /> //配置token處理鏈<br /> Collection<TokenEnhancer> tokenEnhancers = applicationContext.getBeansOfType(TokenEnhancer.class).values();<br /> TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();<br /> tokenEnhancerChain.setTokenEnhancers(new ArrayList<>(tokenEnhancers));<br /> <br /> //配置自定義授權(quán)碼服務(wù)<br /> endpoints.authorizationCodeServices(authorizationCodeServices);<br /> endpoints<br /> .tokenStore(tokenStore())//配置token儲(chǔ)存<br /> .tokenEnhancer(tokenEnhancerChain)//配置token處理鏈<br /> .reuseRefreshTokens(false)<br /> //設(shè)置自定義的oauth2異常處理器<br /> .exceptionTranslator(new OAuth2ResponseExceptionTranslator()); //don't reuse or we will run into session inactivity timeouts<br /> }<br /> //把token轉(zhuǎn)換為jwttoken的轉(zhuǎn)換器<br /> @Bean<br /> public JwtAccessTokenConverter jwtAccessTokenConverter() {<br /> JwtAccessTokenConverter converter = new VerificationCode.RedisTokenConverter(redisTokenStoreEnhance);<br /> KeyPair keyPair = new KeyStoreKeyFactory(<br /> new ClassPathResource(uaaProperties.getKeyStore().getName()), uaaProperties.getKeyStore().getPassword().toCharArray())<br /> .getKeyPair(uaaProperties.getKeyStore().getAlias());<br /> converter.setKeyPair(keyPair);<br /> return converter;<br /> }<br /> @Bean<br /> public JwtTokenStore tokenStore() {<br /> return new JwtTokenStore(jwtAccessTokenConverter());<br /> }<br />}<br /> <br />
<a name="zzZT4"></a>

spring security oauth2 拓展項(xiàng)

<a name="pm5lV"></a>

定制用戶確認(rèn)授權(quán)頁面

@Bean<br /> public TokenStoreUserApprovalPlusHandler tokenStoreUserApprovalPlusHandler(TokenStore tokenStore, ClientDetailsService clientDetailsService, AuthorizationEndpoint authorizationEndpoint){<br /> TokenStoreUserApprovalPlusHandler approvalHandler = new TokenStoreUserApprovalPlusHandler();<br /> approvalHandler.setTokenStore(tokenStore);<br /> approvalHandler.setClientDetailsService(clientDetailsService);<br /> approvalHandler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));<br /> authorizationEndpoint.setUserApprovalHandler(approvalHandler);<br /> authorizationEndpoint.setUserApprovalPage("access");//設(shè)置授權(quán)頁面為access<br /> return approvalHandler;<br /> }<br />
<a name="FYBFb"></a>

定制授權(quán)碼生成服務(wù)實(shí)現(xiàn)

@Component<br />public class RedisAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {<br /> @Autowired<br /> private RedisTemplate redisTemplate;<br /> /**<br /> * 存儲(chǔ)code到redis,并設(shè)置過期時(shí)間,10分鐘
<br /> * value為OAuth2Authentication序列化后的字節(jié)
<br /> * 因?yàn)镺Auth2Authentication沒有無參構(gòu)造函數(shù)
<br /> * redisTemplate.opsForValue().set(key, value, timeout, unit);<br /> * 這種方式直接存儲(chǔ)的話,redisTemplate.opsForValue().get(key)的時(shí)候有些問題,<br /> * 所以這里采用最底層的方式存儲(chǔ),get的時(shí)候也用最底層的方式獲取<br /> */<br /> @Override<br /> protected void store(String code, OAuth2Authentication authentication) {<br /> redisTemplate.opsForValue().set(codeKey(code), authentication,10, TimeUnit.MINUTES);<br /> }<br /> @Override<br /> protected OAuth2Authentication remove(String code) {<br /> try{<br /> return (OAuth2Authentication) redisTemplate.opsForValue().get(codeKey(code));<br /> }catch (Exception e){<br /> CodeAuthDTO codeAuthDTO = (CodeAuthDTO) redisTemplate.opsForValue().get(codeKey(code));<br /> Set<SimpleGrantedAuthority> auths = codeAuthDTO.getAuths().stream().map(a -> new SimpleGrantedAuthority(a)).collect(Collectors.toSet());<br /> OAuth2Request oAuth2Request = new OAuth2Request(codeAuthDTO.getRequestParameters(), codeAuthDTO.getClientId(), convertAuth(codeAuthDTO.getClientAuths()), codeAuthDTO.isApproved(), codeAuthDTO.getScope(), codeAuthDTO.getResourceIds(), codeAuthDTO.getRedirectUri(), codeAuthDTO.getResponseTypes(), codeAuthDTO.getExtensions());<br /> return new OAuth2Authentication(oAuth2Request,new UsernamePasswordAuthenticationToken(codeAuthDTO.getPrincipal(),codeAuthDTO.getCredentials(),auths));<br /> }<br /> }<br /> public Set<SimpleGrantedAuthority> convertAuth(Set<String> auths){<br /> return auths.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());<br /> }<br /> public Set<String> convertStr(Collection<? extends GrantedAuthority> auths){<br /> return auths.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());<br /> }<br /> private String codeKey(String code) {<br /> return "oauth2:codes:" + code;<br /> }<br />}<br />
<a name="ym9ZF"></a>

定制登出處理器(統(tǒng)一登出)

@Component<br />public class DefaultSSOLogoutSuccessHandler implements SSOLogoutSuccessHandler {<br /> @Autowired<br /> private RedisTokenStoreEnhance redisTokenStoreEnhance;<br /> @Autowired<br /> private ClientDetailsService clientDetailsService;<br /> @Override<br /> public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication, Model model) {<br /> //查詢當(dāng)前用戶登錄的客戶端<br /> Object principal = authentication.getPrincipal();<br /> if(principal instanceof SystemUser){<br /> String userName = ((SystemUser) principal).getUsername();<br /> //通過用戶登錄名查詢?cè)诰€的客戶端<br /> Set<TokenAndClientId> clients = redisTokenStoreEnhance.findTokenAndClientIdByUserName(userName);<br /> //構(gòu)建登出地址<br /> if(!CollectionUtils.isEmpty(clients)){<br /> Set<String> tokens = Sets.newLinkedHashSet();<br /> Set<String> logoutUrls = Sets.newHashSet();<br /> for (TokenAndClientId client : clients) {<br /> String clientId = client.getClientId();<br /> String token = client.getToken();<br /> //記錄要注銷的token<br /> tokens.add(token);<br /> ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);<br /> Map<String, Object> additionalInformation = clientDetails.getAdditionalInformation();<br /> //獲取客戶端配置的登出地址<br /> String logoutUrl = (String) additionalInformation.get("logoutUrl");<br /> if(StringUtils.hasText(logoutUrl)){<br /> //拼接客戶端的退出url,參數(shù)為access_token<br /> logoutUrls.add(resolveLogoutUrl(logoutUrl,token));<br /> }<br /> }<br /> model.addAttribute("logoutUrls",logoutUrls);<br /> //清空所有的授權(quán)token<br /> redisTokenStoreEnhance.deleteUserAccessKey(userName,tokens);<br /> }<br /> }<br /> }<br /> public String resolveLogoutUrl(String logoutUrl,String token){<br /> UriComponentsBuilder template = UriComponentsBuilder.fromUriString(logoutUrl);<br /> template.queryParam("access_token",token);<br /> return template.build().toUriString();<br /> }<br />}
<a name="Ey7eD"></a>

定制用戶同意拒絕處理器

public class TokenStoreUserApprovalPlusHandler extends TokenStoreUserApprovalHandler {<br /> private String scopePrefix = OAuth2Utils.SCOPE_PREFIX;<br /> private ClientDetailsService clientDetailsService;<br /> @Override<br /> public Map<String, Object> getUserApprovalRequest(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {<br /> Map<String, Object> userApprovalRequest = super.getUserApprovalRequest(authorizationRequest, userAuthentication);<br /> String clientId = authorizationRequest.getClientId();<br /> ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);<br /> userApprovalRequest.put("clientName",clientDetails.getAdditionalInformation().getOrDefault("clientName","未知客戶端"));<br /> userApprovalRequest.put("iconUrl",clientDetails.getAdditionalInformation().getOrDefault("iconUrl","未知客戶端"));<br /> String scope = (String) userApprovalRequest.getOrDefault("scope","user_info");<br /> if(scope.indexOf(" ")==-1){<br /> Constants.SCOPE_INFOS.stream().findFirst().filter(s->s.getName().equals(scope)).ifPresent(scopeInfo -> userApprovalRequest.put("scope_info", JSON.toJSONString(Arrays.asList(scopeInfo))));<br /> }else {<br /> List<ScopeInfo> scopeInfos = Arrays.stream(scope.split(" "))<br /> .map(scopeInfo -> Constants.SCOPE_INFOS.stream().filter(s -> s.getName().equals(scopeInfo)).findAny().orElse(null))<br /> .filter(i -> i != null)<br /> .collect(Collectors.toList());<br /> userApprovalRequest.put("scope_info", JSON.toJSONString(scopeInfos));<br /> }<br /> return userApprovalRequest;<br /> }<br /> @Override<br /> public void setClientDetailsService(ClientDetailsService clientDetailsService) {<br /> super.setClientDetailsService(clientDetailsService);<br /> this.clientDetailsService = clientDetailsService;<br /> }<br /> @Override<br /> public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest,<br /> Authentication userAuthentication) {<br /> // Get the approved scopes<br /> Set<String> requestedScopes = authorizationRequest.getScope();<br /> Set<String> approvedScopes = new HashSet<String>();<br /> Set<Approval> approvals = new HashSet<Approval>();<br /> Date expiry = computeExpiry();<br /> // Store the scopes that have been approved / denied<br /> Map<String, String> approvalParameters = authorizationRequest.getApprovalParameters();<br /> for (String requestedScope : requestedScopes) {<br /> String approvalParameter = scopePrefix + requestedScope;<br /> String value = approvalParameters.get(approvalParameter);<br /> value = value == null ? "" : value.toLowerCase();<br /> if ("true".equals(value) || value.startsWith("approve")) {<br /> approvedScopes.add(requestedScope);<br /> approvals.add(new Approval(userAuthentication.getName(), authorizationRequest.getClientId(),<br /> requestedScope, expiry, Approval.ApprovalStatus.APPROVED));<br /> }<br /> else {<br /> approvals.add(new Approval(userAuthentication.getName(), authorizationRequest.getClientId(),<br /> requestedScope, expiry, Approval.ApprovalStatus.DENIED));<br /> }<br /> }<br /> boolean approved;<br /> authorizationRequest.setScope(approvedScopes);<br /> if (approvedScopes.isEmpty() && !requestedScopes.isEmpty()) {<br /> approved = false;<br /> }<br /> else {<br /> approved = true;<br /> }<br /> authorizationRequest.setApproved(approved);<br /> return authorizationRequest;<br /> }<br /> private Date computeExpiry() {<br /> Calendar expiresAt = Calendar.getInstance();<br /> expiresAt.add(Calendar.MONTH, 1);<br /> return expiresAt.getTime();<br /> }<br />}
<a name="FTzoi"></a>

定制認(rèn)證授權(quán)入口

package com.ecidi.uaa.security;<br />import lombok.extern.slf4j.Slf4j;<br />import org.springframework.beans.factory.InitializingBean;<br />import org.springframework.security.core.AuthenticationException;<br />import org.springframework.security.web.;<br />import org.springframework.security.web.util.RedirectUrlBuilder;<br />import org.springframework.security.web.util.UrlUtils;<br />import org.springframework.util.Assert;<br />import org.springframework.util.StringUtils;<br />import javax.servlet.RequestDispatcher;<br />import javax.servlet.ServletException;<br />import javax.servlet.http.HttpServletRequest;<br />import javax.servlet.http.HttpServletResponse;<br />import java.io.IOException;<br />@Slf4j<br />public class OAuth2PlusAuthenticationEntryPoint implements AuthenticationEntryPoint , InitializingBean {<br /> private PortMapper portMapper = new PortMapperImpl();<br /> private PortResolver portResolver = new PortResolverImpl();<br /> private String loginFormUrl;<br /> private boolean forceHttps = false;<br /> private boolean useForward = false;<br /> private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();<br /> public OAuth2PlusAuthenticationEntryPoint(String loginFormUrl) {<br /> Assert.notNull(loginFormUrl, "loginFormUrl cannot be null");<br /> this.loginFormUrl = loginFormUrl;<br /> }<br /> public void afterPropertiesSet() {<br /> Assert.isTrue(<br /> StringUtils.hasText(loginFormUrl)<br /> && UrlUtils.isValidRedirectUrl(loginFormUrl),<br /> "loginFormUrl must be specified and must be a valid redirect URL");<br /> if (useForward && UrlUtils.isAbsoluteUrl(loginFormUrl)) {<br /> throw new IllegalArgumentException(<br /> "useForward must be false if using an absolute loginFormURL");<br /> }<br /> Assert.notNull(portMapper, "portMapper must be specified");<br /> Assert.notNull(portResolver, "portResolver must be specified");<br /> }<br /> protected String determineUrlToUseForThisRequest(HttpServletRequest request,<br /> HttpServletResponse response, AuthenticationException exception) {<br /> return getLoginFormUrl();<br /> }<br /> public void commence(HttpServletRequest request, HttpServletResponse response,<br /> AuthenticationException authException) throws IOException, ServletException {<br /> String redirectUrl = null;<br /> if (useForward) {<br /> if (forceHttps && "http".equals(request.getScheme())) {<br /> // First redirect the current request to HTTPS.<br /> // When that request is received, the forward to the login page will be<br /> // used.<br /> redirectUrl = buildHttpsRedirectUrlForRequest(request);<br /> }<br /> if (redirectUrl == null) {<br /> String loginForm = determineUrlToUseForThisRequest(request, response,<br /> authException);<br /> if (log.isDebugEnabled()) {<br /> log.debug("Server side forward to: " + loginForm);<br /> }<br /> RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);<br /> dispatcher.forward(request, response);<br /> return;<br /> }<br /> }<br /> else {<br /> redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);<br /> }<br /> redirectStrategy.sendRedirect(request, response, redirectUrl);<br /> }<br /> protected String buildRedirectUrlToLoginPage(HttpServletRequest request,<br /> HttpServletResponse response, AuthenticationException authException) {<br /> String loginForm = determineUrlToUseForThisRequest(request, response,<br /> authException);<br /> if (UrlUtils.isAbsoluteUrl(loginForm)) {<br /> return loginForm;<br /> }<br /> int serverPort = portResolver.getServerPort(request);<br /> String scheme = request.getScheme();<br /> RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();<br /> urlBuilder.setScheme(scheme);<br /> urlBuilder.setServerName(request.getServerName());<br /> urlBuilder.setPort(serverPort);<br /> urlBuilder.setContextPath(request.getContextPath());<br /> urlBuilder.setPathInfo(loginForm);<br /> if (forceHttps && "http".equals(scheme)) {<br /> Integer httpsPort = portMapper.lookupHttpsPort(serverPort);<br /> if (httpsPort != null) {<br /> // Overwrite scheme and port in the redirect URL<br /> urlBuilder.setScheme("https");<br /> urlBuilder.setPort(httpsPort);<br /> }<br /> else {<br /> log.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port "<br /> + serverPort);<br /> }<br /> }<br /> return urlBuilder.getUrl();<br /> }<br /> /*<br /> * Builds a URL to redirect the supplied request to HTTPS. Used to redirect the<br /> * current request to HTTPS, before doing a forward to the login page.<br /> */<br /> protected String buildHttpsRedirectUrlForRequest(HttpServletRequest request) {<br /> int serverPort = portResolver.getServerPort(request);<br /> Integer httpsPort = portMapper.lookupHttpsPort(serverPort);<br /> if (httpsPort != null) {<br /> RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();<br /> urlBuilder.setScheme("https");<br /> urlBuilder.setServerName(request.getServerName());<br /> urlBuilder.setPort(httpsPort);<br /> urlBuilder.setContextPath(request.getContextPath());<br /> urlBuilder.setServletPath(request.getServletPath());<br /> urlBuilder.setPathInfo(request.getPathInfo());<br /> urlBuilder.setQuery(request.getQueryString());<br /> return urlBuilder.getUrl();<br /> }<br /> // Fall through to server-side forward with warning message<br /> log.warn("Unable to redirect to HTTPS as no port mapping found for HTTP port "<br /> + serverPort);<br /> return null;<br /> }<br /> public void setForceHttps(boolean forceHttps) {<br /> this.forceHttps = forceHttps;<br /> }<br /> protected boolean isForceHttps() {<br /> return forceHttps;<br /> }<br /> public String getLoginFormUrl() {<br /> return loginFormUrl;<br /> }<br /> public void setPortMapper(PortMapper portMapper) {<br /> Assert.notNull(portMapper, "portMapper cannot be null");<br /> this.portMapper = portMapper;<br /> }<br /> protected PortMapper getPortMapper() {<br /> return portMapper;<br /> }<br /> public void setPortResolver(PortResolver portResolver) {<br /> Assert.notNull(portResolver, "portResolver cannot be null");<br /> this.portResolver = portResolver;<br /> }<br /> protected PortResolver getPortResolver() {<br /> return portResolver;<br /> }<br /> public void setUseForward(boolean useForward) {<br /> this.useForward = useForward;<br /> }<br /> protected boolean isUseForward() {<br /> return useForward;<br /> }<br /> <br />}<br />
<a name="IZ1ms"></a>

定制用戶認(rèn)證過濾器

public class UsernamePasswordPlusAuthenticationFilter extends AbstractAuthenticationProcessingFilter {<br /> /*<br /> * 認(rèn)證類型 用戶名密碼認(rèn)證 釘釘掃碼認(rèn)證<br /> /<br /> public static final String AUTH_TYPE = "encrypt_key";<br /> private static final String PASSWORD_AUTH = "password";<br /> private static final String DING_AUTH = "ding";<br /> public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "user[login]";<br /> public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "encrypt_data[password]";<br /> public static final String CODE_KEY = "code";<br /> public static final String NOISE_KEY = "noise";<br /> private String privateKey = "xxx";<br /> private String authType = AUTH_TYPE;<br /> private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;<br /> private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;<br /> private String codeParameter = CODE_KEY;<br /> private String noiseParameter = NOISE_KEY;<br /> private boolean postOnly = true;<br /> private VerificationCode verificationCode;<br /> public UsernamePasswordPlusAuthenticationFilter(AntPathRequestMatcher antPathRequestMatcher,VerificationCode verificationCode) {<br /> super(antPathRequestMatcher);<br /> this.verificationCode=verificationCode;<br /> }<br /> public Authentication attemptAuthentication(HttpServletRequest request,<br /> HttpServletResponse response) throws AuthenticationException {<br /> if (postOnly && !request.getMethod().equals("POST")) {<br /> throw new AuthenticationServiceException(<br /> "Authentication method not supported: " + request.getMethod());<br /> }<br /> String authType = obtainAuthType(request);<br /> AbstractAuthenticationToken authRequest = null;<br /> if(PASSWORD_AUTH.equals(authType)){<br /> //用戶名密碼認(rèn)證<br /> preCheckCode(request);<br /> String username = obtainUsername(request);<br /> String credentials = obtainPassword(request);<br /> if (username == null) {<br /> username = "";<br /> }<br /> if (credentials == null) {<br /> credentials = "";<br /> }<br /> username = username.trim();<br /> //驗(yàn)證碼校驗(yàn)加在哪比較合適?加載過濾器這行注釋之前比較合適,這樣驗(yàn)證碼錯(cuò)誤不用每次都解密一遍密碼<br /> String decrcyptData = RSAUtils.decryptDataOnJava(credentials, privateKey);<br /> String separator = (String) request.getSession().getAttribute("separator");<br /> String cleanSeparator = separator.replace("", "\\");<br /> String[] split = decrcyptData.split(cleanSeparator);<br /> String csrf_token = split[0];//這里可以再校驗(yàn)一次csrf_token,不過能進(jìn)到這里說明已經(jīng)通過csrf校驗(yàn)了<br /> String password= split[1];<br /> authRequest = new UsernamePasswordAuthenticationToken(<br /> username, password);<br /> // Allow subclasses to set the "details" property<br /> setDetails(request, authRequest);<br /> }<br /> return this.getAuthenticationManager().authenticate(authRequest);<br /> }<br /> /<br /> * 校驗(yàn)驗(yàn)證碼<br /> * @param request 請(qǐng)求對(duì)象<br /> */<br /> private void preCheckCode(HttpServletRequest request) {<br /> String code = obtainCode(request);<br /> String noise = obtainNoise(request);<br /> verificationCode.verifyCode(noise,code);<br /> }<br /> @Nullable<br /> protected String obtainPassword(HttpServletRequest request) {<br /> return request.getParameter(passwordParameter);<br /> }<br /> @Nullable<br /> protected String obtainCode(HttpServletRequest request) {<br /> return request.getParameter(codeParameter);<br /> }<br /> @Nullable<br /> protected String obtainNoise(HttpServletRequest request) {<br /> return request.getParameter(noiseParameter);<br /> }<br /> @Nullable<br /> protected String obtainAuthType(HttpServletRequest request) {<br /> return request.getParameter(authType);<br /> }<br /> @Nullable<br /> protected String obtainUsername(HttpServletRequest request) {<br /> return request.getParameter(usernameParameter);<br /> }<br /> protected void setDetails(HttpServletRequest request,<br /> AbstractAuthenticationToken authRequest) {<br /> authRequest.setDetails(authenticationDetailsSource.buildDetails(request));<br /> }<br /> public void setUsernameParameter(String usernameParameter) {<br /> Assert.hasText(usernameParameter, "Username parameter must not be empty or null");<br /> this.usernameParameter = usernameParameter;<br /> }<br /> public void setPasswordParameter(String passwordParameter) {<br /> Assert.hasText(passwordParameter, "Password parameter must not be empty or null");<br /> this.passwordParameter = passwordParameter;<br /> }<br /> public void setPostOnly(boolean postOnly) {<br /> this.postOnly = postOnly;<br /> }<br /> public final String getUsernameParameter() {<br /> return usernameParameter;<br /> }<br /> public final String getPasswordParameter() {<br /> return passwordParameter;<br /> }<br />}
<a name="02gOX"></a>

定制登錄頁面,授權(quán)頁面

@Override<br /> protected void configure(HttpSecurity http) throws Exception {<br /> http.exceptionHandling()<br /> .authenticationEntryPoint(new OAuth2PlusAuthenticationEntryPoint(LOGIN_PAGE)) //指定登錄頁<br /> .accessDeniedHandler(new OAuth2AccessDeniedHandler())<br /> .accessDeniedPage("/403")<br /> .and()<br /> .authorizeRequests()<br /> .antMatchers("/asserts/**").permitAll()<br /> .antMatchers("/oauth/token").permitAll()<br /> .antMatchers("/oauth/authorize").permitAll()<br /> .antMatchers("/code").permitAll()<br /> .antMatchers("/login2").permitAll()<br /> .antMatchers("/loginpage").permitAll()<br /> .antMatchers("/userprofile").access("#oauth2.hasScope('user_info')")<br /> .anyRequest().authenticated()<br /> .and().sessionManagement().sessionAuthenticationErrorUrl(LOGIN_PAGE);//指定登錄頁<br /> addUsernamePasswordPlusAuthenticationFilter(http);<br /> }<br />@Bean<br /> public TokenStoreUserApprovalPlusHandler tokenStoreUserApprovalPlusHandler(TokenStore tokenStore, ClientDetailsService clientDetailsService, AuthorizationEndpoint authorizationEndpoint){<br /> TokenStoreUserApprovalPlusHandler approvalHandler = new TokenStoreUserApprovalPlusHandler();<br /> approvalHandler.setTokenStore(tokenStore);<br /> approvalHandler.setClientDetailsService(clientDetailsService);<br /> approvalHandler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));<br /> authorizationEndpoint.setUserApprovalHandler(approvalHandler);<br /> authorizationEndpoint.setUserApprovalPage("access");//設(shè)置授權(quán)頁<br /> return approvalHandler;<br /> }<br />
<a name="G4hgZ"></a>

oauth2權(quán)限控制

<a name="FL4tQ"></a>

客戶端模式接口權(quán)限控制

通過客戶端模式獲取的token沒有用戶信息,只有client的信息,client 可以通過 scope 字段控制權(quán)限,也可以通過自authority字段控制權(quán)限(權(quán)限用逗號(hào)隔開)

客戶端信息存儲(chǔ)表為 oauth_client_details

通過scope控制權(quán)限<br />oauth2表達(dá)式參考:org.springframework.security.oauth2.provider.expression.OAuth2SecurityExpressionMethods<br />.antMatchers("/userprofile").access("#oauth2.hasScope('user_info')")<br />.antMatchers("/userprofile").access("#oauth2.clientHasRole('ROLE_USER')")//判斷是否有權(quán)限,spring security中role本質(zhì)就是權(quán)限<br /> <br />oauth2權(quán)限表達(dá)式拓展類參考<br />org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityExpressionHandler
<a name="fQ9jW"></a>

用戶接口權(quán)限控制

普通表達(dá)式參考:org.springframework.security.access.expression.SecurityExpressionOperations<br />.antMatchers("/asserts/**").permitAll()<br />.antMatchers("/oauth/token").permitAll()<br />.antMatchers("/oauth/authorize").permitAll()<br />
<a name="8lGx8"></a>

網(wǎng)關(guān)統(tǒng)一鑒權(quán)

<a name="cJk3a"></a>

實(shí)現(xiàn)邏輯

  1. 解析token,獲取token對(duì)應(yīng)的授權(quán)類型,用戶信息,客戶端信息
  2. 根據(jù)權(quán)限規(guī)則判斷客戶端,用戶是否有對(duì)應(yīng)接口的權(quán)限
  3. 有權(quán)限就放行,沒權(quán)限就返回403
    <a name="Lndcz"></a>

spring cloud gateway api網(wǎng)關(guān)簡單實(shí)現(xiàn)

基于spring-security-oauth2實(shí)現(xiàn),(未實(shí)現(xiàn)鑒權(quán)策略)

<a name="2EBUA"></a>

定制認(rèn)證管理器

@Component<br />public class OAuth2AuthenticationManager implements ReactiveAuthenticationManager {<br /> @Autowired<br /> private ResourceServerTokenServices tokenServices;<br /> @Autowired(required = false)<br /> private ClientDetailsService clientDetailsService;<br /> private String resourceId;<br /> @Override<br /> public Mono<Authentication> authenticate(Authentication authentication) {<br /> return Mono.defer(()->{<br /> //獲取token<br /> String token = (String) authentication.getPrincipal();<br /> //從tokenservice中加載用戶憑證<br /> OAuth2Authentication auth = tokenServices.loadAuthentication(token);<br /> if (auth == null) {<br /> throw new InvalidTokenException("Invalid token: " + token);<br /> }<br /> Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();<br /> if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {<br /> throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");<br /> }<br /> //檢查客戶端詳情<br /> checkClientDetails(auth);<br /> if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {<br /> OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();<br /> // Guard against a cached copy of the same details<br /> if (!details.equals(auth.getDetails())) {<br /> // Preserve the authentication details from the one loaded by token services<br /> details.setDecodedDetails(auth.getDetails());<br /> }<br /> }<br /> auth.setDetails(authentication.getDetails());<br /> auth.setAuthenticated(true);<br /> return Mono.just(auth);<br /> });<br /> }<br /> /**<br /> * 校驗(yàn)第三方客戶端<br /> *<br /> * @param auth<br /> */<br /> private void checkClientDetails(OAuth2Authentication auth) {<br /> if (clientDetailsService != null) {<br /> ClientDetails client;<br /> try {<br /> client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());<br /> }<br /> catch (ClientRegistrationException e) {<br /> throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");<br /> }<br /> Set<String> allowed = client.getScope();<br /> for (String scope : auth.getOAuth2Request().getScope()) {<br /> if (!allowed.contains(scope)) {<br /> throw new OAuth2AccessDeniedException(<br /> "Invalid token contains disallowed scope (" + scope + ") for this client");<br /> }<br /> }<br /> }<br /> }<br />}
<a name="URahL"></a>

定制權(quán)限管理器

public class OAuth2ReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {<br /> private static final SecurityExpressionHandler EXPRESSION_HANDLER = new OAuth2WebSecurityExpressionHandler();<br /> private static final AuthorizationDecision ACCESS_DENIED = new AuthorizationDecision(false);<br /> private static final AuthorizationDecision ACCESS_GRANTED = new AuthorizationDecision(true);<br /> private Expression express;<br /> public OAuth2ReactiveAuthorizationManager(Expression express) {<br /> this.express = express;<br /> }<br /> @Override<br /> public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {<br /> return authentication<br /> .defaultIfEmpty(createAnonymouseAuthentication())<br /> .map(a->{<br /> StandardEvaluationContext ctx = createEvaluationContext(a);<br /> return ExpressionUtils.evaluateAsBoolean(express, ctx) ?<br /> ACCESS_GRANTED : ACCESS_DENIED;<br /> });<br /> }<br /> private Authentication createAnonymouseAuthentication(){<br /> return new AnonymousAuthenticationToken("anonymouse","anonymouse",Lists.newArrayList(new SimpleGrantedAuthority("anonymouse")));<br /> }<br /> //創(chuàng)建權(quán)限控制表達(dá)式上下文<br /> private StandardEvaluationContext createEvaluationContext(Authentication authentication){<br /> //創(chuàng)建root對(duì)象<br /> SecurityExpressionRoot root = new WebFluxSecurityExpressRoot(authentication);<br /> root.setPermissionEvaluator(new DenyAllPermissionEvaluator());<br /> root.setTrustResolver( new AuthenticationTrustResolverImpl());<br /> root.setDefaultRolePrefix("ROLE_");<br /> //構(gòu)建上下文對(duì)象,支持oauth2的權(quán)限校驗(yàn)方法 如 .antMatchers("/userprofile").access("#oauth2.hasScope('user_info')")<br /> StandardEvaluationContext ctx = new StandardEvaluationContext();<br /> ctx.setVariable("oauth2", new OAuth2SecurityExpressionMethods(authentication));<br /> ctx.setRootObject(root);<br /> return ctx;<br /> }<br /> public static List<ServerWebExchangeMatcherEntry<ReactiveAuthorizationManager<AuthorizationContext>>> buildDelegatingReactiveAuthorizationManagers(List<ApiEntity> apis){<br /> ExpressionParser expressionParser = EXPRESSION_HANDLER.getExpressionParser();<br /> //TODO 獲取權(quán)限控制元信息 目前匹配器只支持 PathPatternParserServerWebExchangeMatcher<br /> return apis.stream().map(api-> convertServerWebExchangeMatcherEntry(api,expressionParser)).collect(Collectors.toList());<br /> }<br /> public static ServerWebExchangeMatcherEntry<ReactiveAuthorizationManager<AuthorizationContext>> convertServerWebExchangeMatcherEntry(ApiEntity api,ExpressionParser expressionParser){<br /> @NotNull String antPath = api.getAntPath();<br /> @NotNull String method = api.getMethod();<br /> @NotNull String express = api.getExpress();<br /> //解析表達(dá)式<br /> Expression expression = expressionParser.parseExpression(express);<br /> //創(chuàng)建匹配器<br /> ServerWebExchangeMatcher matcher = new PathPatternParserServerWebExchangeMatcher(antPath, method==null?null:HttpMethod.valueOf(method.toUpperCase()));<br /> return new ServerWebExchangeMatcherEntry<>(matcher,new OAuth2ReactiveAuthorizationManager<>(expression));<br /> }<br />}<br />

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。