領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多注意:本文的原始碼和測試將繼續演進,但文字的更改在此處不再維護。請參閱教程版本以獲取最新內容。
在本文中,我們將展示 Spring Security、Spring Boot 和 Angular JS 協同工作的一些優秀功能,以提供令人愉悅且安全的iram體驗。它應該對 Spring 和 Angular JS 的初學者來說是易於理解的,但也有很多細節對兩者的專家都將很有用。這實際上是關於 Spring Security 和 Angular JS 系列文章的第一篇,每一篇都會逐步介紹新功能。我們將在第二篇及後續篇章中改進應用程式,但在此之後的主要更改將是架構性的而非功能性的。
HTML5、豐富的瀏覽器功能和“單頁應用程式”是現代開發者的極其寶貴的工具,但任何有意義的互動都將涉及後端伺服器,因此除了靜態內容(HTML、CSS 和 JavaScript)之外,我們還需要一個後端伺服器。後端伺服器可以扮演許多角色中的任何一個或所有角色:提供靜態內容、有時(如今不那麼頻繁)渲染動態 HTML、對使用者進行身份驗證、保護對受保護資源的訪問,以及(最後但同樣重要的是)透過 HTTP 和 JSON(有時稱為 REST API)與瀏覽器中的 JavaScript 進行互動。
Spring 一直是構建後端功能(尤其是在企業中)的流行技術,並且隨著 Spring Boot 的出現,一切都變得前所未有的簡單。讓我們來看看如何使用 Spring Boot、Angular JS 和 Twitter Bootstrap 從零開始構建一個新的單頁應用程式。選擇這個特定的技術棧沒有特別的原因,但它相當流行,尤其是在企業 Java 商店的核心 Spring 圈子裡,所以這是一個有價值的起點。
我們將詳細介紹如何建立此應用程式,以便不完全熟悉 Spring 和 Angular 的任何人都可以理解正在發生的事情。如果您寧願直奔主題,可以跳到最後檢視應用程式的工作情況,並瞭解所有內容是如何組合在一起的。建立新專案有多種選擇
我們將要構建的完整專案的原始碼在此 Github 上,如果您願意,可以克隆該專案並直接在此基礎上進行開發。然後跳到下一節。
建立新專案入門的最簡單方法是透過 Spring Boot Initializr。例如,在類 Unix 系統上使用 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”的應用程式。
所有 CSS 類("ng-cloak" 除外)都來自 Twitter Bootstrap。一旦我們設定好正確的樣式表,它們將使一切看起來都很漂亮。
問候語中的內容使用 handlebars 標記,例如 {{greeting.content}},它稍後將由 Angular 填充(根據周圍 <div> 上的 ng-controller 指令,使用一個名為“home”的“控制器”)。
Angular JS(和 Twitter Bootstrap)包含在 <body> 的底部,以便瀏覽器可以在處理之前處理所有 HTML。
我們還包含一個單獨的 "hello.js",我們將在此定義應用程式行為。
我們將在稍後建立指令碼和樣式表資源,但現在我們可以忽略它們不存在的事實。
新增主頁檔案後,您的應用程式即可在瀏覽器中載入(即使它目前功能不多)。在命令列上,您可以這樣做
$ mvn spring-boot:run
並在瀏覽器中訪問 https://: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",我們也可以像 CSS 一樣直接使用 Google CDN 的靜態資源。但是,在實際應用程式中,我們幾乎肯定會想要修改樣式表,並且不希望手動編輯 CSS 原始檔,因此我們將使用一個更高級別的工具(例如 Less 或 Sass),所以我們也打算使用一個。
有許多不同的方法可以做到這一點,但就本文而言,我們將使用 wro4j,這是一個基於 Java 的工具鏈,用於預處理和打包前端資源。它可以作為 JIT(即時)Filter 用於任何 Servlet 應用程式,但它也對 Maven 和 Eclipse 等構建工具有很好的支援,我們也將以此方式使用它。因此,我們將構建靜態資原始檔並將它們打包到我們的應用程式 JAR 中。
附註:Wro4j 可能不是硬核前端開發者的首選工具 - 他們可能會使用基於 Node 的工具鏈,配合 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>
您可以將此 verbatim 複製到您的 POM 中,或者如果您正在從 Github 中的原始碼跟進,只需掃描它即可。主要要點是
我們將一些 webjars 庫作為依賴項包含(jquery 和 bootstrap 用於 CSS 和樣式,Angular JS 用於業務邏輯)。這些 jar 檔案中的一些靜態資源將包含在我們生成的 "angular-bootstrap.*" 檔案中,但 jar 檔案本身不需要與應用程式一起打包。
Twitter Bootstrap 依賴於 jQuery,所以我們也將其包含在內。不使用 Bootstrap 的 Angular JS 應用程式不需要這個,因為 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" 的單一資源“組”,最終成為生成的靜態資源的基礎名稱。它包含對我們新增的 webjar 中的 <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" 的空“控制器”。當載入 "index.html" 時,"home" 控制器將被呼叫,因為我們在內容 <div> 上添加了 ng-controller="home"。
請注意,我們向控制器函式注入了一個魔術變數 $scope(Angular 透過命名約定進行依賴注入,並識別函式引數的名稱)。然後,在函式內部使用 $scope 來為該控制器負責的使用者介面元素設定內容和行為。
如果您將該檔案新增到 "src/main/resources/static/js" 下,您的應用程式現在應該安全且功能完善,並且會顯示 "Hello World!"。greeting 由 Angular 在 HTML 中使用 handlebars 佔位符 {{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" 的請求,因為有一個 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,感謝他們仔細審閱文章和原始碼,並教會了我一些我不知道的技巧,即使是在那些我以為我最熟悉的方面。