保護 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 Fork 專案並在您的 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”表單向 /logout 提交 POST 請求。成功退出後,它會將使用者重定向到 /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://:8080。您應該會看到主頁,如下圖所示

The application’s home page

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

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

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

The secured greeting page

如果您點選 Sign Out 按鈕,您的身份驗證將被撤銷,並且您將返回到登入頁面,並顯示一條訊息,表明您已退出登入。

總結

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

另請參閱

以下指南可能也有幫助

想撰寫新指南或為現有指南貢獻力量?請檢視我們的貢獻指南

所有指南的程式碼都使用 ASLv2 許可證釋出,文字內容使用 署名-禁止演繹創作共用許可證 釋出。

獲取程式碼