Spring Security 简明教程

Spring Security - Authentication Provider

Spring Security 允许我们尽可能地自定义身份验证流程。从自定义登录页到我们非常自己的自定义身份验证提供程序和身份验证过滤器,我们几乎可以自定义身份验证流程的每个方面。我们可以定义自己的身份验证流程,其范围可以从使用用户名和密码的基本身份验证到使用令牌和 OTP 的复杂身份验证(例如两因素身份验证)。此外,我们可以使用各种数据库(包括关系型和非关系型),使用各种密码编码器,将恶意用户锁定在其账户之外,等等。

Spring Security allows us to customize our authentication process as much as we want. Starting from a custom login page to our very own customized authentication providers and authentication filters, we can pretty much customize every aspect of the authentication process. We can define our own authentication process which can range from basic authentication using a username and a password to a complex one such as two-factor authentication using tokens and OTP’s. Also, we can use various databases – both relational and non-relational, use various password encoders, lock malicious users out of their accounts, and so on.

Spring Security Architecture

components of spring security architecture

正如我们在上图中看到的,Spring Security 的基本组件如下。我们将在进行过程中对其进行简要讨论。我们还将讨论它们在身份验证和授权过程中的作用。

The basic components of Spring Security, as we can see in the above diagram are given below. We shall discuss them briefly as we go along. We shall also discuss their roles in the authentication and authorization process.

AuthenticationFilter

这是拦截请求并尝试对其进行身份验证的过滤器。在 Spring Security 中,它将请求转换为一个 Authentication Object,并将身份验证委托给 AuthenticationManager。

This is the filter that intercepts requests and attempts to authenticate it. In Spring Security, it converts the request to an Authentication Object and delegates the authentication to the AuthenticationManager.

AuthenticationManager

这是用于身份验证的主要策略界面。它使用单一方法 authenticate() 来对请求进行身份验证。authenticate() 方法执行身份验证,并在身份验证成功后返回一个 Authentication Object,或者在身份验证失败的情况下抛出 AuthenticationException。如果该方法无法判断,它将返回 null。此过程中的身份验证过程将委托给 AuthenticationProvider,我们将在后面讨论它。

It is the main strategy interface for authentication. It uses the lone method authenticate() to authenticate the request. The authenticate() method performs the authentication and returns an Authentication Object on successful authentication or throw an AuthenticationException in case of authentication failure. If the method can’t decide, it will return null. The process of authentication in this process is delegated to the AuthenticationProvider which we will discuss next.

AuthenticationProvider

AuthenticationManager 由 ProviderManager 实现,后者将进程委托给一个或多个 AuthenticationProvider 实例。实现 AuthenticationProvider 接口的任何类都必须实现两个方法——authenticate() 和 supports()。首先,让我们谈谈 supports() 方法。它用于检查我们的 AuthenticationProvider 实现类是否支持特定的身份验证类型。如果支持,则返回 true,否则返回 false。接下来是 authenticate() 方法。身份验证发生在这里。如果支持身份验证类型,则开始身份验证进程。在这里,此类可以使用 UserDetailsService 实现的 loadUserByUsername() 方法。如果找不到用户,它可能会抛出 UsernameNotFoundException。

The AuthenticationManager is implemented by the ProviderManager which delegates the process to one or more AuthenticationProvider instances. Any class implementing the AuthenticationProvider interface must implement the two methods – authenticate() and supports(). First, let us talk about the supports() method. It is used to check if the particular authentication type is supported by our AuthenticationProvider implementation class. If it is supported it returns true or else false. Next, the authenticate() method. Here is where the authentication occurs. If the authentication type is supported, the process of authentication is started. Here is this class can use the loadUserByUsername() method of the UserDetailsService implementation. If the user is not found, it can throw a UsernameNotFoundException.

另一方面,如果找到了用户,那么会使用用户的认证详细信息来对用户进行认证。例如,在基本认证方案中,可能会使用数据库中的密码来检查用户提供的密码。如果发现它们彼此匹配,则是一个成功场景。然后,我们可以从方法中返回一个认证对象,该对象将存储在安全上下文中,我们将在后面讨论安全上下文。

