保护 Web 应用程序

本指南将引导你完成使用 Spring Security 保护资源的简单 Web 应用程序的创建过程。

你将构建什么

你将构建一个 Spring MVC 应用程序,该应用程序使用固定用户列表作为后盾来保护带有登录表单的页面。

你需要什么

如何完成本指南

与大多数 Spring 入门指南 一样,你可以从头开始完成每一步,也可以跳过你已熟悉的设置步骤。无论哪种方式,你最终都会得到可用的代码。

从头开始,请转到 从 Spring Initializr 开始

跳过基础知识,请执行以下操作

完成后,可以将结果与 gs-securing-web/complete 中的代码进行比较。

从 Spring Initializr 开始

可以使用这个预初始化项目,然后单击“生成”以下载 ZIP 文件。此项目已配置为适合本教程中的示例。

手动初始化项目

  1. 导航到 https://start.spring.io。此服务会提取应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven 以及要使用的语言。本指南假定您选择了 Java。

  3. 单击依赖项,然后选择Spring WebThymeleaf

  4. 单击生成

  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 将自动使用“basic”身份验证保护所有 HTTP 端点。但是,您可以进一步自定义安全设置。您需要做的第一件事是将 Spring Security 添加到类路径。

使用 Gradle,您需要在 build.gradle 中的 dependencies 闭包中添加三行(一行用于应用程序,一行用于 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.1.RELEASE'
testImplementation 'org.springframework.security:spring-security-test'

以下清单显示了完成的 build.gradle 文件

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.0'
	id 'io.spring.dependency-management' version '1.1.5'
}

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.1.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

如果您单击 退出 按钮,您的身份验证将被撤销,您将返回到登录页面,并显示一条消息,指出您已注销。

摘要

恭喜!您已经开发了一个使用 Spring Security 保护的简单 Web 应用程序。

另请参阅

以下指南也可能会有所帮助

想编写新指南或为现有指南做出贡献?查看我们的贡献指南

所有指南均随代码的 ASLv2 许可证和写作的署名、禁止派生创意共享许可证发布。