Spring Security 简明教程

Spring Security - Custom Form Login

基于表单的登录是 Spring Security 提供支持的用户名/密码验证的一种形式。这是通过 HTML 表单提供的。

每当用户请求受保护的资源时,Spring Security 都会检查请求的身份验证。如果未对请求进行身份验证/授权,用户将被重定向到登录页面。应用程序必须以某种方式呈现登录页面。Spring Security 默认提供该登录表单,如我们在 Spring Security - Form Login 章中所见。

在大多数实际情况下,登录页面都是自定义的,并且必须按照下面给出的方法显式地提供:

protected void configure(HttpSecurity http) throws Exception {
http
   // ...
   .authorizeHttpRequests(
      request -> request.requestMatchers("/login").permitAll()
      .requestMatchers("/**").authenticated()
   )
   .formLogin(form -> form.loginPage("/login").permitAll())
}

让我们使用 Spring Security 开始实际编程。在开始使用 Spring 框架编写示例之前,您必须确保已正确设置 Spring 环境,如 Spring Security - Environment Setup 章节所述。我们还假设您具备 Spring Tool Suite IDE 方面的部分工作知识。

现在,让我们开始编写一个由 Maven 管理的基于 Spring MVC 的应用程序,该应用程序将要求用户登录、认证用户,然后提供使用 Spring Security 表单登录功能注销的选项。

Create Project using Spring Initializr

Spring Initializr 是开始 Spring Boot 项目的一个好方法。它提供了一个易于使用的用户界面来创建项目、添加依赖项、选择 Java 运行时等。它会生成一个框架项目结构,下载后可以在 Spring Tool Suite 中导入,然后我们可以使用现成的项目结构继续进行。

我们选择了一个 maven 项目,将项目命名为 formlogin,并将 java 版本指定为 21。添加了以下依赖项:

  1. Spring Web

  2. Spring Security

  3. Thymeleaf

  4. Spring Boot DevTools

spring initializr

Thymeleaf 是 Java 的模板引擎。它使我们能够快速开发用于在浏览器中渲染的静态或动态网页。它已得到极大的扩展,它允许我们定义和定制精细处理的模板。除此之外,我们可以通过点击 link 来了解更多有关 Thymeleaf 的信息。

让我们开始生成并下载项目。然后我们将它解压到我们选择的文件夹中并使用任何 IDE 来打开它。我将使用 Spring Tools Suite 4 。它可以从 https://spring.io/tools 网站免费下载,且已针对 spring 应用进行优化。

pom.xml with all relevant dependencies

让我们看一看我们的 pom.xml 文件。它应该与以下内容类似 −

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.1</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.tutorialspoint.security</groupId>
   <artifactId>formlogin</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>formlogin</name>
   <description>Demo project for Spring Boot</description>
   <url/>
   <licenses>
      <license/>
   </licenses>
   <developers>
      <developer/>
   </developers>
   <scm>
      <connection/>
      <developerConnection/>
      <tag/>
      <url/>
   </scm>
   <properties>
      <java.version>21</java.version>
   </properties>
   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <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.thymeleaf.extras</groupId>
         <artifactId>thymeleaf-extras-springsecurity6</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-devtools</artifactId>
         <scope>runtime</scope>
         <optional>true</optional>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-test</artifactId>
         <scope>test</scope>
      </dependency>
   </dependencies>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
</project>

Login.html

此代码需要在映射的文件夹中提供 login.html 文件,该文件在命中 /login 时返回。此 HTML 文件应包含一个登录表单。此外,请求应为发送到 /login 的 post 请求。对于用户名和密码,参数名称应分别为“username”和“password”。

/src/main/resources/templates 文件夹中创建 login.html,使用以下内容作为登录页面。

login.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-springsecurity3">
   <head>
      <title>Spring Security Example</title>
   </head>
   <body>
      <div th:if="${param.error}">
         <p>Bad Credentials</p>
      </div>
      <div th:if="${param.logout}">You have been logged out.</div>
      <form th:action="@{/login}" method="post">
         <div>
            <h1>Please sign in</h1>
            <label for="username"><b>Username</b></label>
            <input type="text" placeholder="Enter Username" name="username" id="username" required>
            <label for="password"><b>Password</b></label>
            <input type="password" placeholder="Enter Password" name="password" id="password" required>
            <input type="submit"  value="Sign In" />
          </div>
      </form>
   </body>
</html>

Update Spring Security Configuration Class

在我们的 config 包中,有 WebSecurityConfig 类(如 Spring Security - Form Login 章中所定义)。让我们更新它的 filterChain() 方法,以获取我们的自定义登录页面