On the other hand, if the user is found, then the authentication details of the user are used to authenticate the user. For example, in the basic authentication scenario, the password provided by the user may be checked with the password in the database. If they are found to match with each other, it is a success scenario. Then we can return an Authentication object from this method which will be stored in the Security Context, which we will discuss later.

Spring Security 提供了以下主要的 AuthenticationProvider 实现。

Spring Security provides following major implementations of AuthenticationProvider.

  1. DaoAuthenticationProvider − This provider is used to provide database based authentication.

  2. LdapAuthenticationProvider − This provider is specialized for LDAP(Lightweight Directory Access Protocol) based authentication.

  3. OpenIDAuthenticationProvider − This provider is used for OpenID based authentication and can be used with OpenID authentication providers like Google/Facebook etc.

  4. JwtAuthenticationProvider − For JWT(Java Web Token) based authentication, we can use JwtAuthenticationProvider class.

  5. RememberMeAuthenticationProvider − This class is used for user authentication based on remember me token of user.

我们将在下一部分创建自己的 AuthenticationProvider。

We’ll be creating our own AuthenticationProvider in coming section.

UserDetailsService

它是 Spring Security 的核心接口之一。任何请求的认证主要取决于 UserDetailsService 接口的实现。它最常用于基于数据库的认证中以检索用户数据。数据通过实现唯一的 loadUserByUsername() 方法来检索,在该方法中,我们可以提供逻辑以获取用户的用户详细信息。如果找不到用户,该方法将抛出 UsernameNotFoundException。

It is one of the core interfaces of Spring Security. The authentication of any request mostly depends on the implementation of the UserDetailsService interface. It is most commonly used in database backed authentication to retrieve user data. The data is retrieved with the implementation of the lone loadUserByUsername() method where we can provide our logic to fetch the user details for a user. The method will throw a UsernameNotFoundException if the user is not found.

PasswordEncoder

在 Spring Security 4 之前,使用 PasswordEncoder 是可选的。用户可以使用基于内存的认证来存储明文密码。但是 Spring Security 5 强制要求使用 PasswordEncoder 来存储密码。这会使用其许多实现之一对用户的密码进行编码。其最常见的实现是 BCryptPasswordEncoder。此外,出于开发目的,我们可以使用 NoOpPasswordEncoder 的一个实例。它将允许密码以明文存储。但它不应该用于生产或实际应用程序。

Until Spring Security 4, the use of PasswordEncoder was optional. The user could store plain text passwords using in-memory authentication. But Spring Security 5 has mandated the use of PasswordEncoder to store passwords. This encodes the user’s password using one its many implementations. The most common of its implementations is the BCryptPasswordEncoder. Also, we can use an instance of the NoOpPasswordEncoder for our development purposes. It will allow passwords to be stored in plain text. But it is not supposed to be used for production or real-world applications.

Spring Security Context

这是在成功认证时存储当前已认证用户详细信息的位置。然后,整个应用程序在会话期间都可以使用认证对象。因此,如果我们需要用户名或任何其他用户详细信息,则需要首先获得 SecurityContext。这是通过 SecurityContextHolder 一个提供程序类来完成的,它提供对安全上下文的访问。我们可以分别使用 setAuthentication() 和 getAuthentication() 方法来存储和检索用户详细信息。

This is where the details of the currently authenticated user are stored on successful authentication. The authentication object is then available throughout the application for the session. So, if we need the username or any other user details, we need to get the SecurityContext first. This is done with the SecurityContextHolder, a helper class, which provides access to the security context. We can use the setAuthentication() and getAuthentication() methods for storing and retrieving the user details respectively.

Custom Authenticator

我们可以通过实现 AuthenticationProvider 接口来创建一个自定义认证器。AuthenticatorProvider 接口有两个方法 authenticate()supports()

