保護 Web 應用程式安全

本指南將引導您完成建立簡單 Web 應用程式的過程,該應用程式的資源受到 Spring Security 的保護。

您將建置什麼

您將建置一個 Spring MVC 應用程式,該應用程式使用登入表單保護頁面安全,並由固定的使用者列表支援。

您需要什麼

如何完成本指南

與大多數 Spring 入門指南一樣,您可以從頭開始並完成每個步驟,或者您可以略過您已熟悉的基礎設定步驟。無論哪種方式,您最終都會得到可運作的程式碼。

若要從頭開始,請移至 從 Spring Initializr 開始

若要略過基礎知識,請執行下列操作

當您完成時,您可以對照 gs-securing-web/complete 中的程式碼檢查您的結果。

從 Spring Initializr 開始

您可以使用這個預先初始化的專案,然後按一下 Generate (產生) 以下載 ZIP 檔案。此專案已設定為符合本教學課程中的範例。

若要手動初始化專案

  1. 導覽至 https://start.spring.io。此服務會提取應用程式所需的所有相依性,並為您完成大部分設定。

  2. 選擇 Gradle 或 Maven 以及您想要使用的語言。本指南假設您選擇 Java。

  3. 按一下 Dependencies (相依性),然後選取 Spring WebThymeleaf

  4. 按一下 Generate (產生)

  5. 下載產生的 ZIP 檔案,這是一個已使用您的選擇設定的 Web 應用程式封存檔。

如果您的 IDE 具有 Spring Initializr 整合,您可以從您的 IDE 完成此程序。
您也可以從 Github 分支專案,並在您的 IDE 或其他編輯器中開啟它。

建立未受保護的 Web 應用程式

在您可以將安全性套用至 Web 應用程式之前,您需要一個要保護的 Web 應用程式。本節將引導您建立一個簡單的 Web 應用程式。然後,您將在下一節使用 Spring Security 保護它。

Web 應用程式包含兩個簡單的檢視畫面:首頁和「Hello, World」頁面。首頁定義在下列 Thymeleaf 範本中 (來自 src/main/resources/templates/home.html)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

        <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
    </body>
</html>

這個簡單的檢視畫面包含一個連結到 /hello 頁面的連結,該頁面定義在下列 Thymeleaf 範本中 (來自 src/main/resources/templates/hello.html)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello world!</h1>
    </body>
</html>

Web 應用程式是以 Spring MVC 為基礎。因此,您需要設定 Spring MVC 並設定檢視畫面控制器以公開這些範本。以下列表 (來自 src/main/java/com/example/securingweb/MvcConfig.java) 顯示在應用程式中設定 Spring MVC 的類別

package com.example.securingweb;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/home").setViewName("home");
		registry.addViewController("/").setViewName("home");
		registry.addViewController("/hello").setViewName("hello");
		registry.addViewController("/login").setViewName("login");
	}

}

addViewControllers() 方法 (覆寫 WebMvcConfigurer 中同名的方法) 新增了四個檢視畫面控制器。其中兩個檢視畫面控制器參考名稱為 home 的檢視畫面 (定義在 home.html 中),另一個參考名為 hello 的檢視畫面 (定義在 hello.html 中)。第四個檢視畫面控制器參考另一個名為 login 的檢視畫面。您將在下一節中建立該檢視畫面。

此時,您可以跳到「執行應用程式」並執行應用程式,而無需登入任何內容。

現在您有了一個未受保護的 Web 應用程式,您可以新增安全性。

設定 Spring Security

假設您想要防止未經授權的使用者檢視 /hello 的問候頁面。如同現在的狀況,如果訪客按一下首頁上的連結,他們會看到問候語,而沒有任何障礙阻止他們。您需要新增一個障礙,強制訪客先登入才能看到該頁面。

您可以透過在應用程式中設定 Spring Security 來完成此操作。如果 Spring Security 在類別路徑上,Spring Boot 會自動使用「基本」驗證來保護所有 HTTP 端點。但是,您可以進一步自訂安全性設定。您需要做的第一件事是將 Spring Security 新增至類別路徑。

使用 Gradle 時,您需要在 build.gradledependencies 閉包中新增三行 (應用程式一行、Thymeleaf 和 Spring Security 整合一行,以及測試一行),如下列列表所示

implementation 'org.springframework.boot:spring-boot-starter-security'
//  Temporary explicit version to fix Thymeleaf bug
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'
testImplementation 'org.springframework.security:spring-security-test'

以下列表顯示完成的 build.gradle 檔案

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.0'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	//  Temporary explicit version to fix Thymeleaf bug
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'
	testImplementation 'org.springframework.security:spring-security-test'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

使用 Maven 時,您需要在 pom.xml 中的 <dependencies> 元素中新增兩個額外的項目 (應用程式一個,測試一個),如下列列表所示

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity6</artifactId>
	<!-- Temporary explicit version to fix Thymeleaf bug -->
	<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-test</artifactId>
	<scope>test</scope>
</dependency>

以下列表顯示完成的 pom.xml 檔案

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.3.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>securing-web-complete</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>securing-web-complete</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>17</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity6</artifactId>
			<!-- Temporary explicit version to fix Thymeleaf bug -->
			<version>3.1.1.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

</project>

以下安全性設定 (來自 src/main/java/com/example/securingweb/WebSecurityConfig.java) 確保只有通過驗證的使用者才能看到秘密問候語

