一雪侥、單點(diǎn)登錄SSO介紹
??目前每家企業(yè)或者平臺(tái)都存在不止一套系統(tǒng)碗殷,由于歷史原因每套系統(tǒng)采購(gòu)于不同廠(chǎng)商,所以系統(tǒng)間都是相互獨(dú)立的校镐,都有自己的用戶(hù)鑒權(quán)認(rèn)證體系亿扁,當(dāng)用戶(hù)進(jìn)行登錄系統(tǒng)時(shí),不得不記住每套系統(tǒng)的用戶(hù)名密碼鸟廓,同時(shí)从祝,管理員也需要為同一個(gè)用戶(hù)設(shè)置多套系統(tǒng)登錄賬號(hào),這對(duì)系統(tǒng)的使用者來(lái)說(shuō)顯然是不方便的引谜。我們期望的是如果存在多個(gè)系統(tǒng)牍陌,只需要登錄一次就可以訪(fǎng)問(wèn)多個(gè)系統(tǒng),只需要在其中一個(gè)系統(tǒng)執(zhí)行注銷(xiāo)登錄操作员咽,則所有的系統(tǒng)都注銷(xiāo)登錄毒涧,無(wú)需重復(fù)操作,這就是單點(diǎn)登錄(Single Sign On 簡(jiǎn)稱(chēng)SSO)系統(tǒng)實(shí)現(xiàn)的功能贝室。
??單點(diǎn)登錄是系統(tǒng)功能的定義娇掏,而實(shí)現(xiàn)單點(diǎn)登錄功能唬党,目前開(kāi)源且流行的有CAS和OAuth2兩種方式装蓬,過(guò)去我們用的最多的是CAS怖糊,現(xiàn)在隨著SpringCloud的流行,更多人選擇使用SpringSecurity提供的OAuth2認(rèn)證授權(quán)服務(wù)器實(shí)現(xiàn)單點(diǎn)登錄功能峡迷。
??OAuth2是一種授權(quán)協(xié)議的標(biāo)準(zhǔn)银伟,任何人都可以基于這個(gè)標(biāo)準(zhǔn)開(kāi)發(fā)Oauth2授權(quán)服務(wù)器你虹,現(xiàn)在百度開(kāi)放平臺(tái)、騰訊開(kāi)放平臺(tái)等大部分的開(kāi)放平臺(tái)都是基于OAuth2協(xié)議實(shí)現(xiàn)彤避, OAuth2.0定義了四種授權(quán)類(lèi)型傅物,最新版OAuth2.1協(xié)議定義了七種授權(quán)類(lèi)型,其中有兩種因安全問(wèn)題已不再建議使用:
【OAuth2.1 建議使用的五種授權(quán)類(lèi)型】
- Authorization Code 【授權(quán)碼授權(quán)】:用戶(hù)通過(guò)授權(quán)服務(wù)器重定向URL返回到客戶(hù)端后琉预,應(yīng)用程序從URL中獲取授權(quán)碼董饰,并使用授權(quán)碼請(qǐng)求訪(fǎng)問(wèn)令牌。
- PKCE【Proof Key for Code Exchange 授權(quán)碼交換證明密鑰】:授權(quán)碼類(lèi)型的擴(kuò)展圆米,用于防止CSRF和授權(quán)碼注入攻擊尖阔。
- Client Credentials【客戶(hù)端憑證授權(quán)】:直接由客戶(hù)端使用客戶(hù)端 ID 和客戶(hù)端密鑰向授權(quán)服務(wù)器請(qǐng)求訪(fǎng)問(wèn)令牌,無(wú)需用戶(hù)授權(quán)榨咐,通常用與系統(tǒng)和系統(tǒng)之間的授權(quán)。
- Device Code【設(shè)備代碼授權(quán)】:用于無(wú)瀏覽器或輸入受限的設(shè)備谴供,使用提前獲取好的設(shè)備代碼獲取訪(fǎng)問(wèn)令牌块茁。
- Refresh Token【刷新令牌授權(quán)】:當(dāng)訪(fǎng)問(wèn)令牌失效時(shí),可以通過(guò)刷新令牌獲取訪(fǎng)問(wèn)令牌桂肌,不需要用戶(hù)進(jìn)行交互数焊。
【OAuth2.1 不建議/禁止使用的兩種授權(quán)類(lèi)型】
- Implicit Flow【隱式授權(quán)】:隱式授權(quán)是以前推薦用于本機(jī)應(yīng)用程序和 JavaScript 應(yīng)用程序的簡(jiǎn)化 OAuth 流程,其中訪(fǎng)問(wèn)令牌立即返回崎场,無(wú)需額外的授權(quán)代碼交換步驟佩耳。其通過(guò)HTTP重定向直接返回訪(fǎng)問(wèn)令牌,存在很大的風(fēng)險(xiǎn)谭跨,不建議使用干厚,有些授權(quán)服務(wù)器直接禁止使用此授權(quán)類(lèi)型。
- Password Grant【密碼授權(quán)】:客戶(hù)端通過(guò)用戶(hù)名密碼向授權(quán)服務(wù)器獲取訪(fǎng)問(wèn)令牌螃宙。因客戶(hù)端需收集用戶(hù)名和密碼蛮瞄,所以不建議使用,最新的 OAuth 2 安全最佳實(shí)踐完全不允許密碼授權(quán)谆扎。
【SpringSecurity對(duì)OAuth2協(xié)議的支持】:
??通過(guò)SpringSecurity官網(wǎng)可知挂捅,通過(guò)長(zhǎng)期的對(duì)OAuth2的支持,以及對(duì)實(shí)際業(yè)務(wù)的情景考慮堂湖,大多數(shù)的系統(tǒng)都不需要授權(quán)服務(wù)器闲先,所以,Spring官方不再推薦使用spring-security-oauth2无蜂,SpringSecurity逐漸將spring-security-oauth2中的OAuth2登錄伺糠、客戶(hù)端、資源服務(wù)器等功能抽取出來(lái)酱讶,集成在SpringSecurity中退盯,并單獨(dú)新建spring-authorization-server項(xiàng)目實(shí)現(xiàn)授權(quán)服務(wù)器功能。
??目前我們了解最多的是Spring Security OAuth對(duì)OAuth2協(xié)議的實(shí)現(xiàn)和支持,這里需要區(qū)分Spring Security OAuth和Spring Security是兩個(gè)項(xiàng)目渊迁,過(guò)去OAth2相關(guān)功能都在Spring Security OAuth項(xiàng)目中實(shí)現(xiàn)慰照,但是自SpringSecurity5.X開(kāi)始,SpringSecurity項(xiàng)目開(kāi)始逐漸增加Spring Security OAuth中的功能琉朽,自SpringSecurity5.2開(kāi)始毒租,添加了OAuth 2.0 登錄, 客戶(hù)端, 資源服務(wù)器的功能。但授權(quán)服務(wù)器的功能箱叁,并不打算集成在SpringSecurity項(xiàng)目中墅垮,而是新建了spring-authorization-server項(xiàng)目作為單獨(dú)的授權(quán)服務(wù)器:詳細(xì)介紹。spring-security實(shí)現(xiàn)的是OAuth2.1協(xié)議耕漱,spring-security-oauth2實(shí)現(xiàn)的是OAuth2.0協(xié)議算色。
??Spring未來(lái)的計(jì)劃是將 Spring Security OAuth 中當(dāng)前的所有功能構(gòu)建到 Spring Security 5.x 中。 在 Spring Security 達(dá)到與 Spring Security OAuth 的功能對(duì)等之后螟够,他們將繼續(xù)支持錯(cuò)誤和安全修復(fù)至少一年灾梦。
【GitEgg框架單點(diǎn)登錄實(shí)現(xiàn)計(jì)劃】:
??因spring-authorization-server目前最新發(fā)布版本0.2.3,部分功能仍在不斷的修復(fù)和完善妓笙,還不足以應(yīng)用到實(shí)際生產(chǎn)環(huán)境中若河,所以,我們目前使用spring-security-oauth2作為授權(quán)服務(wù)器寞宫,待后續(xù)spring-authorization-server發(fā)布穩(wěn)定版本后萧福,再進(jìn)行遷移升級(jí)。
【spring-security-oauth2默認(rèn)實(shí)現(xiàn)的授權(quán)類(lèi)型】:
- 隱式授權(quán)(Implicit Flow)【spring-authorization-server不再支持此類(lèi)型】
- 授權(quán)碼授權(quán)(Authorization Code)
- 密碼授權(quán)(Password Grant)【spring-authorization-server不再支持此類(lèi)型】
- 客戶(hù)端憑證授權(quán)(Client Credentials)
- 刷新令牌授權(quán) (Refresh Token)
??在GitEgg微服務(wù)框架中辈赋,gitegg-oauth已經(jīng)引入了spring-security-oauth2鲫忍,代碼中使用了了Oauth2的密碼授權(quán)和刷新令牌授權(quán),并且自定義擴(kuò)展了【短信驗(yàn)證碼授權(quán)類(lèi)型】和【圖形驗(yàn)證碼授權(quán)】钥屈,這其實(shí)是密碼授權(quán)的擴(kuò)展授權(quán)類(lèi)型饲窿。
??目前,基本上所有的SpringCloud微服務(wù)授權(quán)方式都是使用的OAuth2密碼授權(quán)模式獲取token焕蹄,可能你會(huì)有疑惑逾雄,為什么上面最新的Oauth2協(xié)議已經(jīng)不建議甚至是禁止使用密碼授權(quán)類(lèi)型了,而我們GitEgg框架的系統(tǒng)管理界面還要使用密碼授權(quán)模式來(lái)獲取token腻脏?因?yàn)椴唤ㄗh使用密碼授權(quán)類(lèi)型的原因是第三方客戶(hù)端會(huì)收集用戶(hù)名密碼鸦泳,存在安全風(fēng)險(xiǎn)。而在我們這里永品,我們的客戶(hù)端是自有系統(tǒng)管理界面做鹰,不是第三方客戶(hù)端,所有的用戶(hù)名密碼都是我們自有系統(tǒng)的用戶(hù)名密碼鼎姐,只要做好系統(tǒng)安全防護(hù)钾麸,就可最大限度的避免用戶(hù)名密碼泄露給第三方的風(fēng)險(xiǎn)更振。
??在使用spring-security-oauth2實(shí)現(xiàn)單點(diǎn)登錄之前,首先我們一定要搞清楚單點(diǎn)登錄SSO饭尝、OAuth2肯腕、spring-security-oauth2的區(qū)別和聯(lián)系:
- 單點(diǎn)登錄SSO是一種系統(tǒng)登錄解決方案的定義,企業(yè)內(nèi)部系統(tǒng)登錄以及互聯(lián)網(wǎng)上第三方QQ钥平、微信实撒、GitHub登錄等都是單點(diǎn)登錄。
- OAuth2是一種系統(tǒng)授權(quán)協(xié)議涉瘾,它包含多種授權(quán)類(lèi)型知态,我們可以使用授權(quán)碼授權(quán)和刷新令牌授權(quán)兩種授權(quán)類(lèi)型來(lái)實(shí)現(xiàn)單點(diǎn)登錄功能。
- spring-security-oauth2是對(duì)OAuth2協(xié)議中授權(quán)類(lèi)型的具體實(shí)現(xiàn)立叛,也是我們實(shí)現(xiàn)單點(diǎn)登錄功能實(shí)際用到的代碼负敏。
二、SpringSecurity單點(diǎn)登錄服務(wù)端和客戶(hù)端實(shí)現(xiàn)流程解析
單點(diǎn)登錄業(yè)務(wù)流程時(shí)序圖:
A系統(tǒng)(單點(diǎn)登錄客戶(hù)端)首次訪(fǎng)問(wèn)受保護(hù)的資源觸發(fā)單點(diǎn)登錄流程說(shuō)明
- 1秘蛇、用戶(hù)通過(guò)瀏覽器訪(fǎng)問(wèn)A系統(tǒng)被保護(hù)的資源鏈接
- 2原在、A系統(tǒng)判斷當(dāng)前會(huì)話(huà)是否登錄,如果沒(méi)有登錄則跳轉(zhuǎn)到A系統(tǒng)登錄地址/login
- 3彤叉、A系統(tǒng)首次接收到/login請(qǐng)求時(shí)沒(méi)有state和code參數(shù),此時(shí)A系統(tǒng)拼接系統(tǒng)配置的單點(diǎn)登錄服務(wù)器授權(quán)url村怪,并重定向至授權(quán)鏈接秽浇。
- 4、單點(diǎn)登錄服務(wù)器判斷此會(huì)話(huà)是否登錄甚负,如果沒(méi)有登錄柬焕,那么返回單點(diǎn)登錄服務(wù)器的登錄頁(yè)面。
- 5梭域、用戶(hù)在登錄頁(yè)面填寫(xiě)用戶(hù)名斑举、密碼等信息執(zhí)行登錄操作。
- 6病涨、單點(diǎn)登錄服務(wù)器校驗(yàn)用戶(hù)名富玷、密碼并將登錄信息設(shè)置到上下文會(huì)話(huà)中。
- 7既穆、單點(diǎn)登錄服務(wù)器重定向到A系統(tǒng)的/login鏈接赎懦,此時(shí)鏈接帶有code和state參數(shù)。
- 8幻工、A系統(tǒng)再次接收到/login請(qǐng)求励两,此請(qǐng)求攜帶state和code參數(shù),系統(tǒng)A通過(guò)OAuth2RestTemplate請(qǐng)求單點(diǎn)登錄服務(wù)端/oauth/token接口獲取token囊颅。
- 9当悔、A系統(tǒng)獲取到token后傅瞻,首先會(huì)對(duì)token進(jìn)行解析,并使用配置的公鑰對(duì)token進(jìn)行校驗(yàn)(非對(duì)稱(chēng)加密)盲憎,如果校驗(yàn)通過(guò)嗅骄,則將token設(shè)置到上下文,下次訪(fǎng)問(wèn)請(qǐng)求時(shí)直接從上下文中獲取焙畔。
- 10掸读、A系統(tǒng)處理完上下問(wèn)會(huì)話(huà)之后重定向到登錄前請(qǐng)求的受保護(hù)資源鏈接。
B系統(tǒng)(單點(diǎn)登錄客戶(hù)端)訪(fǎng)問(wèn)受保護(hù)的資源流程說(shuō)明
- 1宏多、用戶(hù)通過(guò)瀏覽器訪(fǎng)問(wèn)B系統(tǒng)被保護(hù)的資源鏈接
- 2儿惫、B系統(tǒng)判斷當(dāng)前會(huì)話(huà)是否登錄,如果沒(méi)有登錄則跳轉(zhuǎn)到B系統(tǒng)登錄地址/login
- 3伸但、B系統(tǒng)首次接收到/login請(qǐng)求時(shí)沒(méi)有state和code參數(shù)肾请,此時(shí)B系統(tǒng)拼接系統(tǒng)配置的單點(diǎn)登錄服務(wù)器授權(quán)url,并重定向至授權(quán)鏈接更胖。
- 4铛铁、單點(diǎn)登錄服務(wù)器判斷此會(huì)話(huà)是否登錄,因上面訪(fǎng)問(wèn)A系統(tǒng)時(shí)登陸過(guò)却妨,所以此時(shí)不會(huì)再返回登錄界面饵逐。
- 5、單點(diǎn)登錄服務(wù)器重定向到B系統(tǒng)的/login鏈接彪标,此時(shí)鏈接帶有code和state參數(shù)倍权。
- 6、B系統(tǒng)再次接收到/login請(qǐng)求捞烟,此請(qǐng)求攜帶state和code參數(shù)薄声,系統(tǒng)B通過(guò)OAuth2RestTemplate請(qǐng)求單點(diǎn)登錄服務(wù)端/oauth/token接口獲取token。
- 7题画、B系統(tǒng)獲取到token后默辨,首先會(huì)對(duì)token進(jìn)行解析,并使用配置的公鑰對(duì)token進(jìn)行校驗(yàn)(非對(duì)稱(chēng)加密)苍息,如果校驗(yàn)通過(guò)缩幸,則將token設(shè)置到上下文,下次訪(fǎng)問(wèn)請(qǐng)求時(shí)直接從上下文中獲取竞思。
- 8桌粉、B系統(tǒng)處理完上下問(wèn)會(huì)話(huà)之后重定向到登錄前請(qǐng)求的受保護(hù)資源鏈接。
spring-security-oauth2 單點(diǎn)登錄代碼實(shí)現(xiàn)流程說(shuō)明:
- 1衙四、用戶(hù)通過(guò)瀏覽器訪(fǎng)問(wèn)單點(diǎn)登錄被保護(hù)的資源鏈接
- 2铃肯、SpringSecurity通過(guò)上下文判斷是否登錄(SpringSecurity單點(diǎn)登錄服務(wù)端和客戶(hù)端默認(rèn)都是基于session的),如果沒(méi)有登錄則跳轉(zhuǎn)到單點(diǎn)登錄客戶(hù)端地址/login
- 3传蹈、單點(diǎn)登錄客戶(hù)端OAuth2ClientAuthenticationProcessingFilter攔截器通過(guò)上下文獲取token押逼,因第一次訪(fǎng)問(wèn)單點(diǎn)登錄客戶(hù)端/login時(shí)步藕,沒(méi)有code和state參數(shù),所以?huà)伋鯱serRedirectRequiredException異常
- 4挑格、單點(diǎn)登錄客戶(hù)端捕獲UserRedirectRequiredException異常咙冗,并根據(jù)配置文件中的配置,組裝并跳轉(zhuǎn)到單點(diǎn)登錄服務(wù)端的授權(quán)鏈接/oauth/authorize漂彤,鏈接及請(qǐng)求中會(huì)帶相關(guān)配置參數(shù)
- 5雾消、單點(diǎn)登錄服務(wù)端收到授權(quán)請(qǐng)求,根據(jù)session判斷是否此會(huì)話(huà)是否登錄挫望,如果沒(méi)有登錄則跳轉(zhuǎn)到單點(diǎn)登錄服務(wù)器的統(tǒng)一登錄界面(單點(diǎn)登錄服務(wù)端也是根據(jù)session判斷是否登錄的立润,在這里為了解決微服務(wù)的session集群共享問(wèn)題,引入了spring-session-data-redis)
- 6媳板、用戶(hù)完成登錄操作后桑腮,單點(diǎn)登錄服務(wù)端重定向到單點(diǎn)登錄客戶(hù)端的/login鏈接,此時(shí)鏈接帶有code和state參數(shù)
- 7蛉幸、再次用到第三步的OAuth2ClientAuthenticationProcessingFilter攔截器通過(guò)上下文獲取token破讨,此時(shí)上下文中肯定沒(méi)有token,所以會(huì)通過(guò)OAuth2RestTemplate請(qǐng)求單點(diǎn)登錄服務(wù)端/oauth/token接口使用重定向獲得的code和state換取token
- 8奕纫、單點(diǎn)登錄客戶(hù)端獲取到token后提陶,首先會(huì)對(duì)token進(jìn)行解析,并使用配置的公鑰對(duì)token進(jìn)行校驗(yàn)(非對(duì)稱(chēng)加密)匹层,如果校驗(yàn)通過(guò)隙笆,則將token設(shè)置到上下文,下次訪(fǎng)問(wèn)請(qǐng)求時(shí)直接從上下文中獲取又固。
- 9、單點(diǎn)登錄客戶(hù)端處理完上下問(wèn)會(huì)話(huà)之后重定向到登錄前請(qǐng)求的受保護(hù)資源鏈接煤率。
三仰冠、使用【授權(quán)碼授權(quán)】和【刷新令牌授權(quán)】來(lái)實(shí)現(xiàn)單點(diǎn)登錄服務(wù)器
1、自定義單點(diǎn)登錄服務(wù)器頁(yè)面
??當(dāng)我們的gitegg-oauth作為授權(quán)服務(wù)器使用時(shí)蝶糯,我們希望定制自己的登錄頁(yè)等信息洋只,下面我們自定義登錄、主頁(yè)昼捍、錯(cuò)誤提示頁(yè)识虚、找回密碼頁(yè)。其他需要的頁(yè)面可以自己定義妒茬,比如授權(quán)確認(rèn)頁(yè)担锤,我們此處業(yè)務(wù)不需要用戶(hù)二次確認(rèn),所以這里沒(méi)有自定義此頁(yè)面乍钻。
- 在gitegg-oauth工程的pom.xml中添加Thymeleaf依賴(lài)肛循,作為Spring官方推薦的模板引擎铭腕,我們使用Thymeleaf來(lái)實(shí)現(xiàn)前端頁(yè)面的渲染展示。
<!--thymeleaf 模板引擎 渲染單點(diǎn)登錄服務(wù)器頁(yè)面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 在GitEggOAuthController中新增頁(yè)面跳轉(zhuǎn)路徑
/**
* 單點(diǎn)登錄-登錄頁(yè)
* @return
*/
@GetMapping("/login") public String login() {
return "login";
}
/**
* 單點(diǎn)登錄-首頁(yè):當(dāng)直接訪(fǎng)問(wèn)單點(diǎn)登錄系統(tǒng)成功后進(jìn)入的頁(yè)面多糠。從客戶(hù)端系統(tǒng)進(jìn)入的累舷,直接返回到客戶(hù)端頁(yè)面
* @return
*/
@GetMapping("/index") public String index() {
return "index";
}
/**
* 單點(diǎn)登錄-錯(cuò)誤頁(yè)
* @return
*/
@GetMapping("/error") public String error() {
return "error";
}
/**
* 單點(diǎn)登錄-找回密碼頁(yè)
* @return
*/
@GetMapping("/find/pwd") public String findPwd() {
return "findpwd";
}
-
在resources目錄下新建static(靜態(tài)資源)目錄和templates(頁(yè)面代碼)目錄,新增favicon.ico文件
單點(diǎn)登錄頁(yè)面目錄 - 自定義登錄頁(yè)login.html代碼
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="統(tǒng)一身份認(rèn)證平臺(tái)">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>統(tǒng)一身份認(rèn)證平臺(tái)</title>
<link rel="shortcut icon" th:href="@{/gitegg-oauth/favicon.ico}"/>
<link rel="bookmark" th:href="@{/gitegg-oauth/favicon.ico}"/>
<link type="text/css" rel="stylesheet" th:href="@{/gitegg-oauth/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css}">
<link type="text/css" rel="stylesheet" th:href="@{/gitegg-oauth/assets/bootstrap-validator-0.5.3/css/bootstrapValidator.css}">
<link type="text/css" rel="stylesheet" th:href="@{/gitegg-oauth/assets/css/font-awesome.min.css}">
<link type="text/css" rel="stylesheet" th:href="@{/gitegg-oauth/assets/css/login.css}">
<!--[if IE]>
<script type="text/javascript" th:src="@{/gitegg-oauth/assets/js/html5shiv.min.js}"></script>
<![endif]-->
</head>
<body>
<div class="htmleaf-container">
<div class="form-bg">
<div class="container">
<div class="row login_wrap">
<div class="login_left">
<span class="circle">
<!-- <span></span>
<span></span> -->
<img th:src="@{/gitegg-oauth/assets/images/logo.svg}" class="logo" alt="logo">
</span>
<span class="star">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</span>
<span class="fly_star">
<span></span>
<span></span>
</span>
<p id="title">
GitEgg Cloud 統(tǒng)一身份認(rèn)證平臺(tái)
</p>
</div>
<div class="login_right">
<div class="title cf">
<ul class="title-list fr cf ">
<li class="on">賬號(hào)密碼登錄</li>
<li>驗(yàn)證碼登錄</li>
<p></p>
</ul>
</div>
<div class="login-form-container account-login">
<form class="form-horizontal account-form" th:action="@{/gitegg-oauth/login}" method="post">
<input type="hidden" class="form-control" name="client_id" value="gitegg-admin">
<input id="user_type" type="hidden" class="form-control" name="type" value="user">
<input id="user_mobileType" type="hidden" class="form-control" name="mobile" value="0">
<div class="input-wrapper input-account-wrapper form-group">
<div class="input-icon-wrapper">
<i class="input-icon">
<svg t="1646301169630" class="icon" viewBox="64 64 896 896" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8796" width="1.2em" height="1.2em" fill="currentColor"><path d="M858.5 763.6c-18.9-44.8-46.1-85-80.6-119.5-34.5-34.5-74.7-61.6-119.5-80.6-0.4-0.2-0.8-0.3-1.2-0.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-0.4 0.2-0.8 0.3-1.2 0.5-44.8 18.9-85 46-119.5 80.6-34.5 34.5-61.6 74.7-80.6 119.5C146.9 807.5 137 854 136 901.8c-0.1 4.5 3.5 8.2 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c0.1 4.4 3.6 7.8 8 7.8h60c4.5 0 8.1-3.7 8-8.2-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" p-id="8797"></path></svg>
</i>
</div>
<input type="text" class="input" name="username" placeholder="請(qǐng)輸入您的賬號(hào)">
</div>
<div class="input-wrapper input-psw-wrapper form-group">
<div class="input-icon-wrapper">
<i class="input-icon">
<svg t="1646302713220" class="icon" viewBox="64 64 896 896" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8931" width="1.2em" height="1.2em" fill="currentColor"><path d="M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240z m460 600H232V536h560v304z" p-id="8932"></path><path d="M484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53c12.1-8.7 20-22.9 20-39 0-26.5-21.5-48-48-48s-48 21.5-48 48c0 16.1 7.9 30.3 20 39z" p-id="8933"></path></svg>
</i>
</div>
<input id="password" type="password" class="input" name="password" placeholder="請(qǐng)輸入您的密碼">
</div>
<div id="account-err" class="err-msg" style="width: 100%; text-align: center;"></div>
<button type="submit" class="login-btn" id="loginSubmit">立即登錄</button>
<div class="forget" id="forget">忘記密碼夹孔?</div>
</form>
</div>
<div class="login-form-container mobile-login" style="display: none;">
<form class="form-horizontal mobile-form" th:action="@{/gitegg-oauth/phoneLogin}" method="post">
<input id="tenantId" type="hidden" class="form-control" name="tenant_id" value="0">
<input id="type" type="hidden" class="form-control" name="type" value="phone">
<input id="mobileType" type="hidden" class="form-control" name="mobile" value="0">
<input id="smsId" type="hidden" class="form-control" name="smsId">
<div class="input-wrapper input-account-wrapper form-group input-phone-wrapper">
<div class="input-icon-wrapper">
<i class="input-icon">
<svg t="1646302822533" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9067" width="1.2em" height="1.2em" fill="currentColor"><path d="M744 62H280c-35.3 0-64 28.7-64 64v768c0 35.3 28.7 64 64 64h464c35.3 0 64-28.7 64-64V126c0-35.3-28.7-64-64-64z m-8 824H288V134h448v752z" p-id="9068"></path><path d="M512 784m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" p-id="9069"></path></svg>
</i>
</div>
<input id="phone" type="text" class="input" name="phone" maxlength="11" placeholder="請(qǐng)輸入手機(jī)號(hào)">
</div>
<div class="code-form form-group sms-code-wrapper">
<div class="input-wrapper input-sms-wrapper">
<div class="input-icon-wrapper">
<i class="input-icon">
<svg t="1646302879723" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9203" width="1.2em" height="1.2em" fill="currentColor"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 110.8V792H136V270.8l-27.6-21.5 39.3-50.5 42.8 33.3h643.1l42.8-33.3 39.3 50.5-27.7 21.5z" p-id="9204"></path><path d="M833.6 232L512 482 190.4 232l-42.8-33.3-39.3 50.5 27.6 21.5 341.6 265.6c20.2 15.7 48.5 15.7 68.7 0L888 270.8l27.6-21.5-39.3-50.5-42.7 33.2z" p-id="9205"></path></svg>
</i>
</div>
<input id="code" type="text" class="input-code" name="code" maxlength="6" placeholder="請(qǐng)輸入驗(yàn)證碼">
</div>
<div class="input-code-wrapper">
<a id="sendBtn" href="javascript:sendCode();">獲取驗(yàn)證碼</a>
</div>
</div>
<div id="mobile-err" class="err-msg" style="width: 100%; text-align: center;"></div>
<button type="submit" class="login-btn" id="loginSubmitByCode">立即登錄</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="related">
Copyrights ? 2021 GitEgg All Rights Reserved.
</div>
</div>
<script type="text/javascript" th:src="@{/gitegg-oauth/assets/js/jquery-2.1.4.min.js}"></script>
<script type="text/javascript" th:src="@{/gitegg-oauth/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js}"></script>
<script type="text/javascript" th:src="@{/gitegg-oauth/assets/bootstrap-validator-0.5.3/js/bootstrapValidator.js}"></script>
<script type="text/javascript" th:src="@{/gitegg-oauth/assets/js/md5.js}"></script>
<script type="text/javascript" th:src="@{/gitegg-oauth/assets/js/jquery.form.js}"></script>
<script type="text/javascript" th:src="@{/gitegg-oauth/assets/js/login.js}"></script>
</body>
</html>
- 自定義登錄login.js代碼
var countdown=60;
jQuery(function ($) {
countdown = 60;
$('.account-form').bootstrapValidator({
message: '輸入錯(cuò)誤',
feedbackIcons: {
valid: 'glyphicon glyphicon-ok',
invalid: 'glyphicon glyphicon-remove',
validating: 'glyphicon glyphicon-refresh'
},
fields: {
username: {
container: '.input-account-wrapper',
message: '輸入錯(cuò)誤',
validators: {
notEmpty: {
message: '用戶(hù)賬號(hào)不能為空'
},
stringLength: {
min: 2,
max: 32,
message: '賬號(hào)長(zhǎng)度范圍2-32個(gè)字符被盈。'
},
regexp: {
regexp: /^[a-zA-Z0-9_\.]+$/,
message: '用戶(hù)名只能由字母、數(shù)字搭伤、點(diǎn)和下劃線(xiàn)組成'
}
}
},
password: {
container: '.input-psw-wrapper',
validators: {
notEmpty: {
message: '密碼不能為空'
},
stringLength: {
min: 5,
max: 32,
message: '密碼長(zhǎng)度范圍6-32個(gè)字符只怎。'
}
}
}
}
});
$('.mobile-form').bootstrapValidator({
message: '輸入錯(cuò)誤',
feedbackIcons: {
valid: 'glyphicon glyphicon-ok',
invalid: 'glyphicon glyphicon-remove',
validating: 'glyphicon glyphicon-refresh'
},
fields: {
phone: {
message: '輸入錯(cuò)誤',
container: '.input-phone-wrapper',
validators: {
notEmpty: {
message: '手機(jī)號(hào)不能為空'
},
regexp: {
regexp: /^1\d{10}$/,
message: '手機(jī)號(hào)格式錯(cuò)誤'
}
}
},
code: {
container: '.input-sms-wrapper',
validators: {
notEmpty: {
message: '驗(yàn)證碼不能為空'
},
stringLength: {
min: 6,
max: 6,
message: '驗(yàn)證碼長(zhǎng)度為6位。'
}
}
}
}
});
var options={
beforeSerialize: beforeFormSerialize,
success: formSuccess,//提交成功后執(zhí)行的回掉函數(shù)
error: formError,//提交失敗后執(zhí)行的回掉函數(shù)
headers : {"TenantId" : 0},
clearForm: true,//提交成功后是否清空表單中的字段值
restForm: true,//提交成功后是否充值表單中的字段值闷畸,即恢復(fù)到頁(yè)面加載是的狀態(tài)
timeout: 6000//設(shè)置請(qǐng)求時(shí)間尝盼,超過(guò)時(shí)間后,自動(dòng)退出請(qǐng)求佑菩,單位(毫秒)
}
var mobileOptions={
success: mobileFormSuccess,//提交成功后執(zhí)行的回掉函數(shù)
error: mobileFormError,//提交失敗后執(zhí)行的回掉函數(shù)
headers : {"TenantId" : 0},
clearForm: true,//提交成功后是否清空表單中的字段值
restForm: true,//提交成功后是否充值表單中的字段值盾沫,即恢復(fù)到頁(yè)面加載是的狀態(tài)
timeout: 6000//設(shè)置請(qǐng)求時(shí)間,超過(guò)時(shí)間后殿漠,自動(dòng)退出請(qǐng)求赴精,單位(毫秒)
}
function beforeFormSerialize(){
$("#account-err").html("");
$("#username").val($.trim($("#username").val()));
$("#password").val($.md5($.trim($("#password").val())));
}
function formSuccess(response){
$(".account-form").data('bootstrapValidator').resetForm();
if (response.success)
{
window.location.href = response.targetUrl;
}
else
{
$("#account-err").html(response.message);
}
}
function formError(response){
$("#account-err").html(response);
}
function mobileFormSuccess(response){
$(".mobile-form").data('bootstrapValidator').resetForm();
if (response.success)
{
window.location.href = response.targetUrl;
}
else
{
$("#mobile-err").html(response.message);
}
}
function mobileFormError(response){
$("#mobile-err").html(response);
}
$(".account-form").ajaxForm(options);
$(".mobile-form").ajaxForm(mobileOptions);
$(".nav-left a").click(function(e){
$(".account-login").show();
$(".mobile-login").hide();
});
$(".nav-right a").click(function(e){
$(".account-login").hide();
$(".mobile-login").show();
});
$("#forget").click(function(e){
window.location.href = "/find/pwd";
});
$('.title-list li').click(function(){
var liindex = $('.title-list li').index(this);
$(this).addClass('on').siblings().removeClass('on');
$('.login_right div.login-form-container').eq(liindex).fadeIn(150).siblings('div.login-form-container').hide();
var liWidth = $('.title-list li').width();
if (liindex == 0)
{
$('.login_right .title-list p').css("transform","translate3d(0px, 0px, 0px)");
}
else {
$('.login_right .title-list p').css("transform","translate3d("+liWidth+"px, 0px, 0px)");
}
});
});
function sendCode(){
$(".mobile-form").data('bootstrapValidator').validateField('phone');
if(!$(".mobile-form").data('bootstrapValidator').isValidField("phone"))
{
return;
}
if(countdown != 60)
{
return;
}
sendmsg();
var phone = $.trim($("#phone").val());
var tenantId = $("#tenantId").val();
$.ajax({
//請(qǐng)求方式
type : "POST",
//請(qǐng)求的媒體類(lèi)型
contentType: "application/x-www-form-urlencoded;charset=UTF-8",
dataType: 'json',
//請(qǐng)求地址
url : "/code/sms/login",
//數(shù)據(jù),json字符串
data : {
tenantId: tenantId,
phoneNumber: phone,
code: "aliValidateLogin"
},
//請(qǐng)求成功
success : function(result) {
$("#smsId").val(result.data);
},
//請(qǐng)求失敗绞幌,包含具體的錯(cuò)誤信息
error : function(e){
console.log(e);
}
});
};
function sendmsg(){
if(countdown==0){
$("#sendBtn").css("color","#181818");
$("#sendBtn").html("獲取驗(yàn)證碼");
countdown=60;
return false;
}
else{
$("#sendBtn").css("color","#74777b");
$("#sendBtn").html("重新發(fā)送("+countdown+")");
countdown--;
}
setTimeout(function(){
sendmsg();
},1000);
}
2蕾哟、授權(quán)服務(wù)器配置
- 修改web安全配置WebSecurityConfig,將靜態(tài)文件添加到不需要授權(quán)就能訪(fǎng)問(wèn)
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
}
- 修改Nacos配置莲蜘,將新增頁(yè)面訪(fǎng)問(wèn)路徑添加到訪(fǎng)問(wèn)白名單谭确,使資源服務(wù)器配置ResourceServerConfig中的配置不進(jìn)行鑒權(quán)就能夠訪(fǎng)問(wèn),同時(shí)增加tokenUrls配置票渠,此配置在網(wǎng)關(guān)不進(jìn)行鑒權(quán)逐哈,但是需要OAuth2進(jìn)行Basic鑒權(quán),授權(quán)碼模式必須要用到此鑒權(quán)问顷。
# 以下配置為新增
whiteUrls:
- "/gitegg-oauth/oauth/login"
- "/gitegg-oauth/oauth/find/pwd"
- "/gitegg-oauth/oauth/error"
authUrls:
- "/gitegg-oauth/oauth/index"
whiteUrls:
- "/*/v2/api-docs"
- "/gitegg-oauth/oauth/public_key"
- "/gitegg-oauth/oauth/token_key"
- "/gitegg-oauth/find/pwd"
- "/gitegg-oauth/code/sms/login"
- "/gitegg-oauth/change/password"
- "/gitegg-oauth/error"
- "/gitegg-oauth/oauth/sms/captcha/send"
# 新增OAuth2認(rèn)證接口昂秃,此處網(wǎng)關(guān)放行,由認(rèn)證中心進(jìn)行認(rèn)證
tokenUrls:
- "/gitegg-oauth/oauth/token"
- 因GitEgg框架使用用戶(hù)名+密碼再加密存儲(chǔ)的密碼杜窄,所以這里需要自定義登錄過(guò)濾器來(lái)做相應(yīng)處理肠骆,也可以用同樣的方式新增手機(jī)驗(yàn)證碼登錄、掃碼登錄等功能塞耕。
package com.gitegg.oauth.filter;
import cn.hutool.core.bean.BeanUtil;
import com.gitegg.oauth.token.PhoneAuthenticationToken;
import com.gitegg.platform.base.constant.AuthConstant;
import com.gitegg.platform.base.domain.GitEggUser;
import com.gitegg.platform.base.result.Result;
import com.gitegg.service.system.client.feign.IUserFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定義登陸
* @author GitEgg
*/
public class GitEggLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_RESTFUL_TYPE_PHONE = "phone";
public static final String SPRING_SECURITY_RESTFUL_TYPE_QR = "qr";
public static final String SPRING_SECURITY_RESTFUL_TYPE_DEFAULT = "user";
// 登陸類(lèi)型:user:用戶(hù)密碼登陸蚀腿;phone:手機(jī)驗(yàn)證碼登陸;qr:二維碼掃碼登陸
private static final String SPRING_SECURITY_RESTFUL_TYPE_KEY = "type";
// 登陸終端:1:移動(dòng)端登陸扫外,包括微信公眾號(hào)唯咬、小程序等纱注;0:PC后臺(tái)登陸
private static final String SPRING_SECURITY_RESTFUL_MOBILE_KEY = "mobile";
private static final String SPRING_SECURITY_RESTFUL_USERNAME_KEY = "username";
private static final String SPRING_SECURITY_RESTFUL_PASSWORD_KEY = "password";
private static final String SPRING_SECURITY_RESTFUL_PHONE_KEY = "phone";
private static final String SPRING_SECURITY_RESTFUL_VERIFY_CODE_KEY = "code";
private static final String SPRING_SECURITY_RESTFUL_QR_CODE_KEY = "qrCode";
@Autowired
private IUserFeign userFeign;
private boolean postOnly = true;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String type = obtainParameter(request, SPRING_SECURITY_RESTFUL_TYPE_KEY);
String mobile = obtainParameter(request, SPRING_SECURITY_RESTFUL_MOBILE_KEY);
AbstractAuthenticationToken authRequest;
String principal;
String credentials;
// 手機(jī)驗(yàn)證碼登陸
if(SPRING_SECURITY_RESTFUL_TYPE_PHONE.equals(type)){
principal = obtainParameter(request, SPRING_SECURITY_RESTFUL_PHONE_KEY);
credentials = obtainParameter(request, SPRING_SECURITY_RESTFUL_VERIFY_CODE_KEY);
principal = principal.trim();
authRequest = new PhoneAuthenticationToken(principal, credentials);
}
// 賬號(hào)密碼登陸
else {
principal = obtainParameter(request, SPRING_SECURITY_RESTFUL_USERNAME_KEY);
credentials = obtainParameter(request, SPRING_SECURITY_RESTFUL_PASSWORD_KEY);
Result<Object> result = userFeign.queryUserByAccount(principal);
if (null != result && result.isSuccess()) {
GitEggUser gitEggUser = new GitEggUser();
BeanUtil.copyProperties(result.getData(), gitEggUser, false);
if (!StringUtils.isEmpty(gitEggUser.getAccount())) {
principal = gitEggUser.getAccount();
credentials = AuthConstant.BCRYPT + gitEggUser.getAccount() + credentials;
}
}
authRequest = new UsernamePasswordAuthenticationToken(principal, credentials);
}
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private void setDetails(HttpServletRequest request,
AbstractAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
private String obtainParameter(HttpServletRequest request, String parameter) {
String result = request.getParameter(parameter);
return result == null ? "" : result;
}
}
四、實(shí)現(xiàn)單點(diǎn)登錄客戶(hù)端
?? spring-security-oauth2提供OAuth2授權(quán)服務(wù)器的同時(shí)也提供了單點(diǎn)登錄客戶(hù)端的實(shí)現(xiàn)胆胰,通用通過(guò)幾行注解即可實(shí)現(xiàn)單點(diǎn)登錄功能狞贱。
1、新建單點(diǎn)登錄客戶(hù)端工程蜀涨,引入oauth2客戶(hù)端相關(guān)jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
2瞎嬉、新建WebSecurityConfig類(lèi),添加@EnableOAuth2Sso注解
@EnableOAuth2Sso
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
3厚柳、配置單點(diǎn)登錄服務(wù)端相關(guān)信息
server:
port: 8080
servlet:
context-path: /ssoclient1
security:
oauth2:
client:
# 配置在授權(quán)服務(wù)器配置的客戶(hù)端id和secret
client-id: ssoclient
client-secret: 123456
# 獲取token的url
access-token-uri: http://127.0.0.1/gitegg-oauth/oauth/token
# 授權(quán)服務(wù)器的授權(quán)地址
user-authorization-uri: http://127.0.0.1/gitegg-oauth/oauth/authorize
resource:
jwt:
# 獲取公鑰的地址氧枣,驗(yàn)證token需使用,系統(tǒng)啟動(dòng)時(shí)會(huì)初始化别垮,不會(huì)每次驗(yàn)證都請(qǐng)求
key-uri: http://127.0.0.1/gitegg-oauth/oauth/token_key
備注:
1便监、GitEgg框架中自定義了token返回格式,SpringSecurity獲取token的/oauth/token默認(rèn)返回的是ResponseEntity<OAuth2AccessToken>碳想,自有系統(tǒng)登錄和單點(diǎn)登錄時(shí)需要做轉(zhuǎn)換處理烧董。
2、Gateway網(wǎng)關(guān)鑒權(quán)需要的公鑰地址是gitegg-oauth/oauth/public_key胧奔,單點(diǎn)登錄客戶(hù)端需要公鑰地址
/oauth/token_key逊移,兩者返回的格式不一樣,需注意區(qū)分龙填。
3胳泉、請(qǐng)求/oauth/tonen和/oauth/token_key時(shí),默認(rèn)都需要使用Basic認(rèn)證岩遗,也就是請(qǐng)求時(shí)需添加client_id和client_security參數(shù)扇商。
GitEgg-Cloud是一款基于SpringCloud整合搭建的企業(yè)級(jí)微服務(wù)應(yīng)用開(kāi)發(fā)框架,開(kāi)源項(xiàng)目地址:
Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg
歡迎感興趣的小伙伴Star支持一下宿礁。