搶先一步
VMware 提供培訓和認證,以加速您的進度。
了解更多注意:此部落格的原始碼和測試持續演進,但文字的變更未在此處維護。請參閱教學版本以取得最新的內容。
在本文中,我們展示了 Spring Security、Spring Boot 和 Angular JS 協同工作的一些優良特性,以提供令人愉悅且安全的用戶體驗。它對於 Spring 和 Angular JS 的初學者來說應該很容易理解,但其中也有許多細節對於這兩方面的專家來說很有用。這實際上是關於 Spring Security 和 Angular JS 的一系列文章中的第一篇,每一篇都會逐步展示新的功能。我們將在第二篇及後續文章中改進應用程式,但在此之後的主要變更將是架構上的,而不是功能上的。
HTML5、豐富的基於瀏覽器的功能以及「單頁應用程式」對於現代開發人員來說是非常有價值的工具,但任何有意義的互動都將涉及後端伺服器,因此除了靜態內容(HTML、CSS 和 JavaScript)之外,我們還需要一個後端伺服器。後端伺服器可以扮演多種角色中的任何一種或全部:提供靜態內容、有時(但現在不那麼頻繁)渲染動態 HTML、驗證用戶身份、保護對受保護資源的訪問,以及(最後但並非最不重要)通過 HTTP 和 JSON 與瀏覽器中的 JavaScript 互動(有時稱為 REST API)。
Spring 一直是用於構建後端功能的流行技術(尤其是在企業中),並且隨著 Spring Boot 的出現,事情變得前所未有的容易。讓我們看看如何使用 Spring Boot、Angular JS 和 Twitter Bootstrap 從頭開始構建一個新的單頁應用程式。沒有特別的理由選擇這個特定的堆疊,但它非常受歡迎,尤其是在企業 Java 商店中的 Spring 核心選民中,所以這是一個值得開始的起點。
我們將逐步詳細介紹如何建立此應用程式,以便任何不完全熟悉 Spring 和 Angular 的人都可以了解正在發生的事情。如果您更喜歡直接跳到最後,您可以跳到最後,看看應用程式是如何運作的,並了解它們是如何組合在一起的。建立新專案有多種選擇
我們將要構建的完整專案的原始碼位於 Github 此處,因此您可以直接克隆該專案並從那裡開始工作(如果需要的話)。然後跳到下一節。
開始建立新專案的最簡單方法是通過 Spring Boot Initializr。例如,在類似 UN*X 的系統上使用 curl
$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d style=web \
-d style=security -d name=ui | tar -xzvf -
然後您可以將該專案(預設情況下,這是一個普通的 Maven Java 專案)導入到您喜歡的 IDE 中,或者僅使用檔案並在命令行上使用 "mvn"。然後跳到下一節。
您可以使用 Spring Boot CLI 建立相同的專案,如下所示
$ spring init --dependencies web,security ui/ && cd ui
然後跳到下一節。
如果您願意,您也可以直接從 Spring Boot Initializr 取得相同的程式碼,以 .zip 檔案的形式提供。只需在瀏覽器中打開它,然後選擇依賴項「Web」和「Security」,然後單擊「Generate Project」。 .zip 檔案包含根目錄中的標準 Maven 或 Gradle 專案,因此您可能需要在解壓縮之前建立一個空目錄。然後跳到下一節。
在 Spring Tool Suite(一組 Eclipse 插件)中,您也可以使用 File->New->Spring Starter Project
上的嚮導建立和匯入專案。然後跳到下一節。
單頁應用程式的核心是一個靜態的 "index.html",所以讓我們開始建立一個(在 "src/main/resources/static" 或 "src/main/resources/public" 中)
<!doctype html>
<html>
<head>
<title>Hello AngularJS</title>
<link href="css/angular-bootstrap.css" rel="stylesheet">
<style type="text/css">
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
}
</style>
</head>
<body ng-app="hello">
<div class="container">
<h1>Greeting</h1>
<div ng-controller="home" ng-cloak class="ng-cloak">
<p>The ID is {{greeting.id}}</p>
<p>The content is {{greeting.content}}</p>
</div>
</div>
<script src="js/angular-bootstrap.js" type="text/javascript"></script>
<script src="js/hello.js"></script>
</body>
</html>
它非常簡短,因為它只是要說 "Hello World"。
顯著的功能包括
在 <head>
中匯入了一些 CSS,一個尚不存在的檔案的佔位符,但名稱帶有暗示性 ("angular-bootstrap.css") 和一個定義 "ng-cloak" 類別的內嵌樣式表。
"ng-cloak" 類別應用於內容 <div>
,以便在 Angular JS 有機會處理動態內容之前將其隱藏起來(這可以防止在初始頁面載入期間出現「閃爍」)。
<body>
被標記為 ng-app="hello"
,這意味著我們需要定義一個 JavaScript 模組,Angular 將識別為一個名為 "hello" 的應用程式。
除了 "ng-cloak" 之外的所有 CSS 類別都來自 Twitter Bootstrap。一旦我們設定了正確的樣式表,它們就會讓東西看起來很漂亮。
greeting 中的內容使用 handlebars 進行標記,例如 {{greeting.content}}
,稍後將由 Angular 填寫(根據周圍 <div>
上的 ng-controller
指令,使用名為 "home" 的「控制器」)。
Angular JS(和 Twitter Bootstrap)包含在 <body>
的底部,以便瀏覽器可以在處理所有 HTML 之前處理它。
我們還包含一個單獨的 "hello.js",我們將在其中定義應用程式行為。
我們稍後會建立 script 和 stylesheet 資源,但現在我們可以忽略它們尚不存在的事實。
一旦新增了首頁檔案,您的應用程式就可以在瀏覽器中載入(即使它還沒什麼功能)。您可以在命令列中執行以下操作:
$ mvn spring-boot:run
然後在瀏覽器中前往 https://127.0.0.1:8080。當您載入首頁時,應該會看到一個瀏覽器對話方塊,要求輸入使用者名稱和密碼(使用者名稱是 "user",密碼會在啟動時印在主控台日誌中)。實際上,目前還沒有任何內容,因此成功驗證後,您應該會看到一個空白頁面,上面有一個 "Greeting" 標頭。
提示:如果您不想從主控台日誌中尋找密碼,只需將以下內容新增到 "application.properties"(在 "src/main/resources" 中):
security.user.password=password
(並選擇您自己的密碼)。我們在範例程式碼中使用 "application.yml" 來做到這一點。
在 IDE 中,只需在應用程式類別中執行 main()
方法(如果您使用上面的 "curl" 命令,則只有一個類別,名稱為 UiApplication
)。
若要將其封裝並作為獨立 JAR 執行,您可以執行以下操作:
$ mvn package
$ java -jar target/*.jar
關於 Angular 和其他前端技術的入門教學課程,通常會直接從網際網路包含程式庫資源(例如,Angular JS 網站本身建議從 Google CDN 下載)。我們不會這樣做,而是會透過串連這些程式庫中的幾個檔案來產生 "angular-bootstrap.js" 資源。這對於讓應用程式運作並非絕對必要,但對於生產應用程式來說,這是避免瀏覽器和伺服器(或內容傳遞網路)之間過多請求的最佳實務。由於我們沒有修改或自訂 CSS 樣式表,因此也沒有必要產生 "angular-bootstrap.css",而且我們也可以直接使用來自 Google CDN 的靜態資源。但是,在實際的應用程式中,我們幾乎肯定會想要修改樣式表,而且我們不會想手動編輯 CSS 來源,因此我們會使用更高等級的工具(例如 Less 或 Sass),所以我們也會使用一個。
有很多不同的方法可以做到這一點,但為了本文的目的,我們將使用 wro4j,這是一個基於 Java 的工具鏈,用於預處理和封裝前端資源。它可以作為任何 Servlet 應用程式中的 JIT (Just in Time) Filter
使用,但它也對 Maven 和 Eclipse 等建置工具有良好的支援,這就是我們將使用它的方式。因此,我們將建置靜態資源檔案並將它們捆綁到我們的應用程式 JAR 中。
題外話:對於硬派前端開發人員來說,Wro4j 可能不是首選工具 - 他們可能會使用基於節點的工具鏈,使用 bower 和/或 grunt。這些絕對是很棒的工具,並且在網路上有非常詳細的介紹,所以如果您喜歡,請隨時使用它們。如果您只是將這些工具鏈的輸出放入 "src/main/resources/static" 中,那麼一切都會正常運作。我發現 wro4j 很舒服,因為我不是硬派前端開發人員,而且我知道如何使用基於 Java 的工具。
若要在建置時建立靜態資源,我們會在 Maven pom.xml
中新增一些魔法(這非常冗長,但都是樣板程式碼,因此可以將其提取到 Maven 中的父 pom,或 Gradle 的共用任務或外掛程式)
<build>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
<resource>
<directory>${project.build.directory}/generated-resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<!-- Serves *only* to filter the wro.xml so it can get an absolute
path for the project -->
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/wro</outputDirectory>
<resources>
<resource>
<directory>src/main/wro</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-maven-plugin</artifactId>
<version>1.7.6</version>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<wroManagerFactory>ro.isdc.wro.maven.plugin.manager.factory.ConfigurableWroManagerFactory</wroManagerFactory>
<cssDestinationFolder>${project.build.directory}/generated-resources/static/css</cssDestinationFolder>
<jsDestinationFolder>${project.build.directory}/generated-resources/static/js</jsDestinationFolder>
<wroFile>${project.build.directory}/wro/wro.xml</wroFile>
<extraConfigFile>${basedir}/src/main/wro/wro.properties</extraConfigFile>
<contextFolder>${basedir}/src/main/wro</contextFolder>
</configuration>
<dependencies>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>angularjs</artifactId>
<version>1.3.8</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
您可以逐字複製到您的 POM 中,或者如果您是從 Github 中的來源 一路跟隨,則可以掃描它。重點是
我們包含一些 webjars 程式庫作為依賴項(jquery 和 bootstrap 用於 CSS 和樣式,以及 Angular JS 用於商業邏輯)。這些 jar 檔案中的一些靜態資源將包含在我們產生的 "angular-bootstrap.*" 檔案中,但這些 jar 本身不需要與應用程式一起封裝。
Twitter Bootstrap 具有對 jQuery 的依賴性,因此我們也包含了它。如果 Angular JS 應用程式沒有使用 Bootstrap,則不需要它,因為 Angular 具有自己的 jQuery 功能版本。
產生的資源將進入 "target/generated-resources" 中,並且由於它已在 <resources/>
區段中宣告,因此它們將封裝在專案的輸出 JAR 中,並且可以在 IDE 的類別路徑中使用(只要我們使用 Maven 工具,例如 Eclipse 中的 m2e)。
wro4j-maven-plugin 具有一些 Eclipse 整合功能,您可以從 Eclipse Marketplace 安裝它(如果這是您第一次,請稍後再試 - 完成應用程式不需要它)。如果您這樣做,Eclipse 將會監看來源檔案,並在它們變更時重新產生輸出。如果您在偵錯模式下執行,則變更可以立即在瀏覽器中重新載入。
Wro4j 是從一個 XML 設定檔控制的,該設定檔不知道您的建置類別路徑,並且只了解絕對檔案路徑,因此我們必須建立一個絕對檔案位置並將其插入 wro.xml
中。為此,我們使用 Maven 資源篩選,這就是為什麼有一個明確的 "maven-resources-plugin" 宣告。
這就是我們需要對 POM 進行的所有變更。剩下的就是新增 wro4j 建置檔案,我們已指定它們將位於 "src/main/wro" 中。
如果您查看 Github 中的原始程式碼,您會看到只有 3 個檔案(其中一個是空的,準備好稍後自訂)
wro.properties
是 wro4j 中預處理和呈現引擎的設定檔。您可以使用它來開啟和關閉工具鏈的各個部分。在本例中,我們使用它從 Less 編譯 CSS,並縮小 JavaScript,最終將我們需要的程式庫中的所有來源合併到兩個檔案中。
preProcessors=lessCssImport
postProcessors=less4j,jsMin
wro.xml
宣告一個名為 "angular-bootstrap" 的單一 "group" 資源,這最終會成為產生的靜態資源的基準名稱。它包含對我們新增的 webjars 中的 <css>
和 <js>
元素的參考,以及對本機來源檔案 main.less
的參考。
<groups xmlns="http://www.isdc.ro/wro">
<group name="angular-bootstrap">
<css>webjar:bootstrap/3.2.0/less/bootstrap.less</css>
<css>file:${project.basedir}/src/main/wro/main.less</css>
<js>webjar:jquery/2.1.1/jquery.min.js</js>
<js>webjar:bootstrap/3.2.0/bootstrap.js</js>
<js>webjar:angularjs/1.3.8/angular.min.js</js>
</group>
</groups>
main.less
是空的,但可以用於自訂外觀,變更 Twitter Bootstrap 中的預設設定。例如,若要將顏色從預設的藍色變更為淺粉紅色,您可以新增一行:@brand-primary: #de8579;
。
將這些檔案複製到您的專案並執行 "mvn package",您應該會在您的 JAR 檔案中看到 "bootstrap-angular.*" 資源。如果您現在執行應用程式,您應該會看到 CSS 生效,但商業邏輯和導覽仍然遺失。
讓我們建立 "hello" 應用程式(在 "src/main/resources/static/js/hello.js" 中,以便我們的 "index.html" 底部的 <script/>
可以在正確的位置找到它)。
一個最小的 Angular JS 應用程式看起來像這樣
angular.module('hello', [])
.controller('home', function($scope) {
$scope.greeting = {id: 'xxx', content: 'Hello World!'}
})
應用程式的名稱是 "hello",它有一個空的(且多餘的)"config" 和一個名為 "home" 的空 "controller"。當我們載入 "index.html" 時,將會呼叫 "home" 控制器,因為我們已經用 ng-controller="home"
裝飾了內容 <div>
。
請注意,我們將一個神奇的 $scope
注入到控制器函數中(Angular 會依命名慣例進行依賴注入,並識別您的函數參數的名稱)。然後在函數內部使用 $scope
來設定此控制器負責的 UI 元素的內容和行為。
如果您在 "src/main/resources/static/js" 下新增該檔案,您的應用程式現在應該是安全且可運作的,並且它會說 "Hello World!"。greeting
由 Angular 在 HTML 中使用 handlebar 佔位符 {{greeting.id}}
和 {{greeting.content}}
呈現。
到目前為止,我們有一個具有硬式編碼問候語的應用程式。這對於了解事物如何組合在一起很有用,但實際上我們期望內容來自後端伺服器,因此讓我們建立一個 HTTP 端點,我們可以從中獲取問候語。在您的 應用程式類別(在 "src/main/java/demo" 中),新增 @RestController
註解並定義一個新的 @RequestMapping
@SpringBootApplication
@RestController
public class UiApplication {
@RequestMapping("/resource")
public Map<String,Object> home() {
Map<String,Object> model = new HashMap<String,Object>();
model.put("id", UUID.randomUUID().toString());
model.put("content", "Hello World");
return model;
}
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
}
注意:根據您建立新專案的方式,它可能不名為
UiApplication
,並且可能具有@EnableAutoConfiguration @ComponentScan @Configuration
而不是@SpringBootApplication
。
執行該應用程式並嘗試 curl "/resource" 端點,您會發現預設情況下它是安全的
$ curl localhost:8080/resource
{"timestamp":1420442772928,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/resource"}
現在讓我們在瀏覽器中取得該訊息。修改 "home" 控制器,使用 XHR 載入受保護的資源。
angular.module('hello', [])
.controller('home', function($scope, $http) {
$http.get('/resource/').success(function(data) {
$scope.greeting = data;
})
});
我們注入了一個 $http
服務,它是 Angular 作為核心功能提供的,並使用它來 GET 我們的資源。 Angular 將來自回應主體的 JSON 傳遞給成功時的回呼函式。
再次執行應用程式(或直接重新載入瀏覽器中的首頁),您將看到帶有其唯一 ID 的動態訊息。 因此,即使資源受到保護並且您無法直接使用 curl 取得它,瀏覽器也能夠存取內容。 我們在不到一百行程式碼中就有了一個安全的單頁應用程式!
## 它是如何運作的?注意:您可能需要強制您的瀏覽器在您更改靜態資源後重新載入它們。 在 Chrome(以及帶有外掛程式的 Firefox)中,您可以使用「開發人員工具」(F12),這可能就足夠了。 或者您可能必須使用 CTRL+F5。
如果您使用一些開發人員工具,可以在瀏覽器中看到瀏覽器和後端之間的互動(通常 F12 會打開它,預設在 Chrome 中有效,需要在 Firefox 中安裝外掛程式)。 以下是一個摘要:
動詞 | 路徑 | 狀態 | 回應 |
---|---|---|---|
GET | / | 401 | 瀏覽器提示進行身份驗證 |
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 | /resource | 200 | JSON 招呼語 |
您可能看不到 401,因為瀏覽器將首頁載入視為單一互動,並且您可能會看到對 "/resource" 的 2 個請求,因為存在 CORS 協商。
更仔細地查看請求,您會看到它們都具有 "Authorization" 標頭,如下所示:
Authorization: Basic dXNlcjpwYXNzd29yZA==
瀏覽器正在隨每個請求傳送使用者名稱和密碼(因此請記住在生產環境中完全使用 HTTPS)。 這與 "Angular" 無關,因此它可以與您的 JavaScript 框架或非框架選項一起使用。
從表面上看,我們似乎做得很好,它簡潔、易於實現,我們所有的資料都受到秘密密碼的保護,如果我們更改前端或後端技術,它仍然可以工作。 但還是有一些問題。
基本身份驗證僅限於使用者名稱和密碼身份驗證。
身份驗證 UI 無處不在,但很醜陋(瀏覽器對話方塊)。
沒有針對 跨站請求偽造 (CSRF) 的保護。
CSRF 並不是我們目前應用程式的真正問題,因為它只需要 GET 後端資源(即伺服器中沒有更改狀態)。 只要您的應用程式中有 POST、PUT 或 DELETE,它就無法透過任何合理的現代方法來保護。
在本系列的 下一篇文章中,我們將擴充應用程式以使用基於表單的身份驗證,它比 HTTP Basic 靈活得多。 一旦我們有了表單,我們就需要 CSRF 保護,Spring Security 和 Angular 都有一些不錯的現成功能來幫助解決這個問題。 劇透:我們將需要使用 HttpSession
。
感謝:我要感謝所有幫助我開發這個系列的人,特別是 Rob Winch 和 Thorsten Spaeth,感謝他們對文章和原始碼的仔細審閱,並感謝他們教我一些我甚至不知道的技巧,即使是我認為最熟悉的部分。