http.
//...
   .formLogin(form -> form.loginPage("/login")
   .defaultSuccessUrl("/")
   .failureUrl("/login?error=true")
   .permitAll())
//...
.build();

以下是 Spring Security 配置类的完整代码

WebSecurityConfig.java

package com.tutorialspoint.security.formlogin.config;

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.config.annotation.web.configurers.AbstractHttpConfigurer;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig  {

   @Bean
   protected UserDetailsService userDetailsService() {
      UserDetails user = User.builder()
         .username("user")
         .password(passwordEncoder().encode("user123"))
         .roles("USER")
         .build();
      UserDetails admin = User.builder()
         .username("admin")
         .password(passwordEncoder().encode("admin123"))
         .roles("USER", "ADMIN")
         .build();
      return new InMemoryUserDetailsManager(user, admin);
   }

   @Bean
   protected PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }

   @Bean
   protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      return http
         .csrf(AbstractHttpConfigurer::disable)
         .authorizeHttpRequests(
            request -> request.requestMatchers("/login").permitAll()
            .requestMatchers("/**").authenticated()
         )
         .formLogin(form -> form.loginPage("/login")
            .defaultSuccessUrl("/")
            .failureUrl("/login?error=true")
            .permitAll())
         .logout(config -> config
         .logoutUrl("/logout")
         .logoutSuccessUrl("/login"))
         .build();
   }
}

此处有一些需要说明的点 −

  1. *defaultSuccessUrl ("/") * − 此端点将为我们的应用程序提供索引页和成功页。正如我们之前配置的那样,我们将保护此页面,并且只允许经过身份验证的用户访问此页面。

  2. *failureUrl ("/login?error=true") * − 此端点将载入带有错误标志的登录页面,以显示错误消息。

  3. *logoutUrl ("/logout") * − 将用于注销我们的应用程序。

  4. *logoutSuccessUrl ("/login") * − 一旦用户成功登出,这将用于加载登录页面。

Controller Class

在此类中,我们为该应用程序的索引页和登录页创建了 "/" 端点和 "/login" 的映射。

AuthController.java

package com.tutorialspoint.security.formlogin.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AuthController {
   @GetMapping("/")
   public String home() {
      return "index";
   }
   @GetMapping("/login")
   public String login() {
      return "login";
   }
}

Views

让我们在 /src/main/resources/templates 文件夹中更新 index.html,使用以下内容作为主页并显示登录的用户名。

index.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 sec:authentication="name"></span>!</h1>
      <form th:action="@{/logout}" method="post">
         <input type="submit" value="Sign Out"/>
      </form>
   </body>
<html>

login.html

让我们在 /src/main/resources/templates 文件夹中创建 login.html,并添加以下内容以用作登录页面。我们对文本字段使用默认名称 usernamepassword 。对于其他名称,我们还需要在 spring security config 类中设置相同的名称。

<!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>Spring Security Example</title>
   </head>
   <body>
      <div th:if="${param.error}">
         <p>Bad Credentials</p>
      </div>
      <div th:if="${param.logout}">You have been logged out.</div>
      <form th:action="@{/login}" method="post">
         <div>
            <h1>Please sign in</h1>
            <label for="username">
               <b>Username</b>
            </label>
            <input type="text" placeholder="Enter Username" name="username" id="username" required>
            <label for="password"><b>Password</b></label>
            <input type="password" placeholder="Enter Password" name="password" id="password" required>
            <input type="submit"  value="Sign In" />
         </div>
      </form>
   </body>
</html>

在 login.html 中,我们使用 ${param.error} 读取请求参数 error 。如果它为真,则会打印一条错误消息,如 Bad Credential 所示。类似地,我们使用 ${param.logout} 读取请求参数 logout 。如果它为真,则会打印退出消息。

在登录表单中,我们使用 POST 方法登录,同时使用名称和 ID 为 usernamepassword 的输入字段。

Running the Application

因为我们所有的组件都已就绪,让我们来运行此 Application。右击项目,选择 Run As ,然后选择 Spring Boot App

它将启动该应用,并且一旦应用启动,我们就可以运行 localhost:8080 来查看更改。

Output

现在打开 localhost:8080,您可以看到我们自定义的登录页面。

Login Page

custom formlogin

Login Page for Bad Credentials

输入任何无效的凭据,它将显示错误。

custom formlogin error

Home Page

输入有效的凭据

custom formlogin valid credentials

它将载入主页。

custom formlogin success

After Logout

现在点击注销按钮,这将再次加载登录页面。

custom formlogin