Spring Boot 與 OAuth2 原

本指南將向你展示如何使用OAuth2和Spring Boot構建的具有“社交登錄”功能的應用程序去做完成各種事情。它從一個簡單單點登錄開始,運行一個自我託管的OAuth2授權服務器,此服務器帶有一個身份驗證提供者(Facebook或Github)。這些示例它們都在前端使用了普通的jQuery,但是轉換到不同的JavaScript框架或使用服務器端渲染的改動將非常小。

給大家推薦一個程序員學習羣:854818273。羣裏有分享的視頻,還有思維導圖
羣公告有視頻,都是乾貨的,你可以下載來看。主要分享分佈式架構、高可擴展、高性能、高併發、性能優化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分佈式項目實戰學習架構師視頻。

在每個添加新功能的例子中都有以下特點:

  • 簡單:一個非常基本的靜態應用程序只有一個主頁,並通過Spring Boot的 EnableOAuth2Sso無條件登錄(如果你訪問主頁,你將自動重定向到Facebook)。
  • 點擊:添加用戶必須單擊才能登錄的顯式鏈接。
  • 登出:爲通過身份驗證的用戶添加了登出鏈接。
  • 手動配置:通過取消選中並手動配置來展示 @EnableOAuth2Sso是如何工作的。
  • GitHub:在Github中添加了第二個登錄提供方,以便用戶可以在主頁上選擇使用哪一個。
  • 認證服務:將應用程序變成一個完全成熟的OAuth2授權服務器,能夠發出自己的令牌,但仍然使用外部OAuth2提供程序進行身份驗證。
  • 自定義錯誤:爲未經身份驗證的用戶添加錯誤消息,並基於Github API添加自定義身份驗證。

從一個應用程序遷移到功能階梯的下一個應用程序所需要的更改可以在源代碼中跟蹤(源代碼在Github中)。存儲庫中的前6個更改正在轉變一個應用程序,這樣你就可以很容易地看到差異。 在早期提交的應用程序和在指南中看到的最終程序,你看到的任何進一步的差異都是修飾性的。

它們中的每一個都可以被導入到一個IDE中,並且有一個可以在那裏運行的主類 SocialApplication來啓動應用程序。 他們都在http//localhost8080上提供了一個主頁(如果你想登錄並查看內容,所有這些都需要你至少有一個Facebook帳戶)。你也可以使用 mvn spring-boot:run或通過構建jar文件並使用 mvnpackagejava-jar target/*.jar(根據Spring Boot文檔和其他可用文檔)運行命令行中的所有應用程序。 如果你使用了maven-wrapper,則不需要安裝Maven。比如:

這些應用程序都在 localhost8080上運行,因爲他們使用在Facebook和Github上註冊的OAuth2客戶端來訪問該地址。 要在不同的主機或端口上運行它們,你需要註冊自己的應用程序,並將憑據放在配置文件中。 如果你使用默認值,則不會在本地主機之外泄漏你的Facebook或Github憑據,但要小心你在互聯網上公開的內容,並且不要將你自己的應用程序註冊置於公共源代碼管理中。

用FaceBook做單點登錄

在本節中,我們創建一個使用Facebook進行身份驗證的應用程序。如果我們利用Spring Boot中的自動配置功能,這一過程將相當容易。

創建一個新的工程

首先,我們需要創建一個Spring Boot應用程序,可以通過多種方式來完成。 最簡單的是去http://start.spring.io並生成一個空的項目(選擇“Web”依賴項作爲起點)。相當於於在命令行上執行此操作:

然後,你可以將該項目導入到你最喜歡的IDE中(默認情況下,這是一個普通的Maven Java項目),或者只是在命令行上配合“mvn”使用這些文件。

添加主頁

這些對於演示OAuth2登錄功能來說都不是必須的,但是我們希望最終能有一個好看的用戶界面,所以我們不妨從構造主頁中一些基本的東西開始。

如果你啓動應用程序並加載主頁,則會注意到樣式尚未加載。所以我們也需要添一些依賴:

