
在本節(jié)中,我們繼續(xù)我們的討論如何使用彈簧安全跟角在“單頁應(yīng)用程序”中。在這里,我們展示如何使用春季安全密鑰?春云將我們的 API 網(wǎng)關(guān)擴(kuò)展到后端資源,以執(zhí)行單點(diǎn)登錄和 OAuth2 令牌身份驗(yàn)證。這是一系列部分中的第五部分,您可以通過閱讀第一部分,或者您可以直接轉(zhuǎn)到Github中的源代碼.在最后一節(jié)我們構(gòu)建了一個(gè)小型分布式應(yīng)用程序,它使用春季會(huì)議對(duì)后端資源進(jìn)行身份驗(yàn)證,以及春云在 UI 服務(wù)器中實(shí)現(xiàn)嵌入式 API 網(wǎng)關(guān)。在本節(jié)中,我們將身份驗(yàn)證責(zé)任提取到單獨(dú)的服務(wù)器,以使我們的 UI 服務(wù)器成為授權(quán)服務(wù)器可能的許多單一登錄應(yīng)用程序中的第一個(gè)。這是當(dāng)今許多應(yīng)用程序中的常見模式,無論是在企業(yè)還是在社交初創(chuàng)公司中。我們將使用 OAuth2 服務(wù)器作為身份驗(yàn)證器,以便我們也可以使用它來為后端資源服務(wù)器授予令牌。Spring Cloud 會(huì)自動(dòng)將訪問令牌中繼到我們的后端,并使我們能夠進(jìn)一步簡(jiǎn)化 UI 和資源服務(wù)器的實(shí)現(xiàn)。
(資料圖片)
提醒:如果您正在使用示例應(yīng)用程序完成本節(jié),請(qǐng)務(wù)必清除瀏覽器緩存中的 Cookie 和 HTTP 基本憑據(jù)。在Chrome中,為單個(gè)服務(wù)器執(zhí)行此操作的最佳方法是打開一個(gè)新的隱身窗口。
我們的第一步是創(chuàng)建一個(gè)新服務(wù)器來處理身份驗(yàn)證和令牌管理。按照中的步驟操作第一部分我們可以從Spring Boot Initializr.例如,在類似UN*X的系統(tǒng)上使用curl:
$ curl https://start.spring.io/starter.tgz -d dependencies=web,security -d name=authserver | tar -xzvf -
然后,您可以將該項(xiàng)目(默認(rèn)情況下是普通的Maven Java項(xiàng)目)導(dǎo)入到您喜歡的IDE中,或者只是在命令行上處理文件和“mvn”。
我們需要添加春季OAuth依賴關(guān)系,所以在我們的聚 甲醛我們添加:
絨球.xml
org.springframework.security.oauth spring-security-oauth2
授權(quán)服務(wù)器非常容易實(shí)現(xiàn)。最小版本如下所示:
身份驗(yàn)證服務(wù)器應(yīng)用程序.java
@SpringBootApplication@EnableAuthorizationServerpublic class AuthserverApplication extends WebMvcConfigurerAdapter { public static void main(String[] args) { SpringApplication.run(AuthserverApplication.class, args); }}
我們只需要再做 1 件事(添加后):??@EnableAuthorizationServer?
?
應(yīng)用程序?qū)傩?/p>
---...security.oauth2.client.clientId: acmesecurity.oauth2.client.clientSecret: acmesecretsecurity.oauth2.client.authorized-grant-types: authorization_code,refresh_token,passwordsecurity.oauth2.client.scope: openid---
這會(huì)使用機(jī)密和一些授權(quán)授權(quán)類型(包括“authorization_code”)注冊(cè)客戶端“acme”。
現(xiàn)在讓我們讓它在端口 9999 上運(yùn)行,使用可預(yù)測(cè)的密碼進(jìn)行測(cè)試:
應(yīng)用程序?qū)傩?/p>
server.port=9999security.user.password=passwordserver.contextPath=/uaa...
我們還設(shè)置了上下文路徑,使其不使用默認(rèn)值(“/”),否則您可能會(huì)將本地主機(jī)上其他服務(wù)器的cookie發(fā)送到錯(cuò)誤的服務(wù)器。因此,讓服務(wù)器運(yùn)行,我們可以確保它正常工作:
$ mvn spring-boot:run
或在 IDE 中啟動(dòng)該方法。??main()?
?
我們的服務(wù)器正在使用 Spring Boot 默認(rèn)安全設(shè)置,所以就像服務(wù)器一樣第一部分?它將受到 HTTP 基本身份驗(yàn)證的保護(hù)。啟動(dòng)授權(quán)代碼令牌授予?您訪問授權(quán)端點(diǎn),例如??http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com??身份驗(yàn)證后,您將獲得一個(gè)重定向到帶有授權(quán)代碼的 example.com,例如http://example.com/?code=jYWioI.
出于此示例應(yīng)用程序的目的,我們創(chuàng)建了一個(gè)沒有注冊(cè)重定向的客戶端“Acme”,這使我們能夠獲得 example.com 的重定向。在生產(chǎn)應(yīng)用程序中,應(yīng)始終注冊(cè)重定向(并使用 HTTPS)。 |
可以使用令牌終結(jié)點(diǎn)上的“acme”客戶端憑據(jù)將代碼交換為訪問令牌:
$ curl acme:acmesecret@localhost:9999/uaa/oauth/token \-d grant_type=authorization_code -d client_id=acme \-d redirect_uri=http://example.com -d code=jYWioI{"access_token":"2219199c-966e-4466-8b7e-12bb9038c9bb","token_type":"bearer","refresh_token":"d193caf4-5643-4988-9a4a-1c03c9d657aa","expires_in":43199,"scope":"openid"}
訪問令牌是 UUID (“2219199c...”),由服務(wù)器中的內(nèi)存中令牌存儲(chǔ)提供支持。我們還獲得了一個(gè)刷新令牌,當(dāng)當(dāng)前訪問令牌過期時(shí),我們可以使用該令牌獲取新的訪問令牌。
由于我們?cè)试S為“Acme”客戶端授予“密碼”,因此我們還可以使用 curl 和用戶憑據(jù)而不是授權(quán)代碼直接從令牌端點(diǎn)獲取令牌。這不適合基于瀏覽器的客戶端,但對(duì)于測(cè)試很有用。 |
如果您點(diǎn)擊上面的鏈接,您將看到Spring OAuth提供的白標(biāo)UI。首先,我們將使用它,我們可以稍后回來加強(qiáng)它,就像我們?cè)诘诙糠謱?duì)于自包含服務(wù)器。
如果我們繼續(xù)第四部分,我們的資源服務(wù)器正在使用春季會(huì)議用于身份驗(yàn)證,因此我們可以將其取出并替換為Spring OAuth。我們還需要?jiǎng)h除 Spring 會(huì)話和 Redis 依賴項(xiàng),因此將其替換為:
絨球.xml
org.springframework.session spring-session org.springframework.boot spring-boot-starter-redis
有了這個(gè):
絨球.xml
org.springframework.security.oauth spring-security-oauth2
,然后從??Filter?
?主要應(yīng)用類,將其替換為方便的注釋(來自 Spring Security OAuth2):??@EnableResourceServer?
?
資源應(yīng)用.java
@SpringBootApplication@RestController@EnableResourceServerclass ResourceApplication { @RequestMapping("/") public Message home() { return new Message("Hello World"); } public static void main(String[] args) { SpringApplication.run(ResourceApplication.class, args); }}
通過這一更改,應(yīng)用程序已準(zhǔn)備好挑戰(zhàn)訪問令牌而不是HTTP Basic,但我們需要配置更改才能實(shí)際完成該過程。我們將添加少量外部配置(在“application.properties”中),以允許資源服務(wù)器解碼給定的令牌并對(duì)用戶進(jìn)行身份驗(yàn)證:
應(yīng)用程序?qū)傩?/p>
...security.oauth2.resource.userInfoUri: http://localhost:9999/uaa/user
這告訴服務(wù)器它可以使用令牌訪問“/user”終結(jié)點(diǎn)并使用它來派生身份驗(yàn)證信息(這有點(diǎn)類似于??“/me”終結(jié)點(diǎn)??在臉書 API 中)。實(shí)際上,它為資源服務(wù)器提供了一種解碼令牌的方法,如Spring OAuth2中的接口所示。??ResourceServerTokenServices?
?
運(yùn)行應(yīng)用程序并使用命令行客戶端訪問主頁:
$ curl -v localhost:9000> GET / HTTP/1.1> User-Agent: curl/7.35.0> Host: localhost:9000> Accept: */*>< HTTP/1.1 401 Unauthorized...< WWW-Authenticate: Bearer realm="null", error="unauthorized", error_description="An Authentication object was not found in the SecurityContext"< Content-Type: application/json;charset=UTF-8{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}
您將看到一個(gè)帶有“WWW-Authenticate”標(biāo)頭的 401,指示它需要持有者令牌。
到目前為止,這不是將資源服務(wù)器與解碼令牌的方式連接起來的唯一方法。事實(shí)上,這是一個(gè)最低的共同點(diǎn)(不是規(guī)范的一部分),但通常可以從OAuth2提供商(如Facebook,Cloud Foundry,Github)獲得,并且可以使用其他選擇。例如,您可以在令牌本身中對(duì)用戶身份驗(yàn)證進(jìn)行編碼(例如,使用? |
在授權(quán)服務(wù)器上,我們可以輕松添加該端點(diǎn)
身份驗(yàn)證服務(wù)器應(yīng)用程序.java
@SpringBootApplication@RestController@EnableAuthorizationServer@EnableResourceServerpublic class AuthserverApplication { @RequestMapping("/user") public Principal user(Principal user) { return user; } ...}
我們添加了一個(gè)與 UI 服務(wù)器相同的??@RequestMapping?
?第二部分,以及來自 Spring OAuth 的注釋,默認(rèn)情況下,它保護(hù)授權(quán)服務(wù)器中除“/oauth/*”端點(diǎn)之外的所有內(nèi)容。??@EnableResourceServer?
?
有了該端點(diǎn),我們可以測(cè)試它和問候語資源,因?yàn)樗鼈儸F(xiàn)在都接受由授權(quán)服務(wù)器創(chuàng)建的持有者令牌:
$ TOKEN=2219199c-966e-4466-8b7e-12bb9038c9bb$ curl -H "Authorization: Bearer $TOKEN" localhost:9000{"id":"03af8be3-2fc3-4d75-acf7-c484d9cf32b1","content":"Hello World"}$ curl -H "Authorization: Bearer $TOKEN" localhost:9999/uaa/user{"details":...,"principal":{"username":"user",...},"name":"user"}
(替換您從自己的授權(quán)服務(wù)器獲取的訪問令牌的值,以使其自己工作)。
我們需要完成的這個(gè)應(yīng)用程序的最后一部分是 UI 服務(wù)器,提取身份驗(yàn)證部分并委派給授權(quán)服務(wù)器。所以,就像資源服務(wù)器,我們首先需要?jiǎng)h除 Spring 會(huì)話和 Redis 依賴項(xiàng),并將它們替換為 Spring OAuth2。因?yàn)槲覀冊(cè)?UI 層中使用 Zuul,所以我們實(shí)際使用而不是直接使用(這設(shè)置了一些自動(dòng)配置以通過代理中繼令牌)。??spring-cloud-starter-oauth2?
???spring-security-oauth2?
?
完成后,我們還可以刪除會(huì)話過濾器和“/user”端點(diǎn),并將應(yīng)用程序設(shè)置為重定向到授權(quán)服務(wù)器(使用注釋):??@EnableOAuth2Sso?
?
Ui應(yīng)用程序.java
@SpringBootApplication@EnableZuulProxy@EnableOAuth2Ssopublic class UiApplication { public static void main(String[] args) { SpringApplication.run(UiApplication.class, args); }...}
召回自第四部分UI 服務(wù)器憑借 ,充當(dāng) API 網(wǎng)關(guān),我們可以在 YAML 中聲明路由映射。因此,可以將“/user”端點(diǎn)代理到授權(quán)服務(wù)器:??@EnableZuulProxy?
?
應(yīng)用程序.yml
zuul: routes: resource: path: /resource/** url: http://localhost:9000 user: path: /user/** url: http://localhost:9999/uaa/user
最后,我們需要將應(yīng)用程序更改為 a,因?yàn)楝F(xiàn)在它將用于修改由以下設(shè)置的 SSO 篩選器鏈中的默認(rèn)值:??WebSecurityConfigurerAdapter?
???@EnableOAuth2Sso?
?
安全配置.java
@SpringBootApplication@EnableZuulProxy@EnableOAuth2Ssopublic class UiApplication extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .logout().logoutSuccessUrl("/").and() .authorizeRequests().antMatchers("/index.html", "/app.html", "/") .permitAll().anyRequest().authenticated().and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); }}
主要更改(除了基類名稱之外)是匹配器進(jìn)入自己的方法,不再需要任何方法。顯式配置顯式添加不受保護(hù)的成功 URL,以便成功返回 XHR 請(qǐng)求。??formLogin()?
???logout()?
???/logout?
?
注釋還有一些必需的外部配置屬性,以便能夠與正確的授權(quán)服務(wù)器聯(lián)系并進(jìn)行身份驗(yàn)證。所以我們?cè)?:??@EnableOAuth2Sso?
???application.yml?
?
應(yīng)用程序.yml
security: ... oauth2: client: accessTokenUri: http://localhost:9999/uaa/oauth/token userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize clientId: acme clientSecret: acmesecret resource: userInfoUri: http://localhost:9999/uaa/user
其中大部分是關(guān)于OAuth2客戶端(“acme”)和授權(quán)服務(wù)器位置。還有一個(gè)(就像在資源服務(wù)器中一樣),以便用戶可以在 UI 應(yīng)用本身中進(jìn)行身份驗(yàn)證。??userInfoUri?
?
如果希望 UI 應(yīng)用程序能夠自動(dòng)刷新過期的訪問令牌,則必須將令牌注入執(zhí)行中繼的 Zuul 篩選器中。您可以通過創(chuàng)建該類型的 bean 來執(zhí)行此操作(有關(guān)詳細(xì)信息,請(qǐng)查看):? |
@Beanprotected OAuth2RestTemplate OAuth2RestTemplate( OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) { return new OAuth2RestTemplate(resource, context);}
我們?nèi)匀恍枰獙?duì)前端的 UI 應(yīng)用程序進(jìn)行一些調(diào)整以觸發(fā)到授權(quán)服務(wù)器的重定向。在這個(gè)簡(jiǎn)單的演示中,我們可以將 Angular 應(yīng)用程序簡(jiǎn)化為最基本的內(nèi)容,以便您可以更清楚地看到正在發(fā)生的事情。因此,我們暫時(shí)放棄使用表單或路線,回到單個(gè) Angular 組件:
app.component.ts
import { Component } from "@angular/core";import { HttpClient } from "@angular/common/http";import "rxjs/add/operator/finally";@Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"]})export class AppComponent { title = "Demo"; authenticated = false; greeting = {}; constructor(private http: HttpClient) { this.authenticate(); } authenticate() { this.http.get("user").subscribe(response => { if (response["name"]) { this.authenticated = true; this.http.get("resource").subscribe(data => this.greeting = data); } else { this.authenticated = false; } }, () => { this.authenticated = false; }); } logout() { this.http.post("logout", {}).finally(() => { this.authenticated = false; }).subscribe(); }}
處理所有內(nèi)容,獲取用戶詳細(xì)信息,如果成功,則發(fā)送問候語。它還提供該功能。??AppComponent?
???logout?
?
現(xiàn)在我們需要為這個(gè)新組件創(chuàng)建模板:
app.component.html
Greeting
The ID is {{greeting.id}}
The content is {{greeting.content}}
Login to see your greeting
并將其作為 包含在主頁中。?
?
?? 請(qǐng)注意,“登錄”的導(dǎo)航鏈接是帶有(不是角度路由)的常規(guī)鏈接。它轉(zhuǎn)到的“/login”端點(diǎn)由 Spring Security 處理,如果用戶未經(jīng)身份驗(yàn)證,則會(huì)導(dǎo)致重定向到授權(quán)服務(wù)器。?
?href?
?它是如何工作的?
現(xiàn)在一起運(yùn)行所有服務(wù)器,并在瀏覽器中訪問 UI??http://localhost:8080??.單擊“登錄”鏈接,您將被重定向到授權(quán)服務(wù)器進(jìn)行身份驗(yàn)證(HTTP Basic彈出窗口)并批準(zhǔn)令牌授予(白標(biāo)HTML),然后重定向到UI中的主頁,并使用與我們對(duì)UI進(jìn)行身份驗(yàn)證相同的令牌從OAuth2資源服務(wù)器獲取問候語。
如果您使用某些開發(fā)人員工具,則可以在瀏覽器中看到瀏覽器和后端之間的交互(通常 F12 會(huì)打開它,默認(rèn)情況下在 Chrome 中工作,可能需要 Firefox 中的插件)。以下是摘要:
動(dòng)詞
路徑
地位
響應(yīng)
獲取
/
200
索引.html
獲取
/*。.js
200
來自角度的資產(chǎn)
獲取
/用戶
302
重定向至登錄頁面
獲取
/登錄
302
重定向至“身份驗(yàn)證服務(wù)器”診斷樹
獲取
(UAA)/OAuth/authorize
401
(忽略)
獲取
/登錄
302
重定向至“身份驗(yàn)證服務(wù)器”診斷樹
獲取
(UAA)/OAuth/authorize
200
HTTP 基本身份驗(yàn)證發(fā)生在這里
發(fā)布
(UAA)/OAuth/authorize
302
用戶批準(zhǔn)授權(quán),重定向至/登錄
獲取
/登錄
302
重定向至主頁
獲取
/用戶
200
(代理)經(jīng)過 JSON 身份驗(yàn)證的用戶
獲取
/應(yīng)用.html
200
主頁的 HTML 部分
獲取
/資源
200
(代理)JSON問候語
前綴為 (uaa) 的請(qǐng)求是發(fā)往授權(quán)服務(wù)器的請(qǐng)求。標(biāo)記為“忽略”的響應(yīng)是 Angular 在 XHR 調(diào)用中收到的響應(yīng),由于我們不處理這些數(shù)據(jù),因此它們被丟棄在地板上。對(duì)于“/user”資源,我們確實(shí)會(huì)查找經(jīng)過身份驗(yàn)證的用戶,但由于它在第一次調(diào)用中不存在,因此該響應(yīng)將被丟棄。
在 UI 的“/trace”端點(diǎn)(向下滾動(dòng)到底部)中,您將看到對(duì)“/user”和“/resource”的代理后端請(qǐng)求,以及持有者令牌而不是 cookie(因?yàn)樗緛頃?huì)在?
?remote:true?
?第四部分) 用于身份驗(yàn)證。Spring Cloud Security已經(jīng)為我們解決了這個(gè)問題:通過認(rèn)識(shí)到我們已經(jīng)并且已經(jīng)發(fā)現(xiàn)(默認(rèn)情況下)我們希望將令牌中繼到代理后端。??@EnableOAuth2Sso?
???@EnableZuulProxy?
?
與前面的部分一樣,嘗試為“/trace”使用不同的瀏覽器,這樣就不會(huì)有身份驗(yàn)證交叉的機(jī)會(huì)(例如,如果您使用 Chrome 測(cè)試用戶界面,請(qǐng)使用 Firefox)。
注銷體驗(yàn)
如果單擊“注銷”鏈接,您將看到主頁已更改(不再顯示問候語),因此用戶不再通過UI服務(wù)器進(jìn)行身份驗(yàn)證。單擊“登錄”,您實(shí)際上不需要返回授權(quán)服務(wù)器中的身份驗(yàn)證和批準(zhǔn)周期(因?yàn)槟形醋N)。關(guān)于這是否是理想的用戶體驗(yàn),意見分歧,這是一個(gè)眾所周知的棘手問題(單點(diǎn)注銷:科學(xué)直銷文章和希伯勒斯文檔?).理想的用戶體驗(yàn)在技術(shù)上可能不可行,有時(shí)您還必須懷疑用戶是否真的想要他們所說的東西。“我想"注銷"將我注銷”聽起來很簡(jiǎn)單,但明顯的回應(yīng)是,“注銷了什么?您想注銷此 SSO 服務(wù)器控制的所有系統(tǒng),還是僅注銷您單擊“注銷”鏈接的系統(tǒng)?如果您有興趣,那么有后面的部分本教程將更深入地討論它。
結(jié)論
這幾乎是我們通過Spring Security和Angular堆棧的淺層之旅的結(jié)束。我們現(xiàn)在有一個(gè)很好的架構(gòu),在三個(gè)獨(dú)立的組件中明確了職責(zé),即 UI/API 網(wǎng)關(guān)、資源服務(wù)器和授權(quán)服務(wù)器/令牌授予者。現(xiàn)在,所有層中的非業(yè)務(wù)代碼量都很少,并且很容易看出在哪里擴(kuò)展和改進(jìn)了具有更多業(yè)務(wù)邏輯的實(shí)現(xiàn)。接下來的步驟將是整理授權(quán)服務(wù)器中的UI,并可能添加更多測(cè)試,包括JavaScript客戶端上的測(cè)試。另一個(gè)有趣的任務(wù)是提取所有樣板代碼并將其放入一個(gè)庫(kù)中(例如“spring-security-angular”),其中包含Spring Security和Spring Session自動(dòng)配置以及Angular部分中導(dǎo)航控制器的一些webjars資源。閱讀了 thir 系列中的部分后,任何希望了解 Angular 或 Spring Security 內(nèi)部工作原理的人都可能會(huì)感到失望,但如果您想了解它們?nèi)绾魏芎玫貐f(xié)同工作以及一點(diǎn)點(diǎn)配置如何走很長(zhǎng)的路,那么希望您能有一個(gè)很好的體驗(yàn)。春云是新的,這些示例在編寫時(shí)需要快照,但有可用的候選版本和即將推出的 GA 版本,因此請(qǐng)查看并發(fā)送一些反饋通過 Github或gitter.im.
這下一節(jié)在本系列中是關(guān)于訪問決策(身份驗(yàn)證之外),并在同一代理后面使用多個(gè) UI 應(yīng)用程序。
附錄:授權(quán)服務(wù)器的引導(dǎo) UI 和 JWT 令牌
您將在Github中的源代碼它有一個(gè)漂亮的登錄頁面和用戶批準(zhǔn)頁面,其實(shí)現(xiàn)方式類似于我們?cè)诘卿涰撁嬷袌?zhí)行的方式第二部分.它還使用智威湯遜對(duì)令牌進(jìn)行編碼,因此資源服務(wù)器可以從令牌本身中提取足夠的信息來執(zhí)行簡(jiǎn)單的身份驗(yàn)證,而不是使用“/user”終結(jié)點(diǎn)。瀏覽器客戶端仍然使用它,通過 UI 服務(wù)器代理,以便它可以確定用戶是否經(jīng)過身份驗(yàn)證(與實(shí)際應(yīng)用程序中對(duì)資源服務(wù)器的可能調(diào)用次數(shù)相比,它不需要經(jīng)常這樣做)。
多個(gè) UI 應(yīng)用程序和一個(gè)網(wǎng)關(guān)
在本節(jié)中,我們繼續(xù)我們的討論如何使用彈簧安全跟角在“單頁應(yīng)用程序”中。在這里,我們展示如何使用春季會(huì)議?春云結(jié)合我們?cè)诘诙糠趾偷谒牟糠种袠?gòu)建的系統(tǒng)的功能,實(shí)際上最終構(gòu)建了 3 個(gè)具有不同職責(zé)的單頁應(yīng)用程序。目的是建立一個(gè)網(wǎng)關(guān)(如第四部分),不僅用于 API 資源,還用于從后端服務(wù)器加載 UI。我們簡(jiǎn)化了令牌整理位第二部分通過使用網(wǎng)關(guān)將身份驗(yàn)證傳遞到后端。然后,我們擴(kuò)展系統(tǒng)以展示如何在后端做出本地、精細(xì)的訪問決策,同時(shí)仍然控制網(wǎng)關(guān)的身份和身份驗(yàn)證。對(duì)于構(gòu)建分布式系統(tǒng)來說,這是一個(gè)非常強(qiáng)大的模型,并且在我們構(gòu)建的代碼中引入功能時(shí),我們可以探索許多好處。
提醒:如果您正在使用示例應(yīng)用程序完成本節(jié),請(qǐng)務(wù)必清除瀏覽器緩存中的 Cookie 和 HTTP 基本憑據(jù)。在Chrome中,最好的方法是打開一個(gè)新的隱身窗口。
目標(biāo)體系結(jié)構(gòu)
以下是我們將要開始構(gòu)建的基本系統(tǒng)的圖片:
與本系列中的其他示例應(yīng)用程序一樣,它有一個(gè) UI(HTML 和 JavaScript)和一個(gè)資源服務(wù)器。喜歡中的示例第四節(jié)它有一個(gè)網(wǎng)關(guān),但在這里它是獨(dú)立的,不是 UI 的一部分。UI 有效地成為后端的一部分,為我們提供了更多選擇來重新配置和重新實(shí)現(xiàn)功能,并帶來了我們將看到的其他好處。
瀏覽器會(huì)轉(zhuǎn)到網(wǎng)關(guān)以獲取所有內(nèi)容,并且不必了解后端的體系結(jié)構(gòu)(從根本上說,它不知道有后端)。瀏覽器在此網(wǎng)關(guān)中執(zhí)行的操作之一是身份驗(yàn)證,例如,它發(fā)送用戶名和密碼,例如第二節(jié),它會(huì)得到一個(gè)餅干作為回報(bào)。在后續(xù)請(qǐng)求中,它會(huì)自動(dòng)提供 cookie,網(wǎng)關(guān)將其傳遞到后端。無需在客戶端上編寫代碼即可啟用 cookie 傳遞。后端使用 cookie 進(jìn)行身份驗(yàn)證,并且由于所有組件共享一個(gè)會(huì)話,因此它們共享有關(guān)用戶的相同信息。與此形成對(duì)比第五節(jié)其中 cookie 必須轉(zhuǎn)換為網(wǎng)關(guān)中的訪問令牌,然后訪問令牌必須由所有后端組件獨(dú)立解碼。
如在第四節(jié)網(wǎng)關(guān)簡(jiǎn)化了客戶端和服務(wù)器之間的交互,并提供了一個(gè)小的、定義明確的表面來處理安全性。例如,我們不需要擔(dān)心跨源資源共享,這是一個(gè)受歡迎的緩解,因?yàn)樗苋菀壮鲥e(cuò)。
我們將要構(gòu)建的完整項(xiàng)目的源代碼位于Github在這里,因此您可以根據(jù)需要克隆項(xiàng)目并直接從那里工作。此系統(tǒng)的最終狀態(tài)中有一個(gè)額外的組件(“雙管理員”),因此暫時(shí)忽略它。
構(gòu)建后端
在此體系結(jié)構(gòu)中,后端與“春季會(huì)議”我們內(nèi)置的示例第三節(jié),除了它實(shí)際上不需要登錄頁面。獲得我們這里想要的內(nèi)容的最簡(jiǎn)單方法可能是從第 III 節(jié)復(fù)制“資源”服務(wù)器并從“基本”樣本在第一節(jié).要從“基本”UI到我們?cè)谶@里想要的UI,我們只需要添加幾個(gè)依賴項(xiàng)(就像我們第一次使用時(shí)一樣春季會(huì)議在第三節(jié)中):
絨球.xml
org.springframework.session spring-session org.springframework.boot spring-boot-starter-redis 由于這現(xiàn)在是一個(gè) UI,因此不需要“/resource”終結(jié)點(diǎn)。完成此操作后,您將擁有一個(gè)非常簡(jiǎn)單的 Angular 應(yīng)用程序(與“基本”示例中相同),它大大簡(jiǎn)化了對(duì)其行為的測(cè)試和推理。
最后,我們希望這個(gè)服務(wù)器作為后端運(yùn)行,所以我們將給它一個(gè)非默認(rèn)端口來偵聽(在):?
?application.properties?
?應(yīng)用程序?qū)傩?/p>
server.port: 8081security.sessions: NEVER如果這是全部內(nèi)容,那么應(yīng)用程序?qū)⑹前踩模⑶颐麨椤皍ser”的用戶可以使用隨機(jī)密碼訪問,但在啟動(dòng)時(shí)打印在控制臺(tái)上(在日志級(jí)別 INFO)。“security.sessions”設(shè)置意味著Spring Security將接受cookie作為身份驗(yàn)證令牌,但除非它們已經(jīng)存在,否則不會(huì)創(chuàng)建它們。?
?application.properties?
?資源服務(wù)器
資源服務(wù)器很容易從我們現(xiàn)有的一個(gè)示例生成。它與 中的“春季會(huì)話”資源服務(wù)器相同第三節(jié):只是一個(gè)“/resource”端點(diǎn) Spring 會(huì)話來獲取分布式會(huì)話數(shù)據(jù)。我們希望此服務(wù)器具有要偵聽的非默認(rèn)端口,并且我們希望能夠在會(huì)話中查找身份驗(yàn)證,因此我們需要這個(gè)(在):?
?application.properties?
?應(yīng)用程序?qū)傩?/p>
server.port: 9000security.sessions: NEVER我們將發(fā)布對(duì)消息資源的更改,這是本教程中的新功能。這意味著我們將需要在后端進(jìn)行CSRF保護(hù),并且我們需要做通常的技巧來使Spring Security與Angular很好地配合使用:
@Overrideprotected void configure(HttpSecurity http) throws Exception { http.csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());}完成的示例是在 GitHub 這里如果你想看一眼。
網(wǎng)關(guān)
對(duì)于網(wǎng)關(guān)的初始實(shí)現(xiàn)(最簡(jiǎn)單的方法),我們可以只使用一個(gè)空的 Spring Boot Web 應(yīng)用程序并添加注釋。正如我們?cè)?
?@EnableZuulProxy?
?第一節(jié)有幾種方法可以做到這一點(diǎn),一種是使用Spring Initializr以生成框架項(xiàng)目。更容易的是使用春云初始化這是一回事,但對(duì)于春云應(yīng)用。使用與第 I 節(jié)中相同的命令行操作順序:$ mkdir gateway && cd gateway$ curl https://cloud-start.spring.io/starter.tgz -d style=web \ -d style=security -d style=cloud-zuul -d name=gateway \ -d style=redis | tar -xzvf -然后,您可以將該項(xiàng)目(默認(rèn)情況下是普通的Maven Java項(xiàng)目)導(dǎo)入到您喜歡的IDE中,或者只是在命令行上處理文件和“mvn”。有一個(gè)版本在 GitHub 中如果你想從那里開始,但它有一些我們還不需要的額外功能。
從空白的 Initializr 應(yīng)用程序開始,我們添加 Spring 會(huì)話依賴項(xiàng)(如上面的 UI 所示)。網(wǎng)關(guān)已準(zhǔn)備好運(yùn)行,但它還不知道我們的后端服務(wù),所以讓我們?cè)谄渲性O(shè)置它(如果您執(zhí)行了上面的 curl 操作,請(qǐng)重命名):?
?application.yml?
???application.properties?
?應(yīng)用程序.yml
zuul: sensitive-headers: routes: ui: url: http://localhost:8081 resource: url: http://localhost:9000security: user: password: password sessions: ALWAYS代理中有 2 條路由,它們都使用該屬性將 cookie 傳遞到下游,UI 和資源服務(wù)器各一條,并且我們?cè)O(shè)置了默認(rèn)密碼和會(huì)話持久性策略(告訴 Spring Security 始終在身份驗(yàn)證時(shí)創(chuàng)建會(huì)話)。最后一點(diǎn)很重要,因?yàn)槲覀兿M诰W(wǎng)關(guān)中管理身份驗(yàn)證,因此會(huì)話。?
?sensitive-headers?
?啟動(dòng)并運(yùn)行
我們現(xiàn)在有三個(gè)組件,在 3 個(gè)端口上運(yùn)行。如果將瀏覽器指向??http://localhost:8080/ui/??您應(yīng)該會(huì)收到 HTTP 基本質(zhì)詢,并且可以作為“用戶/密碼”(您在網(wǎng)關(guān)中的憑據(jù))進(jìn)行身份驗(yàn)證,完成此操作后,您應(yīng)該會(huì)在 UI 中看到一個(gè)問候語,通過通過代理對(duì)資源服務(wù)器的后端調(diào)用。
如果您使用某些開發(fā)人員工具,則可以在瀏覽器中看到瀏覽器和后端之間的交互(通常 F12 會(huì)打開它,默認(rèn)情況下在 Chrome 中工作,可能需要 Firefox 中的插件)。以下是摘要:
動(dòng)詞
路徑
地位
響應(yīng)
獲取
/用戶界面/
401
瀏覽器提示進(jìn)行身份驗(yàn)證
獲取
/用戶界面/
200
索引.html
獲取
/ui/*.js
200
角度資產(chǎn)
獲取
/ui/js/hello.js
200
應(yīng)用程序邏輯
獲取
/ui/user
200
認(rèn)證
獲取
/資源/
200
JSON問候語
您可能看不到 401,因?yàn)闉g覽器將主頁加載視為單個(gè)交互。所有請(qǐng)求都是代理的(網(wǎng)關(guān)中尚無內(nèi)容,超出執(zhí)行器端點(diǎn)進(jìn)行管理)。
萬歲,它有效!您有兩個(gè)后端服務(wù)器,其中一個(gè)是 UI,每個(gè)服務(wù)器都具有獨(dú)立的功能,可以單獨(dú)測(cè)試,并且它們與您控制的安全網(wǎng)關(guān)連接在一起,并且您已為其配置了身份驗(yàn)證。如果瀏覽器無法訪問后端,那也沒關(guān)系(事實(shí)上,這可能是一個(gè)優(yōu)勢(shì),因?yàn)樗梢宰屇玫乜刂莆锢戆踩裕?/p>
添加登錄表單
就像在“基本”示例中一樣第一節(jié)我們現(xiàn)在可以將登錄表單添加到網(wǎng)關(guān),例如通過從第二節(jié).當(dāng)我們這樣做時(shí),我們還可以在網(wǎng)關(guān)中添加一些基本的導(dǎo)航元素,這樣用戶就不必知道代理中 UI 后端的路徑。因此,讓我們首先將靜態(tài)資產(chǎn)從“單個(gè)”UI 復(fù)制到網(wǎng)關(guān)中,刪除消息呈現(xiàn)并將登錄表單插入到我們的主頁(在某處):?
?
?? 應(yīng)用.html
代替消息渲染,我們將有一個(gè)漂亮的大導(dǎo)航按鈕:
索引.html
如果你正在github中查看示例,它還有一個(gè)帶有“注銷”按鈕的最小導(dǎo)航欄。以下是屏幕截圖中的登錄表單:
為了支持登錄表單,我們需要一些 TypeScript 和一個(gè)組件來實(shí)現(xiàn)我們?cè)?中聲明的功能,并且我們需要設(shè)置標(biāo)志,以便主頁將根據(jù)用戶是否經(jīng)過身份驗(yàn)證而以不同的方式呈現(xiàn)。例如:?
?login()?
????
???authenticated?
?app.component.ts
include::src/app/app.component.ts其中函數(shù)的實(shí)現(xiàn)類似于?
?login()?
?第二節(jié).我們可以使用 來存儲(chǔ)標(biāo)志,因?yàn)樵谶@個(gè)簡(jiǎn)單的應(yīng)用程序中只有一個(gè)組件。?
?self?
???authenticated?
?如果我們運(yùn)行此增強(qiáng)的網(wǎng)關(guān),則不必記住UI的URL,只需加載主頁并跟蹤鏈接即可。下面是經(jīng)過身份驗(yàn)證的用戶的主頁:
后端的精細(xì)訪問決策
到目前為止,我們的應(yīng)用程序在功能上與第三節(jié)或第四節(jié),但帶有額外的專用網(wǎng)關(guān)。額外層的優(yōu)勢(shì)可能還不明顯,但我們可以通過稍微擴(kuò)展系統(tǒng)來強(qiáng)調(diào)它。假設(shè)我們想使用該網(wǎng)關(guān)公開另一個(gè)后端 UI,以便用戶“管理”主 UI 中的內(nèi)容,并且我們希望將此功能的訪問權(quán)限限制為具有特殊角色的用戶。因此,我們將在代理后面添加一個(gè)“Admin”應(yīng)用程序,系統(tǒng)將如下所示:
網(wǎng)關(guān)中有一個(gè)新組件(管理員)和一個(gè)新路由:?
?application.yml?
?應(yīng)用程序.yml
zuul: sensitive-headers: routes: ui: url: http://localhost:8081 admin: url: http://localhost:8082 resource: url: http://localhost:9000在上面的網(wǎng)關(guān)框(綠色字母)的框圖中指示了現(xiàn)有 UI 可供“USER”角色使用的事實(shí),以及需要“ADMIN”角色才能轉(zhuǎn)到 Admin 應(yīng)用程序的事實(shí)。“ADMIN”角色的訪問決策可以應(yīng)用于網(wǎng)關(guān),在這種情況下,它將出現(xiàn)在 中,也可以應(yīng)用于管理應(yīng)用程序本身(我們將在下面看到如何執(zhí)行此操作)。?
?WebSecurityConfigurerAdapter?
?因此,首先,創(chuàng)建一個(gè)新的 Spring Boot 應(yīng)用程序,或者復(fù)制 UI 并進(jìn)行編輯。除了名稱開始之外,無需在 UI 應(yīng)用中進(jìn)行太多更改。完成的應(yīng)用在Github在這里.
假設(shè)在管理員應(yīng)用程序中,我們希望區(qū)分“READER”和“WRITER”角色,以便我們可以允許(假設(shè))審計(jì)員用戶查看主管理員用戶所做的更改。這是一個(gè)精細(xì)的訪問決策,其中規(guī)則僅在后端應(yīng)用程序中是已知的,并且應(yīng)該只知道。在網(wǎng)關(guān)中,我們只需要確保我們的用戶帳戶具有所需的角色,并且此信息可用,但網(wǎng)關(guān)不需要知道如何解釋它。在網(wǎng)關(guān)中,我們創(chuàng)建用戶帳戶以保持示例應(yīng)用程序的獨(dú)立性:
安全配置.class
@Configurationpublic class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("password").roles("USER") .and() .withUser("admin").password("admin").roles("USER", "ADMIN", "READER", "WRITER") .and() .withUser("audit").password("audit").roles("USER", "ADMIN", "READER"); }}其中“管理員”用戶已通過 3 個(gè)新角色(“管理員”、“讀取者”和“編寫者”)進(jìn)行了增強(qiáng),我們還添加了具有“管理員”訪問權(quán)限的“審核”用戶,但不是“編寫者”。
在生產(chǎn)系統(tǒng)中,用戶帳戶數(shù)據(jù)將在后端數(shù)據(jù)庫(kù)(很可能是目錄服務(wù))中進(jìn)行管理,而不是在 Spring 配置中進(jìn)行硬編碼。連接到此類數(shù)據(jù)庫(kù)的示例應(yīng)用程序很容易在互聯(lián)網(wǎng)上找到,例如在??春季安全示例??.
訪問決策在管理應(yīng)用程序中進(jìn)行。對(duì)于“ADMIN”角色(此后端全局需要),我們?cè)赟pring Security中執(zhí)行此操作:
安全配置.java
@Configurationpublic class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Override protected void configure(HttpSecurity http) throws Exception { http ... .authorizeRequests() .antMatchers("/index.html", "/").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ... }}對(duì)于“READER”和“WRITER”角色,應(yīng)用程序本身是分開的,由于應(yīng)用程序是用JavaScript實(shí)現(xiàn)的,這就是我們需要做出訪問決定的地方。一種方法是通過路由器嵌入一個(gè)帶有計(jì)算視圖的主頁:
app.component.html
Admin
當(dāng)組件加載時(shí)計(jì)算路由:
app.component.ts
@Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"]})export class AppComponent { user: {}; constructor(private app: AppService, private http: HttpClient, private router: Router) { app.authenticate(response => { this.user = response; this.message(); }); } logout() { this.http.post("logout", {}).subscribe(function() { this.app.authenticated = false; this.router.navigateByUrl("/login"); }); } message() { if (!this.app.authenticated) { this.router.navigate(["/unauthenticated"]); } else { if (this.app.writer) { this.router.navigate(["/write"]); } else { this.router.navigate(["/read"]); } } }...}應(yīng)用程序要做的第一件事是查看“檢查用戶是否經(jīng)過身份驗(yàn)證”,并通過查看用戶數(shù)據(jù)來計(jì)算路由。路由在主模塊中聲明:
app.module.ts
const routes: Routes = [ { path: "", pathMatch: "full", redirectTo: "read"}, { path: "read", component: ReadComponent}, { path: "write", component: WriteComponent}, { path: "unauthenticated", component: UnauthenticatedComponent}, { path: "changes", component: ChangesComponent}];這些組件中的每一個(gè)(每個(gè)路由一個(gè))都必須單獨(dú)實(shí)現(xiàn)。下面是一個(gè)示例:?
?ReadComponent?
?read.component.ts
import { Component } from "@angular/core";import { HttpClient } from "@angular/common/http";@Component({ templateUrl: "./read.component.html"})export class ReadComponent { greeting = {}; constructor(private http: HttpClient) { http.get("/resource").subscribe(data => this.greeting = data); }}read.component.html
Greeting
The ID is {{greeting.id}}
The content is {{greeting.content}}
這是類似的,但有一個(gè)表單來更改后端中的消息:?
?WriteComponent?
?write.component.ts
import { Component } from "@angular/core";import { HttpClient } from "@angular/common/http";@Component({ templateUrl: "./write.component.html"})export class WriteComponent { greeting = {}; constructor(private http: HttpClient) { this.http.get("/resource").subscribe(data => this.greeting = data); } update() { this.http.post("/resource", {content: this.greeting["content"]}).subscribe(response => { this.greeting = response; }); }}寫組件.html
還需要提供數(shù)據(jù)來計(jì)算路由,因此在函數(shù)中看到以下內(nèi)容:?
?AppService?
???authenticate()?
?app.service.ts
http.get("/user").subscribe(function(response) { var user = response.json(); if (user.name) { self.authenticated = true; self.writer = user.roles && user.roles.indexOf("ROLE_WRITER")>0; } else { self.authenticated = false; self.writer = false; } callback && callback(response); })為了在后端支持這個(gè)函數(shù),我們需要端點(diǎn),例如在我們的主應(yīng)用程序類中:?
?/user?
?管理應(yīng)用程序.java
@SpringBootApplication@RestControllerpublic class AdminApplication { @RequestMapping("/user") public Mapuser(Principal user) { Map map = new LinkedHashMap (); map.put("name", user.getName()); map.put("roles", AuthorityUtils.authorityListToSet(((Authentication) user) .getAuthorities())); return map; } public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); }}
角色名稱來自帶有“ROLE_”前綴的“/user”端點(diǎn),因此我們可以將它們與其他類型的權(quán)限區(qū)分開來(這是Spring Security的事情)。因此,JavaScript 中需要 “ROLE_” 前綴,但在 Spring Security 配置中不需要,從方法名稱中可以清楚地看出“角色”是操作的重點(diǎn)。
網(wǎng)關(guān)中支持管理 UI 的更改
我們還將使用角色在網(wǎng)關(guān)中做出訪問決策(因此我們可以有條件地顯示指向管理 UI 的鏈接),因此我們也應(yīng)該將“角色”添加到網(wǎng)關(guān)中的“/user”端點(diǎn)。一旦到位,我們可以添加一些JavaScript來設(shè)置一個(gè)標(biāo)志,以指示當(dāng)前用戶是“管理員”。在函數(shù)中:?
?authenticated()?
?app.component.ts
this.http.get("user", {headers: headers}).subscribe(data => { this.authenticated = data && data["name"]; this.user = this.authenticated ? data["name"] : ""; this.admin = this.authenticated && data["roles"] && data["roles"].indexOf("ROLE_ADMIN") > -1;});我們還需要將標(biāo)志重置為用戶注銷時(shí):?
?admin?
???false?
?app.component.ts
this.logout = function() { http.post("logout", {}).subscribe(function() { self.authenticated = false; self.admin = false; });}然后在 HTML 中,我們可以有條件地顯示一個(gè)新鏈接:
app.component.html
運(yùn)行所有應(yīng)用并轉(zhuǎn)到??http://localhost:8080??以查看結(jié)果。一切應(yīng)該工作正常,并且 UI 應(yīng)該根據(jù)當(dāng)前經(jīng)過身份驗(yàn)證的用戶而更改。
我們?yōu)槭裁丛谶@里?
現(xiàn)在我們有一個(gè)不錯(cuò)的小系統(tǒng),有 2 個(gè)獨(dú)立的用戶界面和一個(gè)后端資源服務(wù)器,所有這些都受網(wǎng)關(guān)中相同身份驗(yàn)證的保護(hù)。網(wǎng)關(guān)充當(dāng)微代理的事實(shí)使得后端安全問題的實(shí)現(xiàn)非常簡(jiǎn)單,他們可以自由地專注于自己的業(yè)務(wù)問題。春季會(huì)話的使用(再次)避免了大量的麻煩和潛在的錯(cuò)誤。
一個(gè)強(qiáng)大的功能是后端可以獨(dú)立地?fù)碛兴麄兿矚g的任何類型的身份驗(yàn)證(例如,如果您知道它的物理地址和一組本地憑據(jù),您可以直接轉(zhuǎn)到 UI)。網(wǎng)關(guān)施加一組完全不相關(guān)的約束,只要它可以對(duì)用戶進(jìn)行身份驗(yàn)證并為其分配滿足后端訪問規(guī)則的元數(shù)據(jù)。對(duì)于能夠獨(dú)立開發(fā)和測(cè)試后端組件來說,這是一個(gè)很好的設(shè)計(jì)。如果我們?cè)敢猓覀兛梢曰氐酵獠?OAuth2 服務(wù)器(如第五節(jié),甚至完全不同的東西)用于網(wǎng)關(guān)的身份驗(yàn)證,并且不需要觸摸后端。
此體系結(jié)構(gòu)的一個(gè)額外功能(控制身份驗(yàn)證的單個(gè)網(wǎng)關(guān)和跨所有組件的共享會(huì)話令牌)是“單點(diǎn)注銷”,我們發(fā)現(xiàn)該功能很難在第五節(jié),免費(fèi)提供。更準(zhǔn)確地說,在我們完成的系統(tǒng)中自動(dòng)提供一種特殊的單點(diǎn)注銷用戶體驗(yàn)方法:如果用戶注銷任何 UI(網(wǎng)關(guān)、UI 后端或管理員后端),他將從所有其他 UI 中注銷,假設(shè)每個(gè)單獨(dú)的 UI 都以相同的方式實(shí)現(xiàn)“注銷”功能(使會(huì)話無效)。
謝謝:我要再次感謝所有幫助我開發(fā)這個(gè)系列的人,特別是羅伯·溫奇和托爾斯滕·斯佩特感謝他們對(duì)部分和源代碼的仔細(xì)審查。因?yàn)榈谝还?jié)出版后,它沒有太大變化,但所有其他部分都根據(jù)讀者的評(píng)論和見解進(jìn)行了演變,因此也感謝閱讀這些部分并不厭其煩地加入討論的任何人。
測(cè)試 Angular 應(yīng)用程序
在本節(jié)中,我們繼續(xù)我們的討論如何使用彈簧安全跟角在“單頁應(yīng)用程序”中。在這里,我們展示了如何使用 Angular 測(cè)試框架為客戶端代碼編寫和運(yùn)行單元測(cè)試。您可以了解應(yīng)用程序的基本構(gòu)建塊,也可以通過閱讀第一部分,或者您可以直接轉(zhuǎn)到Github中的源代碼(源代碼與第一部分相同,但現(xiàn)在添加了測(cè)試)。本節(jié)實(shí)際上很少有使用 Spring 或 Spring Security 的代碼,但它以一種在通常的 Angular 社區(qū)資源中可能不那么容易找到的方式涵蓋了客戶端測(cè)試,而且我們認(rèn)為對(duì)于大多數(shù) Spring 用戶來說會(huì)很舒服。
提醒:如果您正在使用示例應(yīng)用程序完成本節(jié),請(qǐng)務(wù)必清除瀏覽器緩存中的 Cookie 和 HTTP 基本憑據(jù)。在Chrome中,為單個(gè)服務(wù)器執(zhí)行此操作的最佳方法是打開一個(gè)新的隱身窗口。
編寫規(guī)范
我們?cè)凇盎尽睉?yīng)用程序中的“app”組件非常簡(jiǎn)單,因此徹底測(cè)試它不會(huì)花費(fèi)太多時(shí)間。以下是代碼提醒:
app.component.ts
include::basic/src/app/app.component.ts我們面臨的主要挑戰(zhàn)是在測(cè)試中提供對(duì)象,因此我們可以斷言它們?cè)诮M件中的使用方式。實(shí)際上,即使在我們面臨這一挑戰(zhàn)之前,我們也需要能夠創(chuàng)建一個(gè)組件實(shí)例,以便我們可以測(cè)試加載時(shí)會(huì)發(fā)生什么。這是您可以做到這一點(diǎn)的方法。?
?http?
?從中創(chuàng)建的應(yīng)用程序中的 Angular 構(gòu)建已經(jīng)有一個(gè)規(guī)范和一些配置來運(yùn)行它。生成的規(guī)范位于“src/app”中,它的開頭是這樣的:?
?ng new?
?app.component.ts
import { TestBed, async } from "@angular/core/testing";import { AppComponent } from "./app.component";describe("AppComponent", () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [], declarations: [ AppComponent ] }).compileComponents(); })); it("should create the app", async(() => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); ...}在這個(gè)非常基本的測(cè)試套件中,我們有以下重要元素:
我們正在測(cè)試的東西(在這種情況下是“AppComponent”)與函數(shù)。describe()
在該函數(shù)中,我們提供了一個(gè)回調(diào),用于加載 Angular 組件。beforeEach()
行為是通過調(diào)用來表達(dá)的,我們用語言陳述期望是什么,然后提供一個(gè)做出斷言的函數(shù)。it()
測(cè)試環(huán)境在發(fā)生任何其他事情之前初始化。這是大多數(shù) Angular 應(yīng)用程序的樣板文件。這里的測(cè)試函數(shù)是如此微不足道,它實(shí)際上只斷言組件存在,所以如果失敗,那么測(cè)試就會(huì)失敗。
改進(jìn)單元測(cè)試:模擬 HTTP 后端
為了將規(guī)格提高到生產(chǎn)級(jí),我們需要實(shí)際斷言控制器加載時(shí)會(huì)發(fā)生什么。由于它發(fā)出了調(diào)用,我們需要模擬該調(diào)用,以避免僅為單元測(cè)試運(yùn)行整個(gè)應(yīng)用程序。為此,我們使用 角度 :?
?http.get()?
???HttpClientTestingModule?
?app.component.spec
Unresolved directive in testing.adoc - include::basic/src/app/app.component.spec[indent=0]這里的新作品是:
在 .HttpClientTestingModule
TestBed
beforeEach()
在測(cè)試函數(shù)中,我們?cè)趧?chuàng)建組件之前為后端設(shè)置了期望,告訴它期望調(diào)用“resource/”,以及響應(yīng)應(yīng)該是什么。運(yùn)行規(guī)范
為了運(yùn)行我們的測(cè)試“代碼,我們可以使用設(shè)置項(xiàng)目時(shí)創(chuàng)建的便利腳本來執(zhí)行(或)。它還作為 Maven 生命周期的一部分運(yùn)行,因此也是運(yùn)行測(cè)試的好方法,這就是 CI 構(gòu)建中將發(fā)生的情況。?
?./ng test?
???./ng build?
???./mvnw install?
?端到端測(cè)試
Angular還有一個(gè)標(biāo)準(zhǔn)的構(gòu)建設(shè)置,用于使用瀏覽器和您生成的JavaScript進(jìn)行“端到端測(cè)試”。這些被寫成頂級(jí)目錄中的“規(guī)范”。本教程中的所有示例都包含一個(gè)非常簡(jiǎn)單的端到端測(cè)試,該測(cè)試在 Maven 生命周期中運(yùn)行(因此,如果您在任何“ui”應(yīng)用程序中運(yùn)行,您將看到瀏覽器窗口彈出窗口)。?
?e2e?
???mvn install?
?結(jié)論
能夠?yàn)?Javascript 運(yùn)行單元測(cè)試在現(xiàn)代 Web 應(yīng)用程序中很重要,這是我們?cè)诒鞠盗兄泻雎裕ɑ蚧乇埽┑闹黝}。在本期中,我們介紹了如何編寫測(cè)試的基本要素,如何在開發(fā)時(shí)運(yùn)行它們,更重要的是,在持續(xù)集成環(huán)境中運(yùn)行它們。我們采取的方法并不適合所有人,所以請(qǐng)不要因?yàn)橐圆煌姆绞阶鲞@件事而感到難過,但要確保你擁有所有這些成分。我們?cè)谶@里做的方式可能會(huì)讓傳統(tǒng)的Java企業(yè)開發(fā)人員感到舒適,并且與他們現(xiàn)有的工具和流程很好地集成,所以如果你屬于這一類,我希望你會(huì)發(fā)現(xiàn)它作為一個(gè)起點(diǎn)很有用。更多使用 Angular 和 Jasmine 進(jìn)行測(cè)試的例子可以在互聯(lián)網(wǎng)上的很多地方找到,但第一個(gè)調(diào)用點(diǎn)可能是“單一”樣本來自本系列,現(xiàn)在有一些最新的測(cè)試代碼,與本教程中我們需要為“基本”示例編寫的代碼相比,這些代碼要簡(jiǎn)單一些。
從 OAuth2 客戶端應(yīng)用程序注銷
在本節(jié)中,我們繼續(xù)我們的討論如何使用彈簧安全跟角在“單頁應(yīng)用程序”中。在這里,我們將展示如何獲取 OAuth2 示例并添加不同的注銷體驗(yàn)。許多實(shí)現(xiàn)OAuth2單點(diǎn)登錄的人發(fā)現(xiàn)他們有一個(gè)難題需要解決如何“干凈”地注銷?這是一個(gè)難題的原因是沒有一種正確的方法可以做到這一點(diǎn),您選擇的解決方案將取決于您正在尋找的用戶體驗(yàn)以及您愿意承擔(dān)的復(fù)雜性。復(fù)雜性的原因源于這樣一個(gè)事實(shí),即系統(tǒng)中可能存在多個(gè)瀏覽器會(huì)話,所有會(huì)話都有不同的后端服務(wù)器,因此當(dāng)用戶從其中一個(gè)注銷時(shí),其他會(huì)話會(huì)發(fā)生什么?這是教程的第九部分,您可以通過閱讀第一部分,或者您可以直接轉(zhuǎn)到Github中的源代碼.
注銷模式
在本教程中注銷示例的用戶體驗(yàn)是注銷 UI 應(yīng)用,而不是從身份驗(yàn)證服務(wù)器注銷,因此當(dāng)你重新登錄到 UI 應(yīng)用時(shí),身份驗(yàn)證服務(wù)器不會(huì)再次質(zhì)詢憑據(jù)。當(dāng)身份驗(yàn)證服務(wù)器是外部時(shí),這是完全預(yù)期、正常和可取的 - Google 和其他外部身份驗(yàn)證服務(wù)器提供商既不希望也不允許您從不受信任的應(yīng)用程序從他們的服務(wù)器注銷 - 但如果身份驗(yàn)證服務(wù)器確實(shí)是同一系統(tǒng)的一部分,這不是最佳的用戶體驗(yàn)用戶界面。?
?oauth2?
?從廣義上講,從經(jīng)過身份驗(yàn)證為 OAuth2 客戶端的 UI 應(yīng)用注銷有三種模式:
外部身份驗(yàn)證服務(wù)器(EA,原始示例)。用戶將身份驗(yàn)證服務(wù)器視為第三方(例如,使用Facebook或Google進(jìn)行身份驗(yàn)證)。您不希望在應(yīng)用程序會(huì)話結(jié)束時(shí)注銷身份驗(yàn)證服務(wù)器。您確實(shí)希望批準(zhǔn)所有授權(quán)。本教程中的 (和 ) 示例實(shí)現(xiàn)了此模式。oauth2
oauth2-vanilla
網(wǎng)關(guān)和內(nèi)部身份驗(yàn)證服務(wù)器 (GIA)。您只需要注銷 2 個(gè)應(yīng)用程序,并且它們屬于用戶感知的同一系統(tǒng)的一部分。通常,您希望自動(dòng)批準(zhǔn)所有授權(quán)。單點(diǎn)注銷 (SL)。一個(gè)身份驗(yàn)證服務(wù)器和多個(gè) UI 應(yīng)用程序都具有自己的身份驗(yàn)證,當(dāng)用戶注銷其中一個(gè)時(shí),您希望它們都效仿。由于網(wǎng)絡(luò)分區(qū)和服務(wù)器故障,可能會(huì)因幼稚的實(shí)現(xiàn)而失敗 - 您基本上需要全局一致的存儲(chǔ)。有時(shí),即使您有外部身份驗(yàn)證服務(wù)器,您也希望控制身份驗(yàn)證并添加內(nèi)部訪問控制層(例如,身份驗(yàn)證服務(wù)器不支持的范圍或角色)。然后,最好使用 EA 進(jìn)行身份驗(yàn)證,但有一個(gè)內(nèi)部身份驗(yàn)證服務(wù)器,可以將您需要的其他詳細(xì)信息添加到令牌中。來自這個(gè)其他的樣本?
?auth-server?
?OAuth2 教程向您展示如何以非常簡(jiǎn)單的方式執(zhí)行此操作。然后,您可以將 GIA 或 SL 模式應(yīng)用于包含內(nèi)部身份驗(yàn)證服務(wù)器的系統(tǒng)。如果您不想要 EA,這里有一些選項(xiàng):
從身份驗(yàn)證服務(wù)器以及瀏覽器客戶端中的 UI 應(yīng)用程序注銷。簡(jiǎn)單的方法,并與一些仔細(xì)的CRSF和CORS配置一起工作。沒有SL。令牌可用后立即從身份驗(yàn)證服務(wù)器注銷。很難在獲取令牌的 UI 中實(shí)現(xiàn),因?yàn)槟抢餂]有身份驗(yàn)證服務(wù)器的會(huì)話 cookie。有一個(gè)春季 OAuth 中的功能請(qǐng)求這展示了一種有趣的方法:一旦生成身份驗(yàn)證代碼,就使身份驗(yàn)證服務(wù)器中的會(huì)話無效。Github 問題包含一個(gè)實(shí)現(xiàn)會(huì)話失效的方面,但作為 .沒有SL。HandlerInterceptor
代理身份驗(yàn)證服務(wù)器通過與UI相同的網(wǎng)關(guān),并希望一個(gè)cookie足以管理整個(gè)系統(tǒng)的狀態(tài)。不起作用,因?yàn)槌谴嬖诠蚕頃?huì)話,這會(huì)在一定程度上破壞對(duì)象(否則身份驗(yàn)證服務(wù)器沒有會(huì)話存儲(chǔ))。僅當(dāng)會(huì)話在所有應(yīng)用程序之間共享時(shí),SL。網(wǎng)關(guān)中的 Cookie 中繼。您使用網(wǎng)關(guān)作為身份驗(yàn)證的事實(shí)來源,并且身份驗(yàn)證服務(wù)器具有它所需的所有狀態(tài),因?yàn)榫W(wǎng)關(guān)而不是瀏覽器管理 cookie。瀏覽器永遠(yuǎn)不會(huì)有來自多個(gè)服務(wù)器的 cookie。沒有SL。使用令牌作為全局身份驗(yàn)證,并在用戶注銷 UI 應(yīng)用時(shí)使其失效。缺點(diǎn):需要令牌被客戶端應(yīng)用失效,這不是它們真正設(shè)計(jì)的目的。 SL 可能,但通常的約束適用。在身份驗(yàn)證服務(wù)器中創(chuàng)建和管理全局會(huì)話令牌(除了用戶令牌之外)。這是OpenId Connect,它確實(shí)為 SL 提供了一些選項(xiàng),但代價(jià)是一些額外的機(jī)器。沒有一個(gè)選項(xiàng)可以免受通常的分布式系統(tǒng)限制:如果網(wǎng)絡(luò)和應(yīng)用程序節(jié)點(diǎn)不穩(wěn)定,則無法保證在需要時(shí)在所有參與者之間共享注銷信號(hào)。所有注銷規(guī)范仍處于草稿形式,以下是規(guī)范的一些鏈接:會(huì)話管理,前通道注銷和反向通道注銷.請(qǐng)注意,如果 SL 很難或不可能,最好將所有 UI 放在單個(gè)網(wǎng)關(guān)后面。然后,您可以使用 GIA(更容易)來控制整個(gè)遺產(chǎn)的注銷。
最簡(jiǎn)單的兩個(gè)選項(xiàng)(非常適合 GIA 模式)可以在教程示例中實(shí)現(xiàn),如下所示(獲取示例并從那里開始工作)。?
?oauth2?
?從瀏覽器注銷兩個(gè)服務(wù)器
將幾行代碼添加到瀏覽器客戶端非常容易,一旦 UI 應(yīng)用程序注銷,這些代碼就會(huì)從身份驗(yàn)證服務(wù)器注銷。F.D.
logout() { this.http.post("logout", {}).finally(() => { self.authenticated = false; this.http.post("http://localhost:9999/uaa/logout", {}, {withCredentials:true}) .subscribe(() => { console.log("Logged out"); }); }).subscribe();};在此示例中,我們將身份驗(yàn)證服務(wù)器注銷終結(jié)點(diǎn) URL 硬編碼到 JavaScript 中,但如果需要,很容易將其外部化。它必須是直接發(fā)送到身份驗(yàn)證服務(wù)器的 POST,因?yàn)槲覀兿M麜?huì)話 cookie 也隨之而來。XHR 請(qǐng)求只會(huì)從瀏覽器中發(fā)出,并附加一個(gè) cookie,如果我們特別要求.?
?withCredentials:true?
?相反,在服務(wù)器上,我們需要一些 CORS 配置,因?yàn)檎?qǐng)求來自不同的域。例如,在?
?WebSecurityConfigurerAdapter?
?@Overrideprotected void configure(HttpSecurity http) throws Exception { http .requestMatchers().antMatchers("/login", "/logout", "/oauth/authorize", "/oauth/confirm_access") .and() .cors().configurationSource(configurationSource()) ...}private CorsConfigurationSource configurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.setAllowCredentials(true); config.addAllowedHeader("X-Requested-With"); config.addAllowedHeader("Content-Type"); config.addAllowedMethod(HttpMethod.POST); source.registerCorsConfiguration("/logout", config); return source;}“/logout”端點(diǎn)已得到一些特殊處理。它允許從任何源調(diào)用,并明確允許發(fā)送憑據(jù)(例如 cookie)。允許的標(biāo)頭只是 Angular 在示例應(yīng)用程序中發(fā)送的標(biāo)頭。
除了 CORS 配置之外,我們還需要禁用注銷端點(diǎn)的 CSRF,因?yàn)?Angular 不會(huì)在跨域請(qǐng)求中發(fā)送標(biāo)頭。身份驗(yàn)證服務(wù)器之前不需要任何 CSRF 配置,但很容易為注銷端點(diǎn)添加忽略:?
?X-XSRF-TOKEN?
?@Overrideprotected void configure(HttpSecurity http) throws Exception { http .csrf() .ignoringAntMatchers("/logout/**") ...}
放棄 CSRF 保護(hù)并不是真正可取的,但您可能準(zhǔn)備容忍它用于此受限用例。
通過這兩個(gè)簡(jiǎn)單的更改,一個(gè)在 UI 應(yīng)用程序客戶端中,一個(gè)在身份驗(yàn)證服務(wù)器中,您會(huì)發(fā)現(xiàn)一旦您注銷 UI 應(yīng)用程序,當(dāng)您重新登錄時(shí),將始終提示您輸入密碼。
另一個(gè)有用的更改是將 OAuth2 客戶端設(shè)置為自動(dòng)批準(zhǔn),以便用戶不必批準(zhǔn)令牌授予。這在內(nèi)部身份驗(yàn)證服務(wù)器中很常見,用戶不會(huì)將其視為一個(gè)單獨(dú)的系統(tǒng)。在初始化客戶端時(shí),您只需要一個(gè)標(biāo)志:?
?AuthorizationServerConfigurerAdapter?
?@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("acme") ... .autoApprove(true);}使身份驗(yàn)證服務(wù)器中的會(huì)話無效
如果您不想放棄注銷端點(diǎn)上的 CSRF 保護(hù),可以嘗試另一種簡(jiǎn)單的方法,即在授予令牌后立即使身份驗(yàn)證服務(wù)器中的用戶會(huì)話失效(實(shí)際上是在生成身份驗(yàn)證代碼后立即)。這也非常容易實(shí)現(xiàn):從示例開始,只需向 OAuth2 終結(jié)點(diǎn)添加 a。?
?oauth2?
???HandlerInterceptor?
?@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { ... endpoints.addInterceptor(new HandlerInterceptorAdapter() { @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (modelAndView != null && modelAndView.getView() instanceof RedirectView) { RedirectView redirect = (RedirectView) modelAndView.getView(); String url = redirect.getUrl(); if (url.contains("code=") || url.contains("error=")) { HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } } } } });}此偵聽器查找 ,這是用戶被重定向回客戶端應(yīng)用的信號(hào),并檢查該位置是否包含身份驗(yàn)證代碼或錯(cuò)誤。如果您也使用隱式授權(quán),則可以添加“token=”。?
?RedirectView?
?通過這個(gè)簡(jiǎn)單的更改,一旦進(jìn)行身份驗(yàn)證,身份驗(yàn)證服務(wù)器中的會(huì)話就已經(jīng)失效,因此無需嘗試從客戶端管理它。注銷 UI 應(yīng)用,然后重新登錄時(shí),身份驗(yàn)證服務(wù)器無法識(shí)別你并提示輸入憑據(jù)。此模式是由示例在?
?oauth2-logout?
?源代碼對(duì)于本教程。這種方法的缺點(diǎn)是你不再真正擁有真正的單點(diǎn)登錄 - 作為系統(tǒng)一部分的任何其他應(yīng)用程序都會(huì)發(fā)現(xiàn) authserver 會(huì)話已死,他們必須再次提示進(jìn)行身份驗(yàn)證 - 如果有多個(gè)應(yīng)用程序,這不是一個(gè)很好的用戶體驗(yàn)。結(jié)論
在本節(jié)中,我們已經(jīng)了解了如何實(shí)現(xiàn)幾種不同的模式,以便從 OAuth2 客戶端應(yīng)用程序注銷(以應(yīng)用程序從第五節(jié)),并討論了其他模式的一些選項(xiàng)。這些選項(xiàng)并不詳盡,但應(yīng)該可以讓您很好地了解所涉及的權(quán)衡,以及一些用于考慮用例最佳解決方案的工具。本節(jié)中只有幾行 JavaScript,而且并不是真正特定于 Angular(它為 XHR 請(qǐng)求添加了一個(gè)標(biāo)志),因此所有課程和模式都適用于本指南中示例應(yīng)用程序的狹窄范圍之外。一個(gè)反復(fù)出現(xiàn)的主題是,所有具有多個(gè) UI 應(yīng)用程序和單個(gè)身份驗(yàn)證服務(wù)器的單點(diǎn)注銷 (SL) 方法都在某些方面存在缺陷:您能做的最好的事情就是選擇讓用戶最不舒服的方法。如果你有一個(gè)內(nèi)部身份驗(yàn)證服務(wù)器和一個(gè)由許多組件組成的系統(tǒng),那么可能唯一讓用戶感覺像單個(gè)系統(tǒng)的體系結(jié)構(gòu)是所有用戶交互的網(wǎng)關(guān)。
標(biāo)簽: 身份驗(yàn)證 應(yīng)用程序 我們需要