API 閘道模式:Angular JS 與 Spring Security 第四部分

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

注意:此部落格的原始碼和測試持續演進,但本文的變更未在此處維護。如需最新的內容,請參閱教學課程版本

在本文中,我們繼續探討如何在「單頁應用程式」中使用 Spring SecurityAngular JS。 這裡我們將展示如何使用 Spring Cloud 建立 API 閘道,以控制對後端資源的驗證和存取。 這是系列文章的第四篇,您可以閱讀第一篇文章來了解應用程式的基本建構區塊或從頭開始構建它,或者您可以直接前往Github 中的原始碼。 在上一篇文章中,我們構建了一個簡單的分散式應用程式,該應用程式使用 Spring Session 來驗證後端資源。 在這篇文章中,我們將 UI 伺服器變成後端資源伺服器的反向代理,解決了上次實作的問題(自訂權杖驗證引入的技術複雜性),並為我們提供了許多新的選項來控制來自瀏覽器用戶端的存取。

提醒:如果您正在使用範例應用程式閱讀本文,請務必清除瀏覽器快取中的 Cookie 和 HTTP Basic 認證。 在 Chrome 中,針對單一伺服器執行此操作的最佳方法是開啟一個新的無痕視窗。

建立 API 閘道

API 閘道是前端用戶端的單一入口點(和控制點),前端用戶端可以是基於瀏覽器的(如本文中的範例)或行動裝置。 用戶端只需要知道一個伺服器的 URL,後端可以隨意重構而無需更改,這是一個顯著的優勢。 在集中化和控制方面還有其他優勢:速率限制、身份驗證、稽核和記錄。 使用 Spring Cloud 實現簡單的反向代理非常簡單。

如果您一直在關注程式碼,您會知道在上一篇文章末尾的應用程式實作有點複雜,所以這不是一個很好的迭代起點。 然而,有一個中間點我們可以更容易地開始,即後端資源尚未受到 Spring Security 的保護。 此原始碼是一個單獨的專案在 Github 中,因此我們將從那裡開始。 它有一個 UI 伺服器和一個資源伺服器,並且它們正在互相通訊。 資源伺服器還沒有 Spring Security,因此我們可以先讓系統運作起來,然後再新增該層。

一行程式碼即可宣告反向代理

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

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

並且在外部配置文件中,我們需要將 UI 伺服器中的本機資源映射到 外部配置("application.yml")中的遠端資源

security:
  ...
zuul:
  routes:
    resource:
      path: /resource/**
      url: http://localhost: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 的 starter POM 一樣,但它控制了我們需要的 Zuul 代理的依賴項。 我們也使用 <dependencyManagement>,因為我們希望能夠依賴所有傳遞依賴項的正確版本。

在用戶端中使用代理

有了這些變更,我們的應用程式仍然可以運作,但除非我們修改用戶端,否則我們實際上還沒有使用新的代理。 幸運的是,這很簡單。 我們只需要從 "home" 控制器的這個實作開始

angular.module('hello', [ 'ngRoute' ])
...
.controller('home', function($scope, $http) {
	$http.get('http://localhost: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 閘道)代理到資源伺服器。

進一步簡化

更好的是:我們不再需要在資源伺服器中使用 CORS 篩選器。 無論如何,我們很快就把那個東西拼湊在一起了,而且我們必須手動完成任何技術重點的事情(尤其是在涉及安全性的地方)應該是一個危險信號。 幸運的是,它現在是多餘的,所以我們可以把它扔掉,然後回去睡個好覺!

保護資源伺服器

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

旁白:如果您的網路架構與應用程式架構相符(您可以讓資源伺服器在物理上無法讓除了 UI 伺服器之外的任何人存取),那麼缺少軟體安全性甚至可能不是問題。 作為一個簡單的示範,我們可以讓資源伺服器僅在本機主機上存取。 只需將此新增到資源伺服器中的 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 {

  ...

}

然後是資源伺服器。 有三個小的變更:一個是將 @EnableRedisHttpSession 新增到 ResourceApplication

@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()
  }

}

補充說明:另一種可以避免身分驗證對話框的方法是保留 HTTP Basic 驗證,但將 401 挑戰改為 "Basic" 以外的其他字串。您可以使用 HttpSecurity 設定回呼中的單行程式碼實作 AuthenticationEntryPoint 來做到這一點。

最後一個方法是在 application.properties 中明確要求非無狀態的 session 建立策略。

security.sessions: NEVER

只要 redis 仍在背景執行 (如果想啟動它,可以使用 fig.yml),系統就會正常運作。在 http://localhost: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 /user 302 重新導向至登入頁面
GET /login 200 Whitelabel 登入頁面 (已忽略)
GET /resource 302 重新導向至登入頁面
GET /login 200 Whitelabel 登入頁面 (已忽略)
GET /login.html 200 Angular 登入表單片段
POST /login 302 重新導向至首頁 (已忽略)
GET /user 200 JSON 驗證的使用者
GET /resource 200 (Proxy) JSON 歡迎訊息

除了 Cookie 名稱略有不同 ("SESSION" 而不是 "JSESSIONID",因為我們使用的是 Spring Session) 之外,這與系列文章第二部分結尾的順序相同。但架構是不同的,而且對 "/resource" 的最後一個請求是特殊的,因為它被代理到資源伺服器。

我們可以透過查看 UI 伺服器中的 "/trace" 端點 (來自 Spring Boot Actuator,我們透過 Spring Cloud 相依性添加的) 來看到反向 Proxy 的作用。在新的瀏覽器中前往 http://localhost:8080/trace 並滾動到結尾 (如果您還沒有,請為您的瀏覽器取得 JSON 外掛程式,使其更美觀且易於閱讀)。您需要使用 HTTP Basic (瀏覽器彈出視窗) 進行身分驗證,但與登入表單相同的憑證有效。在結尾處或附近,您應該會看到一對類似以下的請求

注意:嘗試使用不同的瀏覽器,以免發生驗證跨越 (例如,如果您使用 Chrome 測試 UI,則使用 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"
      }
    }
  }
},

第二個項目是客戶端對 Gateway 上 "/resource" 的請求,您可以看到 Cookie (由瀏覽器添加) 和 CSRF 標頭 (由 Angular 添加,如第二部分中所討論的)。第一個項目具有 remote: true,這意味著它正在追蹤對資源伺服器的呼叫。您可以看到它已傳送到 URI 路徑 "/",並且您可以看到 (至關重要的是) Cookie 和 CSRF 標頭也已傳送。如果沒有 Spring Session,這些標頭對於資源伺服器而言將毫無意義,但按照我們設定的方式,它現在可以使用這些標頭來重新建立具有驗證和 CSRF 權杖資料的 Session。因此,請求被允許,我們就成功了!

結論

我們在本文中涵蓋了很多內容,但我們達到了一個非常好的狀態,即我們的兩個伺服器中只有最少的樣板程式碼,它們都非常安全,並且使用者體驗沒有受到影響。僅憑這一點就足以成為使用 API Gateway 模式的理由,但實際上我們只是觸及了它可能用於什麼的表面 (Netflix 將其用於很多事情)。閱讀Spring Cloud 以了解更多關於如何輕鬆地向 Gateway 添加更多功能。本系列的下一篇文章將透過將身分驗證職責提取到單獨的伺服器 (單一登入模式) 來稍微擴展應用程式架構。

取得 Spring 電子報

與 Spring 電子報保持聯繫

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部