我們添加了Twitter bootstrap和jQuery(這就是我們現在所需要的) 另一個依賴是webjars“定位器”,它由webjars站點作爲提供庫,Spring可以使用這個定位器在webjars中定位靜態資源,而不需要知道確切的版本(因此versionless /webjars/**鏈接 在 index.html中)。 只要不關閉MVC自動配置,webjar定位器在Spring Boot應用程序中默認激活。

在做了以上改變,我們應用程序的主頁應該更加美觀了。

給大家推薦一個程序員學習羣:854818273。羣裏有分享的視頻,還有思維導圖
羣公告有視頻,都是乾貨的,你可以下載來看。主要分享分佈式架構、高可擴展、高性能、高併發、性能優化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分佈式項目實戰學習架構師視頻。

保護應用程序

爲了使應用程序安全,我們只需要添加Spring Security作爲依賴。如果我們這樣做,默認情況下是使用HTTP Basic來保護它,所以既然我們想做一個“社交”登錄(委託給Facebook),我們也添加了Spring Security OAuth2依賴項:

爲了能夠鏈接到FaceBook我們需要在主類添加註解 @EnableOAuth2Sso

還有一些配置(這裏把 application.properties轉換爲YAML形式,這樣可讀性強)

該配置是指向Facebook開發者站點註冊的客戶端應用程序,其中你必須爲該應用程序提供註冊的重定向(主頁)。這個註冊到“localhost:8080”,所以只有運行在該地址上的應用才能生效。

做了以上改變,你可以再次運行應用程序,並訪問 http//localhost:8080的主頁。接下來你應該重定向到Facebook登錄而不是主頁。如果你登陸了並同意你要求的任何授權,你將被重定向回到本地應用程序並且可以看到主頁。如果你保持登錄到Facebook,即使你使用新的瀏覽器不使用Cookie和緩存數據打開它也不必使用本地應用重新進行身份驗證。(這就是單點登錄)

如果你正在做示例應用程序的這一部分,請務必清除你的Cookie和HTTP Basic憑據的瀏覽器緩存。在Chrome中,最好在訪問每個服務器主業的時候打開一個新的隱身窗口。

對這個示例進行訪問是安全的,因爲只有本地運行的應用程序可以使用令牌並且它要求的範圍是有限的。當你登錄到這樣的應用程序時,要注意你所批准的內容:他們可能會要求你做更多的事情(比如他們可能會要求你更改你的個人資料,這可能不符合你的利益)。

剛剛發生了什麼?

你剛剛用OAuth2的編寫的應用程序是一個客戶端應用程序,它使用授權代碼授權從Facebook(授權服務器)獲取訪問令牌。 然後,它使用訪問令牌向Facebook詢問一些個人信息(僅限於你允許的內容),包括你的登錄ID和你的姓名。 在這個階段,facebook充當了一個資源服務器,對你發送的令牌進行解碼,並檢查它給了應用程序訪問用戶詳細信息的權限。如果該過程成功,則應用程序將用戶詳細信息插入到Spring Security上下文中,以便進行身份驗證。

如果你用瀏覽器工具(Chrome上的F12),追蹤查看所有路由的網絡流量操作,你將看到Facebook的重定向,最後用新的 Set-Cookie請求頭返回主頁。 這個cookie(默認情況下是 JSESSIONID)是Spring(或任何基於servlet的)應用程序的認證細節的一個標記。

所以我們有一個安全的應用程序,用戶查看任何內容必須通過外部供應服務器(Facebook)認證。 我們不希望將其用於網上銀行網站,而是用於基本的身份識別,並將網站內的不同用戶之間的內容隔離開來,這是一個很好的開端,這就解釋了爲什麼這種認證現在非常流行。在下一節中,我們將爲應用程序添加一些基本功能,並且使用戶更清楚的看到最初重定向到Facebook時發生的事情。

添加一個歡迎頁面

在本節中,我們將修改我們剛剛構建的應用程序,通過添加一個顯式的鏈接登錄Facebook。新的鏈接不會立即被重定向,而是可以在主頁上看到,用戶可以選擇登錄或不經過身份驗證。只有當用戶點擊了鏈接,他纔會顯示內容。

主頁中受保護的內容

我們可以使用服務器端渲染頁面(例如,使用Freemarker或Tymeleaf)通過用戶是否通過驗證來確定其是否可訪問受保護的內容,或者我們可以使用一些JavaScript請求瀏覽器。 要做到這一點,我們要使用AngularJS,但是如果你更喜歡使用不同的框架,你可把客戶端的代碼轉換成其他框架。

要開始使用動態內容,我們需要在部分內容中標記HTML,以顯示它:

這個HTML滿足了一些客戶端代碼的需求,這些客戶端代碼可以操作 authenticatedunauthenticateduser元素。下面是這些功能的簡單實現(將它們放在 <body>的末尾):

 

服務器端的改動

爲此,我們需要在服務器端進行一些更改。“home”控制器需要一個描述當前身份驗證用戶的“/user”端點。這很容易做到,例如在我們的主類:

注意 @restcontroller@requestmappingjava.security的使用。我們將其注入到了處理方法中。

在/user端點中返回一個完整的用戶信息主體不是一個好主意(它可能包含你不願向瀏覽器客戶機顯示的信息)。我們這樣做只是爲了讓應用盡快正常運行。在後面的指南中,我們將轉換端點來隱藏瀏覽器不需要的信息。

這個應用程序現在可以像以前一樣正常運行了,但是用戶還不能點擊我們剛剛新添加的鏈接。爲了使鏈接可見,我們還需要通過添加 WebSecurityConfigurer來關閉主頁上一些不必要的安全保護:

完成以上步驟,我們的應用就完整了,如果你運行它並訪問主頁,該看到一個美觀的的HTML鏈接“登錄到Facebook”。鏈接不會直接傳送到Facebook,而是定位到處理身份驗證的本地路徑(並將重定向發送到Facebook)。一旦你通過身份驗證,你會被重定向回到本地的應用程序,本地應用將會顯示你的名字(假設你已經在Facebook上設置了允許訪問這些數據的權限)。

添加登出按鈕

在本節中,我們修改了應用通過添加一個按鈕,允許用戶退出程序。這似乎是一個簡單的功能,但實際上需要仔細考慮它的實現,所以它值得花一些時間討論如何去做。大多數改動都是由於我們正在將應用程序從只讀資源轉換爲讀寫操作(註銷需要狀態更改),因此在任何實際應用程序中都需要相同的更改,而不僅僅是靜態內容。

客戶端改動

在客戶端,我們只需要提供一個註銷按鈕和一些JavaScript,以調用服務器請求取消身份驗證。首先,在UI的“驗證”部分中,我們添加按鈕:

接下來我們在JavaScript中提供 logout()函數,以供調用:

logout()函數執行一個POST /logout,然後屏蔽受保護內容。現在我們可以切換到服務器端來實現這個端點。

添加一個Logout端點

Spring Security已經構建了一個支持 /logout的端點,它將爲我們做正確的事情(清除會話並使Cookie無效)。 要配置端點,我們只需在 WebSecurityConfigurer中擴展現有的 configure()方法:

340/5000 /logout端點需要我們用POST方法請求,並保護用戶免受跨站點請求僞造(CSRF,發音爲“sea surf”)攻擊,它要求在請求中包含一個令牌。該令牌的值與當前提供保護的會話相關聯,因此我們需要一種方法將這些數據放入到我們的JavaScript應用程序中。

許多JavaScript框架都支持CSRF(例如,在Angular中,他們稱之爲XSRF),但是它通常以與Spring Security的開箱即用方式稍有不同的方式實現。例如,在Angular中,前端希望服務器發送一個叫做“XSRF-TOKEN”的cookie,如果它看到的話,它會把這個值作爲一個名爲“X-XSRF-TOKEN”的請求頭發回去。我們可以用簡單的jQuery客戶端來實現相同的行爲,然後服務器端做很少的改動去與其他前端實現一起工作。爲了讓Spring Security適應這些,我們需要添加一個創建cookie的過濾器,同時我們還需要告訴現有的CRSF過濾器相應的請求頭名。 在 WebSecurityConfigurer中:

在客戶端添加CSRF令牌

由於我們在這個示例中沒有使用封裝更好的框架,所以我們需要顯式地添加CSRF令牌,這是我們從後端提供的cookie。爲了使代碼更簡單,我們還需要依賴一個額外的庫:

在HTML頁面中引入:

然後我們可以使用xhr中比較便利的 Cookies方法:

準備工作完成!

做了以上改動,我們可以準備運行應用程序,並嘗試新的註銷按鈕。啓動應用程序並在新的瀏覽器窗口中加載主頁。點擊“登錄”鏈接將你帶到Facebook(如果你已經登錄,你可能不會注意到重定向)。點擊“註銷”按鈕取消當前會話,並將應用程序返回到未認證狀態。如果你足夠細心,你應該能夠在瀏覽器與本地服務器交換的請求中看到新的cookie和請求頭。

請注意,現在logout端點與瀏覽器一起工作,那麼所有其他HTTP請求(POST、PUT、DELETE等)也會正常工作。因此,對於一些具有更實際的特性的應用程序來說,這應該是一個很好的平臺。

手動配置OAuth2客戶端

在本節中,我們通過選擇 @EnableOAuth2Sso註釋中的“magic”來修改我們已經構建的應用程序,手動配置其中的所有內容以使其顯式化。

客戶端與認證

@EnableOAuth2Sso有兩個特性:OAuth2客戶端和身份驗證。客戶端是可重用的,因此你還可以使用它與你的授權服務器(在本例中是Facebook)提供的OAuth2資源進行交互(在本例中爲Graph API)。認證件將你的應用與Spring安全的其他部分結合在一起,所以一旦你的應用程序與Facebook的同步,它就會和其他安全的Spring應用程序一樣。

客戶端是由Spring Security OAuth2提供的,並由一個不同的註釋 @EnableOAuth2Client開啓。因此,這個改動的第一步是刪除 @EnableOAuth2Sso並將其替換爲較低級別的註釋:

通過這樣的更改,我們創建了一些有用的東西。首先,我們可以注入一個 OAuth2ClientContext,並使用它來構建一個身份驗證過濾器,以添加到我們的安全配置中:

這個過濾器是在我們使用 OAuth2ClientContext的新方法中創建的:

SocialApplication
private Filter ssoFilter() {
  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
  tokenServices.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(tokenServices);
  return facebookFilter;
}

過濾器還需要知道客戶端註冊了Facebook:

SocialApplication
  @Bean
  @ConfigurationProperties("facebook.client")
  public AuthorizationCodeResourceDetails facebook() {
    return new AuthorizationCodeResourceDetails();
  }

要完成身份驗證,它需要知道用戶信息端點在Facebook中的位置:

SocialApplication
  @Bean
  @ConfigurationProperties("facebook.resource")
  public ResourceServerProperties facebookResource() {
    return new ResourceServerProperties();
  }

注意,在這些“靜態”數據對象( facebook()facebookResource())中,我們使用了一個 @Bean修飾的 @ConfigurationProperties。這意味着我們可以創建 application.yml的新格式,在這裏,配置的前綴是 facebook,而不是 security.oauth2:

application.yml
facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me

最後,我們需要將登錄的路徑改爲在上面的過濾器聲明中特定於facebook的,因此我們要在HTML中做出相同的更改:

index.html
<h1>Login</h1>
<div class="container unauthenticated">
    <div>
    With Facebook: <a href="/login/facebook">click here</a>
    </div>
</div>

處理重定向

我們需要做的最後一個改變是顯式地支持從我們的應用程序到Facebook的重定向。這是在Spring OAuth2中使用servlet Filter處理的,並且過濾器已經在應用程序上下文中可用,因爲我們使用了 @EnableOAuth2Client。所需要的是將過濾器連接起來,以便在Spring Boot應用程序中以正確的順序調用它。要做到這一點,我們需要一個 FilterRegistrationBean:

SocialApplication.java
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
    OAuth2ClientContextFilter filter) {
  FilterRegistrationBean registration = new FilterRegistrationBean();
  registration.setFilter(filter);
  registration.setOrder(-100);
  return registration;
}

我們對已可用的過濾器進行自動連接,並在主Spring Security過濾器之前以較爲靠後的順序對其進行註冊。通過這種方式,我們可以使用它來處理在身份驗證請求中所表示的重定向。

做完以上改動,應用就可以很好的運行了,在運行時就相當於我們在上一節中構建的註銷示例。通過這樣將配置分解明確告訴我們Spring Boot所做的事情並沒有什麼神奇之處(它只是配置鍋爐版),而且它還提供了自動注入的功能讓我們的應用程序繼承,添加我們自己的代碼和業務需求。

用GitHub登陸

在本節中,我們將修改已經構建好的應用程序,添加一個鏈接,這樣除了原始的Facebook鏈接外,用戶還可以選擇使用Github進行身份驗證。

增加Github鏈接

在客戶端,更改很簡單,我們只需添加另一個鏈接:

index.html
<div class="container unauthenticated">
  <div>
    With Facebook: <a href="/login/facebook">click here</a>
  </div>
  <div>
    With Github: <a href="/login/github">click here</a>
  </div>
</div>

原則上,一旦我們開始添加身份驗證提供者,我們可能需要對從“/user”端點返回的數據更加小心。事實證明,Github和Facebook在他們的用戶信息中都有一個“name”字段,所以我們的端點沒有任何變化。

增加Github認證過濾器

服務器上的主要變化是添加一個附加的安全過濾器來處理來自我們的新鏈接的“/login/github”請求。我們已經在我們的 ssoFilter()方法中創建了一個用於Facebook的自定義驗證過濾器,所以我們需要做的就是用一個可以處理多個身份驗證路徑的函數來替換它:

SocialApplication.java
private Filter ssoFilter() {

  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();

  OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/facebook");
  OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
  facebookFilter.setRestTemplate(facebookTemplate);
  UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId());
  tokenServices.setRestTemplate(facebookTemplate);
  facebookFilter.setTokenServices(tokenServices);
  filters.add(facebookFilter);

  OAuth2ClientAuthenticationProcessingFilter githubFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/github");
  OAuth2RestTemplate githubTemplate = new OAuth2RestTemplate(github(), oauth2ClientContext);
  githubFilter.setRestTemplate(githubTemplate);
  tokenServices = new UserInfoTokenServices(githubResource().getUserInfoUri(), github().getClientId());
  tokenServices.setRestTemplate(githubTemplate);
  githubFilter.setTokenServices(tokenServices);
  filters.add(githubFilter);

  filter.setFilters(filters);
  return filter;

}

我們的舊 ssoFilter()的代碼已經被複制,一次是用於Facebook,一次是Github,兩個過濾器合併成一個複合過濾器。

請注意,相對於 github()githubResource()方法,我們已經添加了類似的方法 facebook()facebookResource():

SocialApplication.java
@Bean
@ConfigurationProperties("github.client")
public AuthorizationCodeResourceDetails github() {
    return new AuthorizationCodeResourceDetails();
}

@Bean
@ConfigurationProperties("github.resource")
public ResourceServerProperties githubResource() {
    return new ResourceServerProperties();
}

和相應的配置:

application.yml
github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

這裏的客戶端詳細信息是用Github註冊的,地址也指向 localhost:8080(與Facebook相同)。

現在,這個應用可以運行了,而且用戶可以選擇用Facebook登陸,或者Github登陸

如何添加本地用戶數據庫

即使身份驗證被委託給外部提供者,許多應用程序也需要在本地保存其用戶的數據。在這裏我們沒有展示代碼,但是很容易通過兩個步驟完成。

1.爲數據庫選擇後端,併爲自定義 User對象設置一些存儲庫(例如,使用Spring Data),該對象符合你的需求,並且可以通過外部驗證服務器完成全部或部分身份驗證。

2.通過檢查 /User端點中的數據庫,爲登錄的每個唯一用戶配置 User對象。如果已存在具有當前主體 Principal的用戶,則可以更新該用戶,否則將創建該用戶。

提示:在 User對象中添加一個字段以鏈接到外部提供程序中的唯一標識符(不是用戶名,而是外部提供程序中帳戶的唯一標誌)。

託管授權服務器

在本節中,我們將修改我們構建的Github應用程序,使其成爲一個成熟的oauth2授權服務器,仍然使用Facebook和Github進行身份驗證,但能夠創建自己的訪問令牌。然後,可以使用這些令牌來保護後端資源,或者對我們碰巧需要以同樣方式保護的其他應用程序執行SSO。

整理身份驗證配置

在開始使用授權服務器功能之前,我們只需整理兩個外部提供程序的配置代碼。 ssoFilter()方法中有一些重複的代碼,因此我們將它們提取到共同方法中:

SocialApplication.java
private Filter ssoFilter() {
  CompositeFilter filter = new CompositeFilter();
  List<Filter> filters = new ArrayList<>();
  filters.add(ssoFilter(facebook(), "/login/facebook"));
  filters.add(ssoFilter(github(), "/login/github"));
  filter.setFilters(filters);
  return filter;
}

新的方法具有舊方法的所有重複代碼:

SocialApplication.java
private Filter ssoFilter(ClientResources client, String path) {
  OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
  OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
  filter.setRestTemplate(template);
  UserInfoTokenServices tokenServices = new UserInfoTokenServices(
      client.getResource().getUserInfoUri(), client.getClient().getClientId());
  tokenServices.setRestTemplate(template);
  filter.setTokenServices(tokenServices);
  return filter;
}

並且它使用了一個新的包裝對象 ClientResources,它整合了 OAuth2ProtectedResourceDetailsResourceServerProperties,在應用程序的最後一個版本中,它們被聲明爲單獨的 @Bean:

SocialApplication.java
class ClientResources {

  @NestedConfigurationProperty
  private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

  @NestedConfigurationProperty
  private ResourceServerProperties resource = new ResourceServerProperties();

  public AuthorizationCodeResourceDetails getClient() {
    return client;
  }

  public ResourceServerProperties getResource() {
    return resource;
  }
}

包裝器使用 @NestedConfigurationProperty通知註釋處理器對該類型標記爲元數據,它不表示單個值,而是完整的嵌套類型。

有了這個包裝器,我們可以像以前一樣使用相同的YAML配置,但一個方法只能給一個提供程序使用:

SocialApplication.java

@Bean
@ConfigurationProperties("github")
public ClientResources github() {
  return new ClientResources();
}

@Bean
@ConfigurationProperties("facebook")
public ClientResources facebook() {
  return new ClientResources();
}

啓用授權服務器

如果我們想把我們的應用程序變成oauth2授權服務器,不需要做得很麻煩,從一些基本功能(一個客戶端和創建訪問令牌的能力)開始。授權服務器不過是一堆端點,它們在Spring oaut 2中實現爲SpringMVC handler。我們已經有了一個安全的應用程序,因此只需添加 @EnableuthorizationServer註解即可:

SocialApplication.java

@SpringBootApplication
@RestController
@EnableOAuth2Client
@EnableAuthorizationServer
public class SocialApplication extends WebSecurityConfigurerAdapter {

   ...

}

添加新註解後,Spring Boot將安裝所有必要的端點併爲其設置安全性,前提是我們提供了我們想要支持的OAuth2客戶端的詳細信息:

application.yml
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      scope: read,write
      auto-approve-scopes: '.*'

此客戶端相當於 facebook.client*github.client*所提供的外部身份驗證。通過外部提供服務器,我們必須註冊並獲取客戶端ID和在應用程序中使用的密碼。在這種情況下,我們提供了相同的功能,因此我們需要(至少一個)客戶端才能工作。

我們已經將 auto-approve-scopes設置爲匹配所有作用域的正則表達式。這並不一定要留在線上系統中,但它可以讓我們快速工作,而無需重新放置Spring OAuth2在用戶需要訪問令牌時會爲他們彈出的白色標籤審批頁面。要向令牌授予中添加顯式批准步驟,我們需要提供一個UI來替換whitelabel( /oauth/confirm_access)。

要完成授權服務器,我們只需要爲其UI提供安全配置。事實上,在這個應用程序中沒有多少用戶界面,但是我們仍然需要保護 /oauth/authorize端點,並確保帶有“登錄”按鈕的主頁可見。這就是爲什麼我們需要這個方法:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.antMatcher("/**")                                       (1)
    .authorizeRequests()
      .antMatchers("/", "/login**", "/webjars/**").permitAll() (2)
      .anyRequest().authenticated()                            (3)
    .and().exceptionHandling()
      .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")) (4)
    ...
}

1 默認情況下,所有請求都受到保護2 明確排除主頁和登錄端點3 所有其他端點都需要經過身份驗證的用戶4 未經身份驗證的用戶將重新定向到主頁

如何獲取訪問令牌

現在可以從我們的新授權服務器獲得訪問令牌。到目前爲止,獲取令牌的最簡單方法是獲取一個作爲“acme”客戶端的令牌。如果你運行應用程序並對其curl,你可以看到這一點:

$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=client_credentials
{"access_token":"370592fd-b9f8-452d-816a-4fd5c6b4b8a6","token_type":"bearer","expires_in":43199,"scope":"read write"}

客戶端憑據令牌在某些情況下很有用(如測試令牌端點是否正常工作),但爲了利用服務器的所有功能,我們希望能夠爲用戶創建令牌。要代表應用程序的用戶獲取令牌,我們需要能夠對用戶進行身份驗證。如果在應用程序啓動時仔細查看日誌,你可能會看到爲默認Spring Boot用戶記錄了隨機密碼(根據SpringBoot用戶指南)。你可以使用此密碼代表id爲“user”的用戶獲取令牌:

$ curl acme:acmesecret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=...
{"access_token":"aa49e025-c4fe-4892-86af-15af2e6b72a2","token_type":"bearer","refresh_token":"97a9f978-7aad-4af7-9329-78ff2ce9962d","expires_in":43199,"scope":"read write"}

其中“…”應替換爲實際密碼。這稱爲“密碼”授權,你可以在其中更改用戶名和密碼獲取訪問令牌。

密碼授權對於測試也很有用,但當你有本地用戶數據庫來存儲和驗證憑據時,它可以適用於本機或移動應用程序。對於大多數應用程序或任何具有“社交”登錄的應用程序(如我們的應用程序),你需要“授權代碼”授權,這意味着你需要瀏覽器(或行爲類似瀏覽器的客戶端)來處理重定向和cookie,並從外部提供程序呈現用戶界面。

創建客戶端

我們的授權服務器的客戶端應用程序本身就是一個web應用程序,使用SpringBoot很容易創建。下面是一個示例:

ClientApplication.java
@EnableAutoConfiguration
@Configuration
@EnableOAuth2Sso
@RestController
public class ClientApplication {

  @RequestMapping("/")
  public String home(Principal user) {
    return "Hello " + user.getName();
  }

  public static void main(String[] args) {
    new SpringApplicationBuilder(ClientApplication.class)
        .properties("spring.config.name=client").run(args);
  }

}

ClientApplication類不能在 SocialApplication類的同一包(或子包)中創建。否則,Spring將在啓動 SocialApplicationserver時加載一些 ClientApplication自動配置,從而導致啓動錯誤。

客戶端的組成部分是主頁(只打印用戶名)和配置文件的顯式名稱(通過 spring.config.name=client)。當我們運行此應用程序時,它將查找我們提供的配置文件,如下所示:

client.yml

server:
  port: 9999
  context-path: /client
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      access-token-uri: http://localhost:8080/oauth/token
      user-authorization-uri: http://localhost:8080/oauth/authorize
    resource:
      user-info-uri: http://localhost:8080/me

該配置看起來與我們在主應用程序中使用的配置非常相似,但使用的是“acme”客戶端,而不是Facebook或Github客戶端。應用程序將在端口9999上運行,以避免與主應用程序衝突。它指向的是用戶信息端點“/me”,我們還沒有實現它。

請注意, server.context-path是顯式設置的,因此如果運行應用程序進行測試,請記住主頁是http://localhost:9999/client。單擊該鏈接應該會將你帶到auth服務器,並且在你通過所選的身份驗證服務器進行身份驗證後,你將被重定向回客戶端應用程序

如果同時在localhost上運行客戶端和auth服務器,則上下文路徑必須是顯式的,否則cookie路徑會衝突,並且兩個應用程序無法就會話標識符達成一致。

保護用戶信息端點

要使用我們的新授權服務器進行單點登錄,就像我們使用Facebook和Github一樣,它需要有一個受其創建的訪問令牌保護的 /user端點。到目前爲止,我們有一個 /user端點,它是通過用戶身份驗證時創建的cookie來保護的。要使用本地授予的訪問令牌來保護它,我們可以重新使用現有端點並在新路徑上爲其設置別名:

SocialApplication.java

@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
  Map<String, String> map = new LinkedHashMap<>();
  map.put("name", principal.getName());
  return map;
}

我們已將 Principal轉換爲 Map,以便隱藏不想向瀏覽器公開的部分,並取消兩個外部身份驗證提供程序之間端點的行爲。原則上,我們可以在這裏添加更詳細的信息,例如提供程序特定的唯一標識符,或者電子郵件地址(如果有的話)。

現在可以通過聲明我們的應用程序是資源服務器(以及授權服務器)來使用訪問令牌保護“/me”路徑。我們創建了一個新的配置類(作爲主應用程序中的n個內部類,但也可以將其拆分爲單獨的獨立類) :

SocialApplication.java    
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration
    extends ResourceServerConfigurerAdapter {
  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      .antMatcher("/me")
      .authorizeRequests().anyRequest().authenticated();
  }
}

此外,我們還需要爲主應用程序指定 @Order:

SocialApplication.java    
@SpringBootApplication
...
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SocialApplication extends WebSecurityConfigurerAdapter {
  ...
}

@EnableResourceServer註釋使用 @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER-1)創建安全過濾器,因此通過將主應用程序移動到 @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)確保“/me”的規則優先。

測試OAuth2客戶端

要測試新功能,你只需運行這兩個應用程序,然後在瀏覽器中訪問http://localhost:9999/client。客戶端應用程序將重定向到本地授權服務器,然後用戶可以選擇使用Facebook或Github進行身份驗證。完成後返回到測試客戶端,授予本地訪問令牌並完成身份驗證(你應該在瀏覽器中看到“Hello”消息)。如果你已經使用Github或Facebook進行了身份驗證,你甚至可能不會注意到遠程身份驗證。

爲未經身份驗證的用戶添加錯誤頁

在本節中,我們將修改前面構建的註銷應用程序,切換到Github身份驗證,並向無法進行身份驗證的用戶提供一些反饋。同時,我們利用這個機會擴展身份驗證邏輯,以包括一個規則,該規則只允許用戶屬於特定Github組織。“組織”是一個Github域特定的概念,但是也可以爲其他提供商設計類似的規則,例如,使用Google,你可能只需要驗證來自特定域的用戶。

切換到Github

註銷示例使用Facebook作爲OAuth2提供程序。我們可以通過更改本地配置輕鬆切換到Github :

application.yml    
security:
  oauth2:
    client:
      clientId: bd1c0a783ccdd1c9b9e4
      clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
      accessTokenUri: https://github.com/login/oauth/access_token
      userAuthorizationUri: https://github.com/login/oauth/authorize
      clientAuthenticationScheme: form
    resource:
      userInfoUri: https://api.github.com/user

檢測客戶端中的失敗的認證

在客戶端上,我們需要能夠爲無法進行身份驗證的用戶提供一些反饋。爲此,我們添加了一個div,其中包含一條消息:

index.html

<div class="container text-danger error" style="display:none">
There was an error (bad credentials).
</div>

此文本將僅在顯示“error”元素時顯示,因此我們需要一些代碼來執行此操作:

index.html

$.ajax({
  url : "/user",
  success : function(data) {
    $(".unauthenticated").hide();
    $("#user").html(data.userAuthentication.details.name);
    $(".authenticated").show();
  },
  error : function(data) {
    $("#user").html('');
    $(".unauthenticated").show();
    $(".authenticated").hide();
    if (location.href.indexOf("error=true")>=0) {
      $(".error").show();
    }
  }
});

身份驗證函數在加載時檢查瀏覽器位置,如果發現其中包含“error=true”的URL,則將設置標記。

添加錯誤頁面

爲了支持客戶端中的標誌設置,我們需要能夠捕獲身份驗證錯誤,並使用在查詢參數中設置的標誌重定向到主頁。因此,在一個常規的 @Controller中我們需要一個端點,如下所示:

SocialApplication.java

@RequestMapping("/unauthenticated")
public String unauthenticated() {
  return "redirect:/?error=true";
}

在示例應用程序中,我們將它放在主應用程序類中,該類現在是 @Controller (而不是 @RestController),因此它可以處理重定向。我們最需要的是從未經驗證的響應( HTTP 401,a.k.a. UNAUTHORIZED )到剛剛添加的“/unauthenticated”端點的映射:

ServletCustomizer.java

@Configuration
public class ServletCustomizer {
  @Bean
  public EmbeddedServletContainerCustomizer customizer() {
    return container -> {
      container.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unauthenticated"));
    };
  }
}

(在示例中,爲了簡明起見,這是作爲主應用程序內部的嵌套類添加的)

服務端響應401

如果用戶不能或不希望使用Github登錄,則Spring Security會返回401,因此如果你未能進行身份驗證(例如,拒絕令牌授予),則說明應用程序已經在運行。

爲了使事情更有趣,我們將擴展身份驗證規則以拒絕不在正確組織中的用戶。使用GithubAPI可以很容易地找到有關用戶的更多信息,因此我們只需要正確地將其插入身份驗證過程的部分。幸運的是,對於這樣一個簡單的用例,Spring Boot提供了一個簡單的擴展點:如果我們聲明一個類型爲 AuthoritiesExtractor@Bean,它將被用來構造經過身份驗證的用戶的權限(通常是“角色”)。我們可以使用鉤子函數判斷用戶正處於正確組織中,如果不是,則拋出異常:

SocialApplication.java

@Bean
public AuthoritiesExtractor authoritiesExtractor(OAuth2RestOperations template) {
  return map -> {
    String url = (String) map.get("organizations_url");
    @SuppressWarnings("unchecked")
    List<Map<String, Object>> orgs = template.getForObject(url, List.class);
    if (orgs.stream()
        .anyMatch(org -> "spring-projects".equals(org.get("login")))) {
      return AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
    }
    throw new BadCredentialsException("Not in Spring Projects origanization");
  };
}

請注意,我們已將 OAuth2RestOperations自動注入到此方法中,因此可以使用它代表已驗證的用戶訪問GithubAPI。我們這樣做,並循環遍歷organizations,尋找與“Spring-projects”匹配的組織(這是用於存儲Spring開源項目的組織)。如果你希望能夠成功進行身份驗證,並且不在Spring工程團隊中,則可以在此處替換自己的值。如果沒有匹配項,我們將拋出 BadCredentialsException,Spring Security將接受該異常並轉換爲401響應。

OAuth2RestOperations也必須作爲bean創建(從Spring Boot 1.4開始),但這很簡單,因爲使用 @Enableoauthso後,其成分都是可自動生成的:

@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
    return new OAuth2RestTemplate(resource, context);
}

顯然,上面的代碼可以推廣到其他身份驗證規則,有些適用於Github,有些適用於其他oauth2提供程序。你只需要知道 OAuth2RestOperations和驗證服務器提供方API的一些知識。

總結

我們已經看到了如何使用Spring Boot和Spring Security來構建多種樣式的應用程序,而不需要太多代碼。貫穿所有示例的主要主題是使用外部OAuth2提供程序的“社交”登錄。最終的示例甚至可以用於“內部”提供這種服務,因爲它具有與外部提供商相同的基本特性。所有示例應用程序都可以輕鬆擴展和重新配置,以滿足更具體的使用情形,通常只需更改配置文件即可。請記住,如果你使用自己服務器中的示例版本向Facebook或Github(或類似的)註冊,並獲取自己主機地址的客戶端憑據。記住不要將這些憑據放在公開的代碼管理工具中!

給大家推薦一個程序員學習羣:854818273。羣裏有分享的視頻,還有思維導圖
羣公告有視頻,都是乾貨的,你可以下載來看。主要分享分佈式架構、高可擴展、高性能、高併發、性能優化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分佈式項目實戰學習架構師視頻。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章