API Gateway 模式:Angular JS 和 Spring Security 第四部分

工程 | Dave Syer | 2015年1月28日 | ...

注意:本文的原始碼和測試將繼續演進,但文字的更改在此處不再維護。請參閱教程版本以獲取最新內容。

在本文中,我們將繼續討論如何在“單頁應用程式”中使用Spring SecurityAngular JS。在這裡,我們將展示如何構建一個 API Gateway,使用Spring Cloud來控制後端資源的認證和訪問。這是系列文章中的第四篇,您可以閱讀第一篇文章來了解應用程式的基本構建塊或從頭開始構建,或者直接轉到 Github 上的原始碼。在上一篇文章中,我們構建了一個使用Spring Session對後端資源進行身份驗證的簡單分散式應用程式。在本文中,我們將 UI 伺服器變成後端資源伺服器的反向代理,解決了上一個實現的問(由自定義令牌身份驗證引入的技術複雜性),併為控制瀏覽器客戶端的訪問提供了許多新選項。

提醒:如果您正在使用示例應用程式完成本文,請務必清除瀏覽器快取中的 cookie 和 HTTP Basic 憑據。在 Chrome 中,針對單個伺服器執行此操作的最佳方法是開啟一個新的隱身視窗。

建立 API Gateway

API Gateway 是前端客戶端的單一入口點(和控制點),客戶端可以是基於瀏覽器的(如本文中的示例)或移動端的。客戶端只需要知道一個伺服器的 URL,後端就可以隨意重構而無需更改,這是一個顯著的優勢。在集中化和控制方面還有其他優勢:速率限制、身份驗證、審計和日誌記錄。透過Spring Cloud實現一個簡單的反向代理非常容易。

如果您正在跟著程式碼進行操作,您會知道上一篇文章結尾處的應用程式實現有點複雜,因此不是一個容易迭代的起點。但是,有一箇中間點可以更輕鬆地開始,即後端資源尚未透過 Spring Security 進行保護。此原始碼是 Github 上的一個獨立專案,因此我們將從那裡開始。它有一個 UI 伺服器和一個資源伺服器,它們在相互通訊。資源伺服器還沒有 Spring Security,所以我們可以先讓系統工作起來,然後再新增這一層。

一行宣告式反向代理

要將其變成 API Gateway,UI 伺服器需要一個小小的調整。在 Spring 配置中的某個地方,我們需要新增一個 @EnableZuulProxy 註解,例如在主(唯一)應用程式類中。

@SpringBootApplication
@RestController
@EnableZuulProxy
public class UiApplication {
  ...
}

並在外部配置檔案中,我們需要將 UI 伺服器上的本地資源對映到外部配置(“application.yml”)中的遠端資源。

security:
  ...
