oauth2實現(xiàn)單點登錄
電子書資源見
https://www.yuque.com/docs/share/37bd8227-eece-435a-9805-87254f4b34e9?# 《oauth2授權》
blog 地址 https://youngboy.vip
oauth2是什么?
OAuth2: OAuth2(開放授權)是一個開放標準,允許用戶授權第三方網(wǎng)站訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方網(wǎng)站或分享他們數(shù)據(jù)的所有內(nèi)容。
舉個栗子:第三方網(wǎng)站登錄,oschina通過gitee登錄
訪問oschina,點擊登錄可以選擇使用gitee登錄,gitee登錄之后,oschina就能獲取到登錄的gitee賬號相關信息<br />根據(jù)以上示例,可以將OAuth2分為四個角色:
- Resource Owner:資源所有者 即上述中的gitee用戶
- Resource Server:資源服務器 即上述中的gitee服務器,提供gitee用戶基本信息給到第三方應用
- Client:第三方應用客戶端 即上述中oschina網(wǎng)站
- Authorication Server:授權服務器 該角色可以理解為管理其余三者關系的中間層
<a name="WlZNm"></a>
四種授權模式的使用場景
| 模式 | 適用場景 |
|---|---|
| 授權碼模式 | 適用于網(wǎng)頁第三方授權 |
| 密碼模式 | 適用于手機等客戶端使用 |
| 客戶端模式 | 適用于授權接口給第三方客戶端 |
| 簡化模式 | 適用于web端應用 |
簡化模式<br />
<br />





