Spring Security 简明教程

Spring Security - Redirection

在 Web 应用程序中,我们常常需要根据用户个人资料登录到不同的页面。例如,普通用户可能会登录到用户主页,而管理员则可能会登录到管理控制台。我们可以使用 Spring Security 很容易地实现这一要求,Spring Security 提供支持来处理登录成功,并且根据用户角色,我们可以决定向用户显示哪个页面,或简单地将用户重定向到所需的页面。

In web application, we’re often required to land to different pages based on user profile. For example, a normal user may land on user home page whereas an admin may land on Admin console. We can achieve this requirement very easily using spring security which provides supports to handle login success and based on user role, we can decide which page to be shown to the user or simply redirect the user to the required page.

为了实现重定向,我们需要像下面所示,在 Spring Security 配置中处理 formLogin 的 successHandler:

In order to achieve redirection, we need to handle successHandler of formLogin in Spring Security Configuration as shown below:

protected void configure(HttpSecurity http) throws Exception {
http
   // ...
   // key should be unique
   .formLogin(form -> form.loginPage("/login")
      .defaultSuccessUrl("/")
      .failureUrl("/login?error=true")
      .successHandler(authenticationSuccessHandler())
      .permitAll())
   //
   .build();
}

此处的 authenticationSuccessHandler() 方法是用来处理登录成功并重定向用户到目标页面的另一个 bean。

Here authenticationSuccessHandler() method is another bean to handle login success and redirect user to the required page.

@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
   return new AuthenticationHandler();
}

AuthenticationSuccessHandler

为了实现重定向,我们首先需要通过实现 AuthenticationSuccessHandler 创建一个类,如下所示。在这个类中,我们必须实现 onAuthenticationSuccess() 方法。onAuthenticationSuccess() 方法在用户成功登录后被调用。现在,使用 Authentication 对象,我们可以检查已登录用户的角色,然后确定重定向 URL。使用 HttpServletResponse.sendRedirect() 方法,我们然后就能将用户重定向到目标页面。

In order to achieve redirection, we first need to create a class by implementing AuthenticationSuccessHandler as shown below. In this class, we’ve to implement onAuthenticationSuccess() method. onAuthenticationSuccess() method is called once user is logged in successfully. Now using Authentication object, we can check the role of the logged in user and then determine the redirection url. Using HttpServletResponse.sendRedirect() method, we can then redirect user to the required page.

public class AuthenticationHandler implements AuthenticationSuccessHandler {

   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
      String redirect = request.getContextPath();

      if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
         redirect = "/admin";
      } else if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
         redirect = "/user";
      }

      response.sendRedirect(redirect);
   }
}

让我们从 Spring Security 开始实际编程。在您开始使用 Spring 框架编写第一个示例之前,您必须确保已按照 Spring Security - Environment Setup 章节中所述正确设置了 Spring 环境。我们还假设您对 Spring Tool Suite IDE 有些许了解。

Let us start actual programming with Spring Security. Before you start writing your first example using Spring framework, you have to make sure that you have set up your Spring environment properly as explained in Spring Security - Environment Setup Chapter. We also assume that you have a bit of working knowledge on Spring Tool Suite IDE.

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

Now let us proceed to write a Spring MVC based Application managed by Maven, which will ask user to login, authenticate user and then provide option to logout using Spring Security Form Login Feature.

Create Project using Spring Initializr

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

Spring Initializr is great way to start with Spring Boot project. It provides a easy to use User Interface to create a project, add dependencies, select java runtime etc. It generates a skeleton project structure which once downloaded can be imported in spring tool suite and we can proceed with our readymade project structure.

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

We’re choosing a maven project, naming the project as formlogin, with java version as 21. Following dependencies are added:

  1. Spring Web

  2. Spring Security

  3. Thymeleaf

  4. Spring Boot DevTools

spring initializr

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

Thymeleaf is a templating engine for Java. It allows us to quickly develop static or dynamic web pages for rendering in the browser. It is extremely extensible and allows us to define and customize the processing of our templates in fine detail. In addition to this, we can learn more about Thymeleaf by clicking this link.

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

Let’s move on to generate our project and download it. We then extract it to a folder of our choice and use any IDE to open it. I shall be using Spring Tools Suite 4. It is available for free downloading from the https://spring.io/tools website and is optimized for spring applications.

pom.xml with all relevant dependencies

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

Let’s take a look at our pom.xml file. It should look something similar to this −

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>

Spring Security Configuration Class

在我们的 config 包中,我们创建了 WebSecurityConfig 类。我们将使用此类来进行我们的安全配置,因此让我们用 @Configuration 注解和 @EnableWebSecurity 为它添加注释。因此,Spring Security 知道将此类视为配置类。正如我们所看到的,Spring 让我们能很轻松地配置应用。

Inside of our config package, we have created the WebSecurityConfig class. We shall be using this class for our security configurations, so let’s annotate it with an @Configuration annotation and @EnableWebSecurity. As a result, Spring Security knows to treat this class a configuration class. As we can see, configuring applications have been made very easy by Spring.

WebSecurityConfig

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;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

@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")
            .successHandler(authenticationSuccessHandler())
            .permitAll())
         .rememberMe(config -> config.key("123456")
         .tokenValiditySeconds(3600))
         .logout(config -> config
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login")
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID"))
         .build();
   }

   @Bean
   public AuthenticationSuccessHandler authenticationSuccessHandler() {
      return new AuthenticationHandler();
   }
}