zuul:
  routes:
    resource:
      path: /resource/**
      url: https://:9000

這表示“將此伺服器上的 /resource/** 模式的路徑對映到 localhost:9000 的遠端伺服器上的相同路徑”。簡單而有效(好吧,算上 YAML 共有 6 行,但你並不總是需要它)!

要使此功能正常工作,我們只需要在類路徑上新增正確的內容。為此,我們在 Maven POM 中添加了幾行新內容。

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-parent</artifactId>
      <version>1.0.0.BUILD-SNAPSHOT</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
  </dependency>
  ...
</dependencies>

請注意使用了“spring-cloud-starter-zuul”——它是一個 starter POM,就像 Spring Boot 的 POM 一樣,但它管理了我們對這個 Zuul 代理所需的依賴項。我們還使用了 <dependencyManagement>,因為我們希望能夠依賴於所有傳遞依賴項的正確版本。

在客戶端中使用代理

在進行這些更改後,我們的應用程式仍然可以正常工作,但直到我們修改客戶端,我們實際上還沒有使用新的代理。幸運的是,這非常簡單。我們只需要將“home”控制器從這個實現更改為本地資源:

angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
	$http.get('https://:9000/').success(function(data) {
		$scope.greeting = data;
	})
});

更改為本地資源:

angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
	$http.get('resource/').success(function(data) {
		$scope.greeting = data;
	})
});

現在,當我們啟動伺服器時,一切正常,請求透過 UI(API Gateway)代理到資源伺服器。

進一步簡化

更好的是:我們不再需要在資源伺服器中使用 CORS 過濾器了。我們本來也只是匆忙地設定了它,並且我們必須手動進行任何技術性操作(尤其是在涉及安全性方面)都應該是一個危險訊號。幸運的是,它現在是多餘的,所以我們可以把它丟掉,安心地睡個好覺了!

保護資源伺服器

您可能還記得,在我們開始的中間狀態中,資源伺服器沒有采取任何安全措施。

題外話:如果您的網路架構與應用程式架構相匹配,軟體安全缺失可能不是問題(您可以只讓資源伺服器對除 UI 伺服器之外的任何人物理不可訪問)。作為簡單的演示,我們可以讓資源伺服器僅在 localhost 上可訪問。只需將此新增到資源伺服器的 application.properties 檔案中:

    server.address: 127.0.0.1

哇,這很簡單!在資料中心內可見的網路地址上執行此操作,您就擁有了一個適用於所有資源伺服器和所有使用者桌面的安全解決方案。

假設我們決定確實需要在軟體層面進行安全(出於多種原因,這很可能)。這不會有問題,因為我們只需要在資源伺服器中新增 Spring Security 作為依賴項(在資源伺服器 POM中):

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

這足以讓我們擁有一個安全的資源伺服器,但它還不能讓我們的應用程式正常工作,原因與第三部分中的情況相同:兩個伺服器之間沒有共享的身份驗證狀態。

共享身份驗證狀態

我們可以使用與上次相同的機制來共享身份驗證(和 CSRF)狀態,即Spring Session。我們像以前一樣將依賴項新增到兩個伺服器中:

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session</artifactId>
  <version>1.0.0.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

但這次配置要簡單得多,因為我們只需將相同的 Filter 宣告新增到兩者即可。首先是 UI 伺服器(新增 @EnableRedisHttpSession):

@SpringBootApplication
@RestController
@EnableZuulProxy
@EnableRedisHttpSession
public class UiApplication {

  ...

}

然後是資源伺服器。需要進行三項小的更改:一項是在 ResourceApplication 中新增 @EnableRedisHttpSession

@SpringBootApplication
@RestController
@EnableRedisHttpSession
class ResourceApplication {
  ...
}

另一項是明確停用資源伺服器中的 HTTP Basic(以防止瀏覽器彈出身份驗證對話方塊):

@SpringBootApplication
@RestController
@EnableRedisHttpSession
class ResourceApplication extends WebSecurityConfigurerAdapter {

  ...

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic().disable()
    http.authorizeRequests().anyRequest().authenticated()
  }

}

題外話:另一種選擇,也可以防止身份驗證對話方塊,就是在 HttpSecurity 配置回撥中保持 HTTP Basic,但將 401 挑戰更改為“Basic”以外的內容。您可以透過實現一個單行 AuthenticationEntryPoint 來實現這一點。

最後一項是在 application.properties 中明確設定非有狀態的會話建立策略:

security.sessions: NEVER

只要 Redis 仍在後臺執行(如果您願意,可以使用 fig.yml 來啟動它),系統就可以工作。在 https://:8080 載入 UI 的主頁,登入後您將在主頁上看到來自後端的訊息。

它是如何工作的?

現在幕後發生了什麼?首先,我們可以檢視 UI 伺服器(和 API Gateway)中的 HTTP 請求:

動詞 路徑 狀態 響應
GET / 200 index.html
GET /css/angular-bootstrap.css 200 Twitter Bootstrap CSS
GET /js/angular-bootstrap.js 200 Bootstrap 和 Angular JS
GET /js/hello.js 200 應用程式邏輯
GET /使用者 302 重定向到登入頁面
GET /login 200 Whitelabel 登入頁(已忽略)
GET /resource 302 重定向到登入頁面
GET /login 200 Whitelabel 登入頁(已忽略)
GET /login.html 200 Angular 登入表單部分
POST /login 302 重定向到主頁(已忽略)
GET /使用者 200 JSON 認證使用者
GET /resource 200 (代理)JSON 問候語

除了 cookie 名稱略有不同(“SESSION”而不是“JSESSIONID”),因為我們正在使用 Spring Session 之外,這與第二部分結尾處的序列完全相同。但架構不同,最後一個對“/resource”的請求很特別,因為它被代理到了資源伺服器。

我們可以透過檢視 UI 伺服器上的“/trace”端點(來自 Spring Boot Actuator,我們透過 Spring Cloud 依賴項新增的)來檢視反向代理的實際執行情況。在新瀏覽器中訪問 https://:8080/trace 並滾動到末尾(如果您還沒有,請為您的瀏覽器獲取一個 JSON 外掛,使其顯示更易讀)。您需要使用 HTTP Basic 進行身份驗證(瀏覽器彈出視窗),但相同的憑據也適用於您的登入表單。在末尾或接近末尾時,您應該會看到一對請求,如下所示:

注意:請嘗試使用不同的瀏覽器,這樣可以避免身份驗證交叉汙染(例如,如果您在測試 UI 時使用了 Chrome,請使用 Firefox)——這不會阻止應用程式正常工作,但如果跟蹤資訊包含同一瀏覽器中不同身份驗證的混合,則會更難閱讀。

{
  "timestamp": 1420558194546,
  "info": {
    "method": "GET",
    "path": "/",
    "query": ""
    "remote": true,
    "proxy": "resource",
    "headers": {
      "request": {
        "accept": "application/json, text/plain, */*",
        "x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
        "cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260",
        "x-forwarded-prefix": "/resource",
        "x-forwarded-host": "localhost:8080"
      },
      "response": {
        "Content-Type": "application/json;charset=UTF-8",
        "status": "200"
      }
    },
  }
},
{
  "timestamp": 1420558200232,
  "info": {
    "method": "GET",
    "path": "/resource/",
    "headers": {
      "request": {
        "host": "localhost:8080",
        "accept": "application/json, text/plain, */*",
        "x-xsrf-token": "542c7005-309c-4f50-8a1d-d6c74afe8260",
        "cookie": "SESSION=c18846b5-f805-4679-9820-cd13bd83be67; XSRF-TOKEN=542c7005-309c-4f50-8a1d-d6c74afe8260"
      },
      "response": {
        "Content-Type": "application/json;charset=UTF-8",
        "status": "200"
      }
    }
  }
},