<a name="4HUbK"></a>
常見安全問題
<a name="BODdj"></a>
授權碼安全
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)
授權碼模式和簡化模式都應該帶上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ù),登陸頁面接口請求都要加上 CSRF token校驗
- 日志脫敏,避免泄露憑證或token
- 全站https并開啟HSTS防止請求劫持
- 禁止登陸頁面被嵌入iframe
- 避免在url參數(shù)中傳遞token,游覽器的referrer頭可能會泄露token·
- 選擇正確的授權模式,手機端游覽器禁止使用簡化模式,前端頁面禁止使用密碼模式(前端不能保存client_secret)
- 前端后端開啟xss過濾器
- access_token有效期不應該大于通常使用時間
- 使用成熟的oauth2框架
<br /> <br />Tips:如果您想對OAuth2.0開放標準進行擴展閱讀,請參看:OAuth標準(英文) | OAuth維基百科(中文)<br />
<a name="v5NXd"></a>
SSO單點登錄
<a name="dh3KD"></a>
sso 是什么?
SSO的定義是在多個應用系統(tǒng)中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統(tǒng)。<br />權限控制主要分兩步
- Authentication 認證 (證明自己是自己)
- Authorization 授權(證明自己是自己后能夠做什么)
sso 屬于 Authentication 范疇
<a name="6tiZK"></a>
登錄系統(tǒng)
首先,我們要為“登錄”做一個簡要的定義,令后續(xù)的講述更準確。之前的兩篇文章有意無意地混淆了“登錄”與“身份驗證”的說法,因為在本篇之前,不少“傳統(tǒng)Web應用”都將對身份的識別看作整個登錄的過程,很少出現(xiàn)像企業(yè)應用環(huán)境中那樣復雜的情景和需求。但從之前的文章中我們看到,現(xiàn)代Web應用對身份驗證相關的需求已經(jīng)向復雜化發(fā)展了。<br />我們有必要重新認識一下登錄系統(tǒng)。 登錄指的是從識別用戶身份,到允許用戶訪問其權限相應的資源的過程。 舉個例子,在網(wǎng)上買好了票之后去影院觀影的過程就是一個典型的登錄過程:我們先去取票機,輸入驗證碼取票;接著拿到票去影廳檢票進入。取票的過程即身份驗證,它能夠證明我們擁有這張票;而后面檢票的過程,則是授權訪問的過程。之所以要分成這兩個過程,最直接的原因還是業(yè)務形態(tài)本身具有復雜性——如果觀景過程是免費匿名的,也就免去了這些過程。<br />在登錄的過程中,“鑒權”與“授權”是兩個最關鍵的過程。接下來要介紹的一些技術和實踐,也包含在這兩個方面中。雖然現(xiàn)代Web應用的登錄需求比較復雜,但只要處理好了鑒權和授權兩個方面,其余各個方面的問題也將迎刃而解。在現(xiàn)代Web應用的登錄工程實踐中,需要結(jié)合傳統(tǒng)Web應用的典型實踐,以及一些新的思路,才能既解決好登錄需求,又能符合Web的輕量級架構思路。
解析常見的登錄場景
在簡單的Web系統(tǒng)中,典型的鑒權也就是要求用戶輸入并比對用戶名和密碼的過程,而授權則是確保會話Cookie存在。而在稍微復雜的Web系統(tǒng)中,則需要考慮多種鑒權方式,以及多種授權場景。上一篇文章中所述的“多種登錄方式”和“雙因子鑒權”就是多種鑒權方式的例子。有經(jīng)驗的人經(jīng)常調(diào)侃說,只要理解了鑒權與授權,就能清晰地理解登錄系統(tǒng)了。不光如此,這也是安全登錄系統(tǒng)的基礎所在。<br />鑒權的形式豐富多彩,有傳統(tǒng)的用戶名密碼對、客戶端證書,有人們越來越熟悉的第三方登錄、手機驗證,以及新興的掃碼和指紋等方式,它們都能用于對用戶的身份進行識別。在成功識別用戶之后,在用戶訪問資源或執(zhí)行操作之前,我們還需要對用戶的操作進行授權。<br />在一些特別簡單的情形中——用戶一經(jīng)識別,就可以無限制地訪問資源、執(zhí)行所有操作——系統(tǒng)直接對所有“已登錄的人”放行。比如高速公路收費站,只要車輛有合法的號牌即可放行,不需要給駕駛員發(fā)一張用于指示“允許行駛的方向或時間”的票據(jù)。除了這類特別簡單的情形之外,授權更多時候是比較復雜的工作。<br />在單一的傳統(tǒng)Web應用中,授權的過程通常由會話Cookie來完成——只要服務器發(fā)現(xiàn)瀏覽器攜帶了對應的Cookie,即允許用戶訪問資源、執(zhí)行操作。而在瀏覽器之外,例如在Web API調(diào)用、移動應用和富 Web 應用等場景中,要提供安全又不失靈活的授權方式,就需要借助令牌技術。
令牌
令牌是一個在各種介紹登錄技術的文章中常被提及的概念,也是現(xiàn)代Web應用系統(tǒng)中非常關鍵的技術。令牌是一個非常簡單的概念,它指的是在用戶通過身份驗證之后,為用戶分配的一個臨時憑證。在系統(tǒng)內(nèi)部,各個子系統(tǒng)只需要以統(tǒng)一的方式正確識別和處理這個憑證即可完成對用戶的訪問和操作進行授權。在上文所提到的例子中,電影票就是一個典型的令牌。影廳門口的工作人員只需要確認來客手持印有對應場次的電影票即視為合法訪問,而不需要理會客戶是從何種渠道取得了電影票(比如自行購買、朋友贈予等),電影票在本場次范圍內(nèi)可以持續(xù)使用(比如可以中場出去休息等)、過期作廢。通過電影票這樣一個簡單的令牌機制,電影票的出售渠道可以豐富多樣,檢票人員的工作卻仍然簡單輕松。
OAuth 2、Open ID Connect
令牌在廣為使用的OAuth技術中被采用來完成授權的過程。OAuth是一種開放的授權模型,它規(guī)定了一種供資源擁有方與消費方之間簡單又直觀的交互方法,即從消費方向資源擁有方發(fā)起使用AccessToken(訪問令牌)簽名的HTTP請求。這種方式讓消費方應用在無需(也無法)獲得用戶憑據(jù)的情況下,只要用戶完成鑒權過程并同意消費方以自己的身份調(diào)用數(shù)據(jù)和操作,消費方就可以獲得能夠完成功能的訪問令牌。OAuth簡單的流程和自由的編程模型讓它很好地滿足了開放平臺場景中授權第三方應用使用用戶數(shù)據(jù)的需求。不少互聯(lián)網(wǎng)公司建設開放平臺,將它們的用戶在其平臺上的數(shù)據(jù)以 API 的形式開放給第三方應用來使用,從而讓用戶享受更豐富的服務。<br />OAuth在各個開放平臺的成功使用,令更多開發(fā)者了解到它,并被它簡單明確的流程所吸引。此外,OAuth協(xié)議規(guī)定的是授權模型,并不規(guī)定訪問令牌的數(shù)據(jù)格式,也不限制在整個登錄過程中需要使用的鑒權方法。人們很快發(fā)現(xiàn),只要對OAuth進行合適的利用即可將其用于各種自有系統(tǒng)中的場景。例如,將 Web 服務視作資源擁有方,而將富Web應用或者移動應用視作消費方應用,就與開放平臺的場景完全吻合。<br />另一個大量實踐的場景是基于OAuth的單點登錄。OAuth并沒有對鑒權的部分做規(guī)定,也不要求在握手交互過程中包含用戶的身份信息,因此它并不適合作為單點登錄系統(tǒng)來使用。然而,由于OAuth的流程中隱含了鑒權的步驟,因而仍然有不少開發(fā)者將這一鑒權的步驟用作單點登錄系統(tǒng),這也儼然衍生成為一種實踐模式。更有人將這個實踐進行了標準化,它就是Open ID Connect——基于OAuth的身份上下文協(xié)議,通過它即可以JWT的形式安全地在多個應用中共享用戶身份。接下來,只要讓鑒權服務器支持較長的會話時間,就可以利用OAuth為多個業(yè)務系統(tǒng)提供單點登錄功能了。<br />我們還沒有討論OAuth對鑒權系統(tǒng)的影響。實際上,OAuth對鑒權系統(tǒng)沒有影響,在它的框架內(nèi),只是假設已經(jīng)存在了一種可用于識別用戶的有效機制,而這種機制具體是怎么工作的,OAuth并不關心。因此我們既可以使用用戶名密碼(大多數(shù)開放平臺提供商都是這種方式),也可以使用掃碼登錄來識別用戶,更可以提供諸如“記住密碼”,或者雙因子驗證等其他功能。<br />
匯總
上面羅列了大量術語和解釋,那么具體到一個典型的Web系統(tǒng)中,又應該如何對安全系統(tǒng)進行設計呢?綜合這些技術,從端到云,從Web門戶到內(nèi)部服務,本文給出如下架構方案建議:<br />推薦為整個應用的所有系統(tǒng)、子系統(tǒng)都部署全程的HTTPS,如果出于性能和成本考慮做不到,那么至少要保證在用戶或設備直接訪問的Web應用中全程使用HTTPS。<br />用不同的系統(tǒng)分別用作身份和登錄,以及業(yè)務服務。當用戶登錄成功之后,使用OpenID Connect向業(yè)務系統(tǒng)頒發(fā)JWT格式的訪問令牌和身份信息。如果需要,登錄系統(tǒng)可以提供多種登錄方式,或者雙因子登錄等增強功能。作為安全令牌服務(STS),它還負責頒發(fā)、刷新、驗證和取消令牌的操作。在身份驗證的整個流程的每一個步驟,都使用OAuth及JWT中內(nèi)置的機制來驗證數(shù)據(jù)的來源方是可信的:登錄系統(tǒng)要確保登錄請求來自受認可的業(yè)務應用,而業(yè)務在獲得令牌之后也需要驗證令牌的有效性。<br />在Web頁面應用中,應該申請時效較短的令牌。將獲取到的令牌向客戶端頁面中以httponly的方式寫入會話Cookie,以用于后續(xù)請求的授權;在后緒請求到達時,驗證請求中所攜帶的令牌,并延長其時效?;贘WT自包含的特性,輔以完備的簽名認證,Web 應用無需額外地維護會話狀態(tài)。<br />在富客戶端Web應用(單頁應用),或者移動端、客戶端應用中,可按照應用業(yè)務形態(tài)申請時效較長的令牌,或者用較短時效的令牌、配合專用的刷新令牌使用。<br />在Web應用的子系統(tǒng)之間,調(diào)用其他子服務時,可靈活使用“應用程序身份”(如果該服務完全不直接對用戶提供調(diào)用),或者將用戶傳入的令牌直接傳遞到受調(diào)用的服務,以這種方式進行授權。各個業(yè)務系統(tǒng)可結(jié)合基于角色的訪問控制(RBAC)開發(fā)自有專用權限系統(tǒng)。<br />http://insights.thoughtworkers.org/traditional-web-app-authentication/
<a name="axw0U"></a>
各大開發(fā)平臺oauth2的使用
- qq開發(fā)平臺 https://wiki.open.qq.com/wiki/website/OAuth2.0%E7%AE%80%E4%BB%8B
- gitee平臺 https://gitee.com/api/v5/oauth_doc#/
- 微信平臺 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
<a name="bCLBa"></a>
cas 解決方案
cas 是一種協(xié)議,cas server 是開源的實現(xiàn)
官網(wǎng)地址 https://www.apereo.org/projects/cas
<a name="Y6Vq0"></a>
cas 協(xié)議
CAS協(xié)議是一種簡單且功能強大的基于票證(ticket)的協(xié)議。它涉及一個或多個客戶端和一臺服務器。中央身份驗證服務(CAS)是Web的單點登錄/單點退出協(xié)議。用戶向中央CAS Server應用程序提供一次憑據(jù)(例如用戶ID和密碼),就可以訪問多個應用程序。客戶端嵌入在CASified應用程序中(稱為“ CAS服務”),而CAS服務器是獨立組件:
- CAS服務器負責驗證用戶并授予訪問應用程序
- CAS客戶保護CAS應用程序和檢索CAS服務器的授權用戶的身份。
關鍵概念:
- TGT存儲在TGCcookie中的(“票證授予票證”)代表用戶的SSO會話。
- ST(服務票據(jù)),作為傳輸GET參數(shù)的URL,代表由CAS服務器授予訪問CASified應用程序?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 不僅僅是對cas協(xié)議的實現(xiàn),cas server 還實現(xiàn)了 oidc smal 等協(xié)議
<a name="c0P7Q"></a>
部署要求
- java版本要求jdk11以上
<a name="eubQf"></a>
優(yōu)點
- 功能豐富,支持多種認證協(xié)議實現(xiàn) cas oidc smal
- 支持協(xié)議代理使用pac4j實現(xiàn)
- 客戶端支持齊全,支持spring security spring boot
- 官方文檔比較全
<a name="gWuaN"></a>
缺點
- 如果使用cas協(xié)議,對已有系統(tǒng)改動比較大,改動成本高
- 框架比較復雜
- java版本要求高
<a name="OziOJ"></a>
keycloak 解決方案
Keycloak 是一個為瀏覽器和 RESTful Web 服務提供 SSO 的集成?;贠IDC規(guī)范。最開始是面向 JBoss 和 Wildfly 通訊<br />源碼地址是:https://github.com/keycloak/keycloak/
<a name="6MWhJ"></a>
部署要求
- java8
<a name="ZPzFD"></a>
優(yōu)點
- 支持ladp等中間件
- 功能豐富,有多種認證協(xié)議實現(xiàn) cas oidc smal
- 自帶有完善的用戶角色組織管理系統(tǒng),同時還有自帶的web控制臺管理用戶角色組織客戶端等信息
- 客戶端支持齊全,支持spring security spring boot
<a name="2czN9"></a>
缺點
- 改動成本高
- 中間件比較復雜
- 已有用戶體系的系統(tǒng)不好改造
- 文檔不多,沒有很多人使用
<a name="BwavC"></a>
spring security oauth2 解決方案
oauth2 只是授權協(xié)議并不包含完整的認證協(xié)議,需要在oauth2的基礎上進行改造才能使用,需要寫一些代碼,亦可以使用oidc協(xié)議可以簡單理解為(openid + oauth2),改動后以前的前端登陸實現(xiàn)都需要從項目中遷移出來到用戶認證中心,用戶統(tǒng)一到認證中心登陸(和qq github 等平臺一樣,上面的解決方案也差不多)
<a name="33Elk"></a>
優(yōu)點
可以靈活添加功能改動成本較小如果使用oidc協(xié)議,接入方可以選擇標準的開源成熟的 oidc sdk,不需要再自己寫sdk
<a name="8pe7C"></a>
缺點
需要拓展一些功能(可以參考cas中的實現(xiàn))
<a name="HIGF0"></a>
oauth2 sso 簡單認證流程
| 代號 | 描述 |
|---|---|
| UAA | 認證中心 |
| 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>
權限控制問題
Q: 接入方系統(tǒng)有現(xiàn)成的用戶權限怎么控制?
A: 單點登陸只管認證的問題,不需要考慮接入方的權限怎么控制
Q: 接入方系統(tǒng)怎么控制權限?
A: 使用用戶詳情接口獲取用戶詳細信息,使用client的scope 或者 authorities
Q: 接入方系統(tǒng)與接入方系統(tǒng)間需要相互調(diào)用嗎?
A: 不需要相互調(diào)用
<a name="XBFaU"></a>
單系統(tǒng)登出功能,全局登出功能
- “SSO” 是指單點登錄。
- “SLO” 是指單一注銷。
<br />
目前系統(tǒng)只實現(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 認證流程 http://www.sohu.com/a/206818578_468635<br />
<a name="rtbHT"></a>
基于spring security oauth2單點登錄的實現(xiàn)
<a name="mkcW7"></a>
實現(xiàn)功能點
- 用戶認證(用戶名密碼,釘釘掃碼)
- 跨域名單點登錄
- 統(tǒng)一登出(參考了oidc實現(xiàn))
- 客戶端token獲取
- password模式token獲取
- 簡化模式token獲取
<a name="2NpzV"></a>
spring security oauth2配置項
<a name="I3qAH"></a>
配置授權服務
@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);//配置客戶端詳情服務<br /> }<br /> @Override<br /> public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {<br /> //設置token的訪問權限為permitAll 檢查token 的權限為 isAuthenticated() 在httpSecurity 中配置權限是沒用的<br /> oauthServer<br /> .tokenKeyAccess("permitAll()")//設置oauth/token_key 的訪問權限<br /> .checkTokenAccess("isAuthenticated()") //設置checkToken 接口的訪問權限<br /> .allowFormAuthenticationForClients();//設置允許Client通過Form認證,否則就只能使用basic認證,from認證相關過濾器為 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 /> //配置自定義授權碼服務<br /> endpoints.authorizationCodeServices(authorizationCodeServices);<br /> endpoints<br /> .tokenStore(tokenStore())//配置token儲存<br /> .tokenEnhancer(tokenEnhancerChain)//配置token處理鏈<br /> .reuseRefreshTokens(false)<br /> //設置自定義的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 拓展項
<a name="pm5lV"></a>
定制用戶確認授權頁面
@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");//設置授權頁面為access<br /> return approvalHandler;<br /> }<br />
<a name="FYBFb"></a>
定制授權碼生成服務實現(xiàn)
@Component<br />public class RedisAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {<br /> @Autowired<br /> private RedisTemplate redisTemplate;<br /> /**<br /> * 存儲code到redis,并設置過期時間,10分鐘
<br /> * value為OAuth2Authentication序列化后的字節(jié)
<br /> * 因為OAuth2Authentication沒有無參構造函數(shù)
<br /> * redisTemplate.opsForValue().set(key, value, timeout, unit);<br /> * 這種方式直接存儲的話,redisTemplate.opsForValue().get(key)的時候有些問題,<br /> * 所以這里采用最底層的方式存儲,get的時候也用最底層的方式獲取<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 /> //查詢當前用戶登錄的客戶端<br /> Object principal = authentication.getPrincipal();<br /> if(principal instanceof SystemUser){<br /> String userName = ((SystemUser) principal).getUsername();<br /> //通過用戶登錄名查詢在線的客戶端<br /> Set<TokenAndClientId> clients = redisTokenStoreEnhance.findTokenAndClientIdByUserName(userName);<br /> //構建登出地址<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 /> //清空所有的授權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>
定制認證授權入口
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>
定制用戶認證過濾器
public class UsernamePasswordPlusAuthenticationFilter extends AbstractAuthenticationProcessingFilter {<br /> /*<br /> * 認證類型 用戶名密碼認證 釘釘掃碼認證<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 /> //用戶名密碼認證<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 /> //驗證碼校驗加在哪比較合適?加載過濾器這行注釋之前比較合適,這樣驗證碼錯誤不用每次都解密一遍密碼<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];//這里可以再校驗一次csrf_token,不過能進到這里說明已經(jīng)通過csrf校驗了<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 /> * 校驗驗證碼<br /> * @param request 請求對象<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>
定制登錄頁面,授權頁面
@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");//設置授權頁<br /> return approvalHandler;<br /> }<br />
<a name="G4hgZ"></a>
oauth2權限控制
<a name="FL4tQ"></a>
客戶端模式接口權限控制
通過客戶端模式獲取的token沒有用戶信息,只有client的信息,client 可以通過 scope 字段控制權限,也可以通過自authority字段控制權限(權限用逗號隔開)
客戶端信息存儲表為 oauth_client_details
通過scope控制權限<br />oauth2表達式參考:org.springframework.security.oauth2.provider.expression.OAuth2SecurityExpressionMethods<br />.antMatchers("/userprofile").access("#oauth2.hasScope('user_info')")<br />.antMatchers("/userprofile").access("#oauth2.clientHasRole('ROLE_USER')")//判斷是否有權限,spring security中role本質(zhì)就是權限<br /> <br />oauth2權限表達式拓展類參考<br />org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityExpressionHandler
<a name="fQ9jW"></a>
用戶接口權限控制
普通表達式參考: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)關統(tǒng)一鑒權
<a name="cJk3a"></a>
實現(xiàn)邏輯
- 解析token,獲取token對應的授權類型,用戶信息,客戶端信息
- 根據(jù)權限規(guī)則判斷客戶端,用戶是否有對應接口的權限
- 有權限就放行,沒權限就返回403
<a name="Lndcz"></a>
spring cloud gateway api網(wǎng)關簡單實現(xiàn)
基于spring-security-oauth2實現(xiàn),(未實現(xiàn)鑒權策略)
<a name="2EBUA"></a>
定制認證管理器
@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 /> * 校驗第三方客戶端<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>
定制權限管理器
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)建權限控制表達式上下文<br /> private StandardEvaluationContext createEvaluationContext(Authentication authentication){<br /> //創(chuàng)建root對象<br /> SecurityExpressionRoot root = new WebFluxSecurityExpressRoot(authentication);<br /> root.setPermissionEvaluator(new DenyAllPermissionEvaluator());<br /> root.setTrustResolver( new AuthenticationTrustResolverImpl());<br /> root.setDefaultRolePrefix("ROLE_");<br /> //構建上下文對象,支持oauth2的權限校驗方法 如 .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 獲取權限控制元信息 目前匹配器只支持 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 /> //解析表達式<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 />