在这里,我们在 successHandler() 方法中使用了 authenticationSuccessHandler() 来执行所需的重定向。AuthenticationHandler 类应位于同一包中。

Here we’ve used the authenticationSuccessHandler() in successHandler() method to do the required redirection. The AuthenticationHandler class should be in same package.

AuthenticationHandler class

下面的类实现了 AuthenticationSuccessHandler,如下所示。在这个类中,我们实现了 onAuthenticationSuccess() 方法。onAuthenticationSuccess() 方法在用户成功登录后被调用。

Following class implements AuthenticationSuccessHandler as shown below. In this class, we’ve implemented onAuthenticationSuccess() method. onAuthenticationSuccess() method is called once user is logged in successfully.

AuthenticationHandler

package com.tutorialspoint.security.formlogin.config;

import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class AuthenticationHandler implements AuthenticationSuccessHandler {

   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
      String redirect = request.getContextPath();

      if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
         redirect = "/admin";
      } else if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
         redirect = "/user";
      }

      response.sendRedirect(redirect);
   }
}

Controller Class

在这个类中,我们为该应用程序的首页和登录页创建了 "/" 端点与 "/login" 的映射。对于 user.html 和 admin.html,我们添加了 user() 和 admin() 这两个方法。

In this class, we’ve created a mapping for "/" endpoint and for "/login" for the index page and login page of this application. For user.html and admin.html we’ve two methods user() and admin() added.

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";
   }
   @GetMapping("/user")
   public String user() {
      return "user";
   }
   @GetMapping("/admin")
   public String admin() {
      return "admin";
   }
}

Views

让我们在 /src/main/resources/templates 文件夹中创建 index.html,并添加以下内容以用作主页并显示已登录的用户名。

Let’s create index.html in /src/main/resources/templates folder with following content to act as a home page and to display logged in user name.

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-springsecurity3">
   <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,并使其内容如下,以用作登录页面。我们对文本字段使用默认名称 usernamepasswordremember-me 。对于其他名称,我们需要在 Spring 安全性配置类中设置相同的名称。

Let’s create the login.html in /src/main/resources/templates folder with following content to act as a login page. We’re using default name username, password and remember-me for text fields. In case of other name, we need to set the same in spring security config class as well.

<!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">
         <h1>Please sign in</h1>
         <table>
            <tr>
               <td><label for="username"><b>Username</b></label></td>
               <td><input type="text" placeholder="Enter Username" name="username" id="username" required></td>
            </tr>
            <tr>
               <td><label for="password"><b>Password</b></label></td>
               <td><input type="password" placeholder="Enter Password" name="password" id="password" required></td>
            </tr>
            <tr>
               <td><label for="remember-me"><b>Remember Me</b></label> </td>
               <td><input type="checkbox" name="remember-me" /></td>
            </tr>
            <tr>
               <td> </td>
               <td><input type="submit"  value="Sign In" /></td>
            </tr>
         </table>
      </form>
   </body>
</html>

在登录表单中,我们使用 POST 方法登录,同时使用名称和 ID 为 usernamepasswordremember-me 的输入字段和复选框。如果选中复选框,则当用户登录该应用程序时,将创建一个记住我 Cookie。

In login form, we’re using POST method to login while using input fields with name and id as username, password and remember-me checkbox. If checkbox is selected, a remember-me Cookie will be created when user logs in the application.

admin.html

让我们在 /src/main/resources/templates 文件夹中创建 admin.html,其内容如下,它充当使用 ADMIN 角色凭据登录后的用户主页。

Let’s create the admin.html in /src/main/resources/templates folder with following content to act as a home page for user when logged using ADMIN Role credentials.

<!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>
         Hello Admin!
      </title>
   </head>
   <body>
      <p>Admin Console</p>
      <h1 th:inline="text">Welcome <span sec:authentication="name"></span>!</h1>
      <a href="/logout" alt="logout">Sign Out</a>
   </body>
<html>

user.html

让我们在 /src/main/resources/templates 文件夹中创建 user.html,其内容如下,它充当使用 USER 角色凭据登录后的用户主页。

Let’s create the user.html in /src/main/resources/templates folder with following content to act as a home page for user when logged using USER Role credentials.

<!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>
         Hello User!
      </title>
   </head>
   <body>
      <p>User Dashboard</p>
      <h1 th:inline="text">Hello <span sec:authentication="name"></span>!</h1>
      <a href="/logout" alt="logout">Sign Out</a>
   </body>
<html>

Running the Application

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

As we’ve all component ready, let’s run the Application. Right Click on the project, select Run As and then Spring Boot App.

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

It will boot up the application and once application is started, we can run localhost:8080 to check the changes.

Output

现在打开 localhost:8080,您将看到我们的登录页面。

Now open localhost:8080, you can see our login page.

Login Page with User Details entered

user formlogin

Home Page for User

当我们为用户输入有效凭据时,它会加载用户主页 user.html

When we enter valid credential for a User and it will load home page for User as user.html

homepage user
admin formlogin

Home Page for Admin

当我们为管理员输入有效凭据时,它会加载管理员主页 admin.html

When we enter valid credential for a Admin and it will load home page for Admin as admin.html

homepage admin