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: 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 的一樣,但它管理著我們這個 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 閘道器)代理到資源伺服器。

進一步簡化

更好的是:我們不再需要在資源伺服器中使用 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()
  }

}

旁註:另一種也能阻止認證對話方塊的方法是保留 HTTP Basic,但將 401 挑戰改為“Basic”之外的其他內容。您可以透過在 HttpSecurity 配置回撥中實現一個單行 AuthenticationEntryPoint 來做到這一點。

最後一點是顯式要求在 application.properties 中使用非無狀態的會話建立策略

security.sessions: NEVER

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

它是如何工作的?

現在幕後發生了什麼?首先我們可以看看 UI 伺服器(和 API 閘道器)中的 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 白標登入頁(忽略)
GET /resource 302 重定向到登入頁面
GET /login 200 白標登入頁(忽略)
GET /login.html 200 Angular 登入表單部分
POST /login 302 重定向到主頁(忽略)
GET /user 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 進行認證(瀏覽器彈出視窗),但憑據與您的登入表單相同。在末尾或靠近末尾處,您應該看到一對類似這樣的請求

注意:嘗試使用不同的瀏覽器,這樣就不會出現認證交叉(例如,如果您使用 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"
      }
    }
  }
},

第二個條目是客戶端向閘道器發出的對“/resource”的請求,您可以看到 cookie(由瀏覽器新增)和 CSRF 頭部(由 Angular 新增,如第二部分所述)。第一個條目有 remote: true,這意味著它正在跟蹤對資源伺服器的呼叫。您可以看到它傳送到了 uri 路徑“/”,並且您可以看到(至關重要的是)cookie 和 CSRF 頭部也已傳送。如果沒有 Spring Session,這些頭部對於資源伺服器來說將毫無意義,但透過我們的設定方式,它現在可以使用這些頭部來重建包含認證和 CSRF 令牌資料的會話。因此,請求被允許,我們就可以正常工作了!

結論

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

獲取 Spring 新聞郵件

訂閱 Spring 新聞郵件,保持聯絡

訂閱

先人一步

VMware 提供培訓和認證,助力您加速前行。

瞭解更多

獲取支援

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

瞭解更多

即將到來的活動

檢視 Spring 社群中所有即將到來的活動。

檢視全部