package com.example.securingweb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((requests) -> requests
				.requestMatchers("/", "/home").permitAll()
				.anyRequest().authenticated()
			)
			.formLogin((form) -> form
				.loginPage("/login")
				.permitAll()
			)
			.logout((logout) -> logout.permitAll());

		return http.build();
	}

	@Bean
	public UserDetailsService userDetailsService() {
		UserDetails user =
			 User.withDefaultPasswordEncoder()
				.username("user")
				.password("password")
				.roles("USER")
				.build();

		return new InMemoryUserDetailsManager(user);
	}
}

WebSecurityConfig 類別使用 @EnableWebSecurity 註解,以啟用 Spring Security 的 Web 安全性支援並提供 Spring MVC 整合。它還公開了兩個 Bean,以設定 Web 安全性組態的一些細節

SecurityFilterChain Bean 定義了哪些 URL 路徑應受到保護,哪些不應受到保護。具體來說,//home 路徑被設定為不需要任何驗證。所有其他路徑都必須通過驗證。

當使用者成功登入時,他們會被重新導向到先前請求的需要驗證的頁面。有一個自訂的 /login 頁面 (由 loginPage() 指定),並且允許所有人檢視它。

UserDetailsService Bean 設定了一個具有單一使用者的記憶體內使用者儲存區。該使用者被賦予使用者名稱 user、密碼 password 和角色 USER

現在您需要建立登入頁面。已經有一個用於 login 檢視畫面的檢視畫面控制器,因此您只需要建立登入檢視畫面本身,如下列列表 (來自 src/main/resources/templates/login.html) 所示

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>

這個 Thymeleaf 範本呈現一個表單,該表單擷取使用者名稱和密碼,並將它們發佈到 /login。如設定所示,Spring Security 提供了一個篩選器,該篩選器攔截該請求並驗證使用者。如果使用者驗證失敗,頁面將被重新導向到 /login?error,並且您的頁面會顯示適當的錯誤訊息。成功登出後,您的應用程式將被傳送到 /login?logout,並且您的頁面會顯示適當的成功訊息。

最後,您需要為訪客提供一種方式來顯示目前的使用者名稱並登出。為此,請更新 hello.html 以向目前使用者問好並包含一個 Sign Out 表單,如下列列表 (來自 src/main/resources/templates/hello.html) 所示

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1 th:inline="text">Hello <span th:remove="tag" sec:authentication="name">thymeleaf</span>!</h1>
        <form th:action="@{/logout}" method="post">
            <input type="submit" value="Sign Out"/>
        </form>
    </body>
</html>

我們透過使用 Thymeleaf 與 Spring Security 的整合來顯示使用者名稱。「Sign Out (登出)」表單將 POST 提交到 /logout。成功登出後,它會將使用者重新導向到 /login?logout

Thymeleaf 3.1 不再提供對 HttpServletRequest 的存取,因此 HttpServletRequest#getRemoteUser() 無法用於存取目前已驗證的使用者。

執行應用程式

Spring Initializr 為您建立了一個應用程式類別。在這種情況下,您無需修改該類別。以下列表 (來自 src/main/java/com/example/securingweb/SecuringWebApplication.java) 顯示應用程式類別

package com.example.securingweb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecuringWebApplication {

	public static void main(String[] args) throws Throwable {
		SpringApplication.run(SecuringWebApplication.class, args);
	}

}

建置可執行 JAR

您可以使用 Gradle 或 Maven 從命令列執行應用程式。您也可以建置一個包含所有必要相依性、類別和資源的單一可執行 JAR 檔案並執行它。建置可執行 jar 可以輕鬆地在整個開發生命週期、跨不同環境等情況下,將服務作為應用程式交付、版本化和部署。

如果您使用 Gradle,您可以使用 ./gradlew bootRun 來執行應用程式。或者,您可以使用 ./gradlew build 建置 JAR 檔案,然後執行 JAR 檔案,如下所示

java -jar build/libs/gs-securing-web-0.1.0.jar

如果您使用 Maven,您可以使用 ./mvnw spring-boot:run 來執行應用程式。或者,您可以使用 ./mvnw clean package 建置 JAR 檔案,然後執行 JAR 檔案,如下所示

java -jar target/gs-securing-web-0.1.0.jar
此處描述的步驟會建立可執行的 JAR。您也可以建置傳統 WAR 檔案

一旦應用程式啟動,請將您的瀏覽器指向 https://127.0.0.1:8080。您應該會看到首頁,如下圖所示

The application’s home page

當您按一下連結時,它會嘗試將您帶到 /hello 的問候頁面。但是,由於該頁面受到保護,而且您尚未登入,因此它會將您帶到登入頁面,如下圖所示

The login page
如果您使用未受保護的版本跳到這裡,您看不到登入頁面。您應該返回並撰寫其餘基於安全性的程式碼。

在登入頁面上,輸入 userpassword 分別作為使用者名稱和密碼欄位,以使用者身份登入。一旦您提交登入表單,您就會通過驗證,然後被帶到問候頁面,如下圖所示

The secured greeting page

如果您按一下 Sign Out (登出) 按鈕,您的驗證將被撤銷,並且您將返回到登入頁面,並顯示您已登出的訊息。

摘要

恭喜!您已開發出一個使用 Spring Security 保護安全的簡單 Web 應用程式。

另請參閱

下列指南也可能有所幫助

想要撰寫新的指南或貢獻現有的指南嗎?請查看我們的貢獻指南

所有指南均以 ASLv2 授權發佈程式碼,並以 姓名標示-禁止改作創用 CC 授權條款 3.0 發佈寫作內容。

取得程式碼