Spring Security 简明教程

Spring Security - Authentication Provider

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

Spring Security Architecture

components of spring security architecture

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

AuthenticationFilter

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

AuthenticationManager

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

AuthenticationProvider

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

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

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

  1. DaoAuthenticationProvider − 此提供程序用于提供基于数据库的认证。

  2. LdapAuthenticationProvider − 此提供程序专门用于基于 LDAP(轻量级目录访问协议)的认证。

  3. OpenIDAuthenticationProvider − 此提供程序用于基于 OpenID 的认证,可与 Google/Facebook 等 OpenID 认证提供程序配合使用。

  4. JwtAuthenticationProvider − 对于基于 JWT(Java Web 令牌)的认证,我们可以使用 JwtAuthenticationProvider 类。

  5. RememberMeAuthenticationProvider − 此类用于基于用户记住我的令牌对用户进行认证。

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

UserDetailsService

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

PasswordEncoder

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

Spring Security Context

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

Custom Authenticator

我们可以通过实现 AuthenticationProvider 接口来创建一个自定义认证器。AuthenticatorProvider 接口有两个方法 authenticate()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。

supports() method

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

Spring Security Configuration

在 AuthenticationManager 中使用在 AuthenticationProvider 中创建的认证器并将其标记为托管 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();
}

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

在开始使用 Spring 框架编写第一个示例之前,你必须确保按照 Spring Security - Environment Setup 章节中的说明,正确地设置你的 Spring 环境。我们还假设你对 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>

Authenticator Provider

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

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 让我们能很轻松地配置应用。

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

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

  1. 首先,我们将通过使用 userDetailsService() 方法创建 UserDetailsService 类的 bean。我们将使用此 bean 来管理该应用的用户。在此,为了让操作简单,我们将使用 InMemoryUserDetailsManager 实例来创建用户。这些用户连同我们给定的用户名和密码分别被映射到用户和管理员角色。

Http Security Configuration

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

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

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

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

  1. 我们禁用了 csrfCross-Site Request Forgery 保护。因为这只是出于演示目的的一个简单应用,我们现在可以安全地禁用它。

  2. 然后我们添加需要验证所有请求的配置。

  3. 在之后,我们使用 Spring Security 的 formLogin() 功能如上所述。这使得浏览器可以要求一个用户名/密码的默认登录表单和一个提供注销功能的 logout()。

Authentication Manager Configuration

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

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

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

Controller Class

在这个类中,我们为该应用的首页为一个“/”端点创建了映射,这为了简单起见。这将重定向到 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 以作为主页。

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

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

Output

现在打开 localhost:8080,您会看到浏览器通过系统对话框询问用户名/密码。

Browser’s dialog for username/password

formlogin

Enter Invalid Credential

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

authenticationprovider error

Home Page for User

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

basic authentication success