第二項是客戶端到閘道器的請求,路徑為“/resource”,您可以看到 cookie(由瀏覽器新增)和 CSRF 標頭(由 Angular 新增,如第二部分中所述)。第一項具有 remote: true,這意味著它正在跟蹤對資源伺服器的呼叫。您可以看到它傳送到了 URI 路徑“/”,並且可以看到(至關重要的是)cookie 和 CSRF 標頭也被髮送了。如果沒有 Spring Session,這些標頭對資源伺服器來說將毫無意義,但按照我們的設定,它現在可以使用這些標頭來重新構建具有身份驗證和 CSRF 令牌資料的會話。因此,請求被允許,我們成功了!

結論

我們在本文中涵蓋了很多內容,但我們達到了一個非常理想的狀態:我們的兩個伺服器上的樣板程式碼最少,它們都得到了很好的保護,並且使用者體驗也沒有受到影響。這本身就是使用 API Gateway 模式的一個理由,但實際上我們才剛剛觸及它可能用途的表面(Netflix 使用它來做很多事情)。閱讀Spring Cloud以瞭解更多關於如何輕鬆地為閘道器新增更多功能的資訊。本系列的下一篇文章將透過將身份驗證職責提取到一個單獨的伺服器(單點登入模式)來擴充套件應用程式架構。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有