We can create a custom Authenticator by implementing AuthenticationProvider interface. AuthenticatorProvider interface has two methods authenticate() and supports().

authenticate() method

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   String username = authentication.getName();
   String password = authentication.getCredentials().toString();

   UserDetails user = userDetailsService.loadUserByUsername(username);

   if (user == null || !password.equals(user.getPassword())) {
      throw new BadCredentialsException("Invalid username or password");
   }

   List<GrantedAuthority> authorities = new ArrayList();
   authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
   return new UsernamePasswordAuthenticationToken(username, password, authorities);
}

此处在 authenticate() 方法中,我们正在使用认证对象获取用户名和密码,并且我们将用户名/密码与用户凭证进行比较。如果用户详细信息无效,我们将抛出一个 BadCredentialsException 异常。否则,准备一个新角色,并返回带有所需角色的 UsernamePasswordAuthenticationToken。

Here in authenticate() method, we’re getting username and password using authentication object and we’re comparing username/password with the user credentials. In case user details are invalid, we’re throwing an exception as BadCredentialsException. Otherwise, a new role is prepared and UsernamePasswordAuthenticationToken is returned with required role.

supports() method

@Override
public boolean supports(Class<?> authentication) {
   return authentication.equals(UsernamePasswordAuthenticationToken.class);
}

Spring Security Configuration

在 AuthenticationManager 中使用在 AuthenticationProvider 中创建的认证器并将其标记为托管 bean。

Use the AuthenticationProvider created in the AuthenticationManager and mark it as managed bean.

@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
   AuthenticationManagerBuilder authenticationManagerBuilder =
      http.getSharedObject(AuthenticationManagerBuilder.class);
   WebAuthenticationProvider authProvider = new WebAuthenticationProvider(userDetailsService());
   authenticationManagerBuilder.authenticationProvider(authProvider);
   return authenticationManagerBuilder.build();
}

这就是我们需要做的一切。现在让我们仔细看看完整的操作代码。

That’s all we need. Now let’s see the complete code in action.

在开始使用 Spring 框架编写第一个示例之前,你必须确保按照 Spring Security - Environment Setup 章节中的说明,正确地设置你的 Spring 环境。我们还假设你对 Spring Tool Suite IDE 有些基本的操作知识。

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>

Authenticator Provider

在我们的 config 包中,我们通过实现 AuthenticationProvider 接口创建了 WebAuthenticationProvider 类。如同在 authenticate() 方法中,我们必须比较用户名和密码,我们使用了 UserDetailsService 实例来获取用户详情。

Inside of our config package, we have created the WebAuthenticationProvider class by implementing AuthenticationProvider interface. As in authenticate() method, we’ve to compare username along with password, we’ve used UserDetailsService instance to get the user details.

package com.tutorialspoint.security.formlogin.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

@Component
public class WebAuthenticationProvider implements AuthenticationProvider {

   private final UserDetailsService userDetailsService;
   public WebAuthenticationProvider(UserDetailsService userDetailsService) {
      this.userDetailsService = userDetailsService;
   }

   @Override
   public boolean supports(Class<?> authentication) {
      return authentication.equals(UsernamePasswordAuthenticationToken.class);
   }

   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      String username = authentication.getName();
      String password = authentication.getCredentials().toString();

      UserDetails user = userDetailsService.loadUserByUsername(username);

      if (user == null || !password.equals(user.getPassword())) {
         throw new BadCredentialsException("Invalid username or password");
      }

      List<GrantedAuthority> authorities = new ArrayList();
      authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
      return new UsernamePasswordAuthenticationToken(username, password, authorities);

   }
}

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.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

   @Bean
   public AuthenticationManager authManager(HttpSecurity http) throws Exception {
      AuthenticationManagerBuilder authenticationManagerBuilder =
         http.getSharedObject(AuthenticationManagerBuilder.class);
      WebAuthenticationProvider authProvider = new WebAuthenticationProvider(userDetailsService());
      authenticationManagerBuilder.authenticationProvider(authProvider);
      return authenticationManagerBuilder.build();
   }

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

   @Bean
   protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      return http
         .csrf(AbstractHttpConfigurer::disable)
         .authorizeHttpRequests(
            request -> request.requestMatchers("/login").permitAll()
            .requestMatchers("/**").authenticated()
         )
         .formLogin(Customizer.withDefaults())
         .logout(config -> config
         .logoutUrl("/logout")
         .logoutSuccessUrl("/login"))
         .build();
   }
}

