Web 应用安全

本指南将引导您完成创建简单的 Web 应用的过程,该应用的资源受 Spring Security 保护。

您将构建什么

您将构建一个 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 会自动使用“基本”身份验证来保护所有 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.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以向当前用户问好并包含一个“注销”表单,如下所示(来自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 的集成来显示用户名。“注销”表单将 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
如果您使用未保护的版本跳到这里,您将看不到登录页面。您应该返回并编写其余基于安全性的代码。

在登录页面,使用用户名字段输入user,密码字段输入password,以测试用户身份登录。提交登录表单后,您将通过身份验证并进入问候页面,如下图所示

The secured greeting page

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

摘要

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

另请参阅

以下指南可能对您有所帮助

想编写新的指南或为现有指南贡献内容?请查看我们的贡献指南

所有指南均采用ASLv2许可证发布代码,并采用署名-非衍生创作共享许可证发布文本。