Configuration Class Details

让我们看一看我们的配置类。

Let’s take a look at our configuration class.

  1. First, we shall create a bean of our UserDetailsService class by using the userDetailsService() method. We shall be using this bean for managing our users for this application. Here, to keep things simple, we shall use an InMemoryUserDetailsManager instance to create users. These users, along with our given username and password, are mapped to User and Admin roles respectively.

Http Security Configuration

在以上步骤之后,我们将继续进行下一步配置。在此,我们定义了 filterChain 方法。此方法接受 HttpSecurity 作为参数。我们将配置它来使用我们的表单登录和注销功能。

After the above steps, we move on to our next configuration. Here, we’ve defined the filterChain method. This method takes HttpSecurity as a parameter. We shall be configuring this to use our form login and logout function.

我们可以观察到所有这些功能均在 Spring Security 中提供。让我们详细研究以下部分 −

We can observe that all these functionalities are available in Spring Security. Let’s study the below section in detail −

return http
   .csrf(AbstractHttpConfigurer::disable)
   .authorizeHttpRequests(
      request -> request.requestMatchers("/login").permitAll()
         .requestMatchers("/**").authenticated()
    )
    .formLogin(Customizer.withDefaults())
    .logout(config -> config
    .logoutUrl("/logout")
    .logoutSuccessUrl("/login"))
    .build();

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

There are a few points to note here −

  1. We have disabled csrf or Cross-Site Request Forgery protection As this is a simple application only for demonstration purposes, we can safely disable this for now.

  2. Then we add configuration which requires all requests to be authenticated.

  3. After that, we’re using formLogin() functionality of Spring Security as mentioned above. This makes browser to ask for a default login form for username/password and logout() to provide logout functionality.

Authentication Manager Configuration

让我们看一看我们的自定义 AuthenticationProvider 配置。

Let’s take a look at our custom AuthenticationProvider configuration.

AuthenticationManagerBuilder authenticationManagerBuilder =
   http.getSharedObject(AuthenticationManagerBuilder.class);
WebAuthenticationProvider authProvider = new WebAuthenticationProvider(userDetailsService());
authenticationManagerBuilder.authenticationProvider(authProvider);
return authenticationManagerBuilder.build();

我们首先构建了一个 AuthenticationManagerBuilder 实例,并为它传递一个 authenticationProvider。WebAuthenticationProvider 实例被创建为一个带有 userDetailsService 对象的自定义验证提供程序。

We’ve first built a AuthenticationManagerBuilder instance and pass it a authenticationProvider. WebAuthenticationProvider instance is created as custom authentication provider with a userDetailsService object.

Controller Class

在这个类中,我们为该应用的首页为一个“/”端点创建了映射,这为了简单起见。这将重定向到 index.html。

In this class, we’ve created a mapping for single "/" endpoint for the index page of this application, for simplicity. This will redirect to index.html.

AuthController

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";
   }
}

Views

使用以下内容在 /src/main/resources/templates 文件夹中创建 index.html 以作为主页。

Create index.html in /src/main/resources/templates folder with following content to act as a home page.

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 World!</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 that browser is asking for username/password via system dialog.

Browser’s dialog for username/password

formlogin

Enter Invalid Credential

如果我们输入无效的凭证,则会再次弹出相同的对话框。

If we enter invalid credential, then same dialog will popup again.

authenticationprovider error

Home Page for User

如果我们为用户输入了有效的凭证,它将加载该用户的主页

If we enter valid credential for a User and it will load home page for User

basic authentication success