Spring Security 认证源码超详细分析

news2024/11/15 7:27:49

Spring Security 认证源码超详细分析

认证(Authentication)是系统确认用户信息的重要途径,用户通过认证之后,系统才能明确用户的身份,进而才可以为该用户分配一定的权限,这个过程也叫授权(Authorization)。认证也是进入 Spring Security 框架的第一步。

  • 搭建 Spring Security 入门案例
    (1)引入依赖:
<!--Spring Boot父工程-->
  <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>3.2.0</version>
  </parent>


  <dependencies>
      <!--Web场景启动器-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      <!--Spring Security场景启动器-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
  </dependencies>

(2)场景启动器:

package cn.xdf;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
@SpringBootApplication
public class Test01Application {
    public static void main(String[] args) {
        SpringApplication.run(Test01Application.class, args);
    }
}

(3)编写一个Controller:

package cn.xdf.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){
        return "Hello Spring Security!";
    }
}

在引入了Spring Security的场景启动器后,默认情况下所有的请求都将被Spring Security接管,都需要认证(登录)后才可以访问。
我们启动项目,访问:http://localhost:8080/hello,会发现请求被Spring Security拦截下来了,以下是Spring Security提供的登录页面,如图所示:


Spring Security提供的默认用户名为:user,默认密码会在Spring Boot项目启动时控制台中输出。
我们也可以在application.yml配置文件中对用户名和密码进行设定:

spring:
  security:
    user:
      name: xiaohui
      password: admin

3.1 认证架构组件分析

在 Spring Security 的认证整体架构中存在非常多的组件,这些组件在 Java 中都被一些接口或抽象类所描述,我们先了解这些组件的工作原理,然后学习它们的实现类有哪些,不同的实现类提供的功能又是如何。这样我们才能对 Spring Security 的认证整体流程有一个非常清晰的认识。

3.1.1 SecurityContextHolder

SecurityContextHolder 是 Spring Security 中保存认证信息(用户的身份信息)的核心组件,它负责管理当前请求的 Authentication 对象(保存用户信息),包括存储、清除等。如果 SecurityContextHolder 中存储了 Authentication 对象,那么 Spring Security 则认为当前请求已认证。
Authentication 对象是被包裹为 SecurityContext 对象存储在 SecurityContextHolder 中的,其关系如图所示:

下面的伪代码展示了 SecurityContext 的创建过程。

// 通过 SecurityContextHolder 创建 SecurityContext 对象
SecurityContext context = SecurityContextHolder.createEmptyContext(); 

// 创建用户认证信息
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 

// 将用户认证信息存入 SecurityContext 中
context.setAuthentication(authentication);

// 将 SecurityContext 设置到 SecurityContextHolder
SecurityContextHolder.setContext(context); 

当用户认证成功后,Authentication 对象先被包裹成 SecurityContext 对象然后存储在 SecurityContextHolder 中。然后 Spring Security 会将存储在 SecurityContextHolder 中的 SecurityContext 对象存入 Session 中,名为 SPRING_SECURITY_CONTEXT,最后将 SecurityContexHolder 中的数据清空。
我们可以编写一个接口,来查看 Session 中存储的 Security 上下文:

@RequestMapping("/showAuthentication")
public SecurityContext showAuthentication(HttpSession session){
    // 获取 session 中存储的 Security 上下文
    SecurityContext loginUser = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
    System.out.println(loginUser);

    return loginUser;
}

该接口响应的数据如下:

{
    "authentication": {				// 该 SecurityContext 保存的 authentication对象
        "authorities": [],		
        "details": {
            "remoteAddress": "0:0:0:0:0:0:0:1",
            "sessionId": "597B5AC2A6EC50541A74D0CE779588BE"
        },
        "authenticated": true,		
        "principal": {
            "password": null,
            "username": "xiaohui",
            "authorities": [],
            "accountNonExpired": true,
            "accountNonLocked": true,
            "credentialsNonExpired": true,
            "enabled": true
        },
        "credentials": null,
        "name": "xiaohui"
    }
}

用户认证成功之后,每当有请求到来时,Spring Security 就会先从 Session 中取出用户数据(SecurityContext),保存到 SecurityContextHolder 中,因为该请求后续还要经过 Spring Security 的其他过滤器,这些过滤器中需要通过 SecurityContextHolder 中的 SecurityContext 数据来判断用户的认证状态。同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后再通过 FilterChainProxy 将 SecurityContextHolder 中的数据清空。

Tips:默认情况下,SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起,这意味着 SecurityContext 对同一线程中的方法是可用的。
有些应用程序它们与线程的工作方式比较特殊,可能并不完全适合使用 ThreadLocal,那么可以在创建时用一个策略来配置 SecurityContextHolder 来指定如何存储 Security 上下文

3.1.2 AuthenticationManager

AuthenticationManager 接口是 Spring Security 负责认证的核心组件,它定义了处理认证请求的基本流程,用于接收一个封装了用户认证信息的 Authentication 对象(只包含用户名、密码等),并根据配置的认证策略对用户的身份进行验证。如果验证成功,它将返回一个已认证的 Authentication 对象(包含用户名、密码、权限、是否认证等完整信息)。
AuthenticationManager 接口如下:

public interface AuthenticationManager {
    // 对传递的身份验证对象(Authentication)进行身份验证,如果成功,则返回一个完全填充的身份验证对象(包括授予的权限)
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

关于 AuthenticationManager 的实现,在官方中有如下定义:
(1)如果帐户被禁用,并且AuthenticationManager可以测试此状态,则必须抛出DisabledException。
(2)如果帐户被锁定并且AuthenticationManager可以测试帐户锁定,则必须抛出LockedException。
(3)如果提供了不正确的凭据,必须抛出BadCredentialsException。
AuthenticationManager 的默认实现是 ProviderManager,它内部维护了一个 AuthenticationProvider 的列表。这个列表中的每一个 AuthenticationProvider 都代表了一种特定的身份认证方式,当需要进行身份认证时,ProviderManager 会遍历这个列表,依次调用每个 AuthenticationProvider 的 authenticate 方法进行认证尝试。
ProviderManager 本身也可以配置一个 AuthenticationManager 作为 parent。这种父子关系的设置允许在子 ProviderManager 认证失败时,将认证请求转发给父ProviderManager 进行再次认证。

(1)AuthenticationProvider

通常,在 AuthenticationManager 中会存在一个或多个 AuthenticationProvider,AuthenticationManager 通过委托的方式将具体的认证逻辑交给 AuthenticationProvider 实现,这样我们就可以为某个 AuthenticationManager 提供多种认证方式,即提供多个 AuthenticationProvider,每个 AuthenticationProvider 都执行一种特定类型的认证,Spring Security 官方也提供了非常多的 AuthenticationProvider 子类,如:

  • TestingAuthenticationProvider:用于测试的认证实现,它可以绕过实际的认证逻辑,直接返回一个预定义的 Authentication 对象。
  • RememberMeAuthenticationProvider:用于完成“记住我”功能的认证实现,允许用户在成功登录后,在一定时间内无需再次输入用户名和密码即可自动登录系统。
  • DaoAuthenticationProvider:用于用户名+密码的认证实现。通过集成数据库(如JDBC、JPA等)来实现用户认证。它依赖于UserDetailsService接口的实现类来从数据库中查询用户信息,并根据这些信息与用户提供的凭证(如用户名和密码)进行比对。

AuthenticationProvider 接口如下:

public interface AuthenticationProvider {
    /**
     *  对Authentication进行认证,如果身份验证失败,抛出:AuthenticationException。
     *  如过返回null,则代表当前AuthenticationProvider无法认证,继续使用下一个Provider进行认证
     */
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    // 传入该authentication的类型(字节码对象),返回当前Provider是否支持对该authentication对象
    boolean supports(Class<?> authentication);
}

使用 AuthenticationProvider 来对请求认证,这也是官方推荐的模式。但这并非是强制性约束,因为在AuthenticationManager 接口中只有一个方法,即:Authentication authenticate(Authentication authentication) ,用户只需要实现该方法并完成相应的功能,即:传递一个封装了认证信息的 Authentication 然后对该用户进行认证,最终返回一个已认证的 Authentication 即可。具体如何实现认证过程,是借助 AuthenticationProvider 来完成认证还是自行实现认证细节全部由用户自定义。
默认情况下,Spring Security 将使用 AuthenticationProvider 的子类 AbstractUserDetailsAuthenticationProvider 类来进行认证处理,该类重写了 authenticate(…) 方法,在该方法中调用了retrieveUser(…) 方法作为具体认证流程的方法。但 AbstractUserDetailsAuthenticationProvider 自身并没有对 retrieveUser(…) 方法实现。而是最终交给了它的子类——DaoAuthenticationProvider 类。
下列是 AbstractUserDetailsAuthenticationProvider 的伪代码:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = determineUsername(authentication);
        // 先从缓存中获取UserDetails对象
        UserDetails user = this.userCache.getUserFromCache(username);
        
        // 调用retrieveUser方法查询到一个UserDetails对象
        user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        // 根据user来创建Authentication对象
        return createSuccessAuthentication(principalToReturn, authentication, user);
}

// 抽象方法
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;

// 将UserDetails对象转为Authentication对象
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
    
    // 该Authentication对象是已经经过认证的(authenticated为true)
    UsernamePasswordAuthenticationToken result = 
        new UsernamePasswordAuthenticationToken(principal,authentication.getCredentials(),
                                                this.authoritiesMapper.mapAuthorities(user.getAuthorities())
                                               );
    result.setDetails(authentication.getDetails());
    return result;
}

DaoAuthenticationProvider 类对 retrieveUser(…) 方法做了实现,并交由 UserDetailsService 组件进行后续的认证流程处理。retrieveUser(…) 方法返回一个 UserDetails 对象(封装了用户信息),AbstractUserDetailsAuthenticationProvider 负责将 UserDetails 封装为一个合格的 Authentication 对象。然后请求回到 FilterChainProxy ,由 FilterChainProxy 处理后续流程,至此,Spring Security 认证流程结束。
下列是 DaoAuthenticationProvider 类的伪代码:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication){
    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    return loadedUser;
}
(2)UserDetailsService

UserDetailsService 作为 DaoAuthenticationProvider 的一个内部成员,用于加载用户信息并进行身份验证,通过用户名加载用户的详细信息,并将其封装为 UserDetails 对象返回,以供 Spring Security 进行后续的身份验证和授权操作。

Tips:需要注意的是,我们现在讨论的是 Spring Security 默认的认证流程。实际上,如果我们自定义了 AuthenticationManager 自身来实现认证流程,而非使用 AuthenticationProvider,那么 AuthenticationProvider、UserDetailsService 等组件将不会执行,除非我们自身使用到了这些组件。

UserDetailsService 提供了一个关键方法 loadUserByUsername(String username),通过用户名加载用户的详细信息,并将其封装为 UserDetails 对象返回,以供Spring Security 进行后续的身份验证和授权操作。
UserDetailsService 接口如下:

public interface UserDetailsService {
    // 根据用户名查询用户信息,该用户信息被封装为(UserDetails)
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService 只提供了根据用户名查询用户信息(UserDetails)方法,UserDetailsManager 作为 UserDetailsService 的子接口还提供了一些管理 UserDetails 的方法,UserDetailsManager 接口如下:

public interface UserDetailsManager extends UserDetailsService {
    // 创建UserDetails
    void createUser(UserDetails user);

    // 更新UserDetails
    void updateUser(UserDetails user);

    // 根据用户名删除UserDetails
    void deleteUser(String username);

    // 修改UserDetails密码
    void changePassword(String oldPassword, String newPassword);

    // 检查该UserDetails是否存在
    boolean userExists(String username);
}

大多数情况下,我们都是使用 UserDetailsManager 接口而非 UserDetailsService接口,Spring Security 的默认实现为 InMemoryUserDetailsManager,从名字我们也能看得出来,该类实现与 UserDetailsManager 接口。InMemoryUserDetailsManager 类将读取内存中的存储的用户信息与用户传递过来的用户信息进行比对。
InMemoryUserDetailsManager 如实现代码如下:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 根据用户名获取对应的UserDetails对象(该UserDetails对象存储在内存中,服务器启动时就已经加载)
    UserDetails user = this.users.get(username.toLowerCase());
    if (user == null) {
        throw new UsernameNotFoundException(username);
    }
    // 将用户名、密码、权限等所有信息封装成一个User对象(UserDetails)返回
    return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
                    user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
(3)UserDetails

UserDetails 用于表示用户信息,它封装了用户的认证信息,包括用户名、密码、权限等。这个接口用于表示用户的具体信息。AuthenticationProvider 调用 UserDetailsService 的 loadUserByUsername 方法来加载用户信息,并将用户信息封装为一个实现了 UserDetails 接口的对象返回。
UserDetails 接口如下:

public interface UserDetails extends Serializable {
    // 返回用户的权限(不能为null)
    Collection<? extends GrantedAuthority> getAuthorities();

    // 返回用户的密码
    String getPassword();

    // 返回用户名(用户名不能为null)
    String getUsername();

    // 返回用户帐号是否过期(过期的帐户无法进行身份验证)
    boolean isAccountNonExpired();

    // 返回处于锁定状态或未锁定状态(被锁定的用户无法进行认证)
    boolean isAccountNonLocked();

    // 指示用户的凭证(密码)是否已过期(过期凭据阻止身份验证)
    boolean isCredentialsNonExpired();

    // 返回用户是否启用或禁用(禁用的用户无法进行认证)
    boolean isEnabled();
}

UserDetails 接口的默认实现为 User 类,在 InMemoryUserDetailsManager 中读取了内存中存储的用户信息,并将用户信息封装为一个 User 对象返回。

3.1.3 Authentication

Authentication 是 Spring Security 用于封装用户认证相关的核心组件,它包含了用户的身份信息、认证状态以及用户所具有的权限信息。
Authentication 对象包含如下信息:

  • 身份信息:包含用户提交的凭据信息,如用户名和密码。我们可以通过 Authentication 对象获取该用户名和密码等信息。
  • 权限信息:包含用户的权限信息,即 GrantedAuthority 集合。这些权限信息用于决定用户可以访问哪些资源或执行哪些操作。
  • 认证状态:包含用户的认证状态,即用户是否已经被成功认证。这对 Spring Security 后续的执行流程影响非常重大。

Authentication 接口的源代码如下:

public interface Authentication extends Principal, Serializable {
    // 权限信息
    Collection<? extends GrantedAuthority> getAuthorities();
    // 密码
    Object getCredentials();
    // 有关认证的其他信息,由用户自行封装,默认为ip地址、sessionId
    Object getDetails();
    // 用户信息(用户名)
    Object getPrincipal();
    // 是否认证
    boolean isAuthenticated();
    // 设置认证状态
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

默认情况下,当 Spring Security 对用户进行认证时,首先会将用户的信息包装成一个 Authentication 对象,该对象仅包含一些基本信息(如用户名、密码等),之后 Spring Security 将交由 AuthenticationManager 中的 AuthenticationProvider 来对该 Authentication 进行认证处理,认证成功后将重置该对象的认证状态并为该对象赋值一些其他信息(如权限等信息),之后将该对象存入 SecurityContextHolder 中。
Authentication 接口在 Spring Security 中主要有两个作用。
(1)封装用户的信息:用于传递给 AuthenticationManager 进行认证处理。
(2)代表当前认证的用户:当用户认证成功后,Authentication 将存储我们认证的所有信息。

(1)Authentication-未认证

在 Spring Security 默认注册的16个过滤器中,其中有一个 UsernamePasswordAuthenticationFilter 过滤器,该过滤器主要负责处理基于表单的登录请求。当用户提交包含用户名和密码的表单时,该过滤器会拦截这些请求,并启动身份验证过程。
在 UsernamePasswordAuthenticationFilter 过滤器中,将表单提交的用户名和密码封装为了一个 UsernamePasswordAuthenticationToken 对象(Authentication的子类),该 Authentication 对象还未经过认证, 然后交由 AuthenticationManager 组件进行后续的认证流程。
UsernamePasswordAuthenticationFilter 的核心代码大致如下:

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    // 必须是post提交
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    // 获取表单的用户名
    String username = obtainUsername(request);
    username = (username != null) ? username : "";
    username = username.trim();
    // 获取表单的密码
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    
    // 将用户名和密码封装为一个Authentication对象,该Authentication是未经过认证的(authenticated为false)
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    // 设置Authentication的一些其他信息
    setDetails(request, authRequest);
    // 调用AuthenticationManager组件进行后续的认证操作,并返回一个已认证的Authentication对象
    return this.getAuthenticationManager().authenticate(authRequest);
}
(2)Authentication-已认证

Authentication 可以代表一个未认证的用户信息,也可以代表一个已经认证的用户信息。在 AbstractUserDetailsAuthenticationProvider 中,authenticate()方法用于获取一个 UserDetails 对象,并将 UserDetails 对象封装成一个已经通过认证的 Authentication 对象。

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = determineUsername(authentication);
        // 先从缓存中获取UserDetails对象
        UserDetails user = this.userCache.getUserFromCache(username);
        // 调用retrieveUser方法查询到一个UserDetails对象
        user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            // 用户名
            principalToReturn = user.getUsername();
        }
        // 根据UserDetails来创建Authentication对象
        return createSuccessAuthentication(principalToReturn, authentication, user);
}

// 抽象方法
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;

// 将UserDetails对象转为Authentication对象
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
    
    // 该Authentication对象是已经经过认证的(authenticated为true)
    UsernamePasswordAuthenticationToken result = 
        new UsernamePasswordAuthenticationToken(
                                                // 用户名
                                                principal,
                                                // 密码
                                                authentication.getCredentials(),	
                                                // 密码
                                                this.authoritiesMapper.mapAuthorities(user.getAuthorities())
                                               );
    // 设置Authentication的一些其他信息
    result.setDetails(authentication.getDetails());
    return result;
}
(3)GrantedAuthority

GrantedAuthority 是 Spring Security 用于存储用户权限的核心组件,通过 Authentication 的 getAuthorities() 方法可以获取到当前用户的所有 GrantedAuthority 实例,每个 GrantedAuthority 实例通常对应一个字符串值,例如 “ROLE_ADMIN” 或 “CAN_DELETE”,用于访问过程中的权限检查。
GrantedAuthority 接口信息如下:

public interface GrantedAuthority extends Serializable {
    // 返回权限字符串
    String getAuthority();
}

GrantedAuthority 是作为 Authentication 中的一部分存储在 Authentication 对象中的。因此,GrantedAuthority 自然也是由 AuthenticationProvider 的实例(AbstractUserDetailsAuthenticationProvider、DaoAuthenticationProvider、InMemoryUserDetailsManager等组件)进行封装。当用户信息验证成功后,AuthenticationProvider 会构造一个 Authentication 对象,并将用户的 GrantedAuthority 集合(权限信息)设置到该对象中。
GrantedAuthority 的默认实现为 SimpleGrantedAuthority。该类只有一个成员变量,用于存储该对象保存的字符串权限:

public final class SimpleGrantedAuthority implements GrantedAuthority {
    ...
    public SimpleGrantedAuthority(String role) {
        Assert.hasText(role, "A granted authority textual representation is required");
        this.role = role;
    }

    @Override
    public String getAuthority() {
        return this.role;
    }
    ...
}

3.1.4 AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 为处理认证请求提供了一个基础框架。这个抽象类主要用于处理 HTTP 请求,并将其转换为 Authentication 对象,然后提交给 AuthenticationManager 进行认证。这不就是我们前面介绍的 UsernamePasswordAuthenticationFilter 吗?没错,AbstractAuthenticationProcessingFilter 的默认实现就是 UsernamePasswordAuthenticationFilter 。
AbstractAuthenticationProcessingFilter 的工作流程如下:

  • 1)当用户提交他们的凭证时,AbstractAuthenticationProcessingFilter 将会调用 attemptAuthentication(…)方法来进行后续的认证流程,该方法为抽象方法,具体执行流程需要参考子类的实现,默认情况下的子类为:UsernamePasswordAuthenticationFilter,该类会从 HttpServletRequest 中获取用户的信息并以此来创建一个需要认证的 Authentication(该对象还未经过认证)。创建的认证的类型以及具体认证的流程取决于 AbstractAuthenticationProcessingFilter 的子类。
  • 2)一般情况下,Authentication 将被传入 AuthenticationManager,以进行认证(这取决于子类),返回一个已经认证成功的 Authentication 对象。
  • 3)如果认证失败,AbstractAuthenticationProcessingFilter 将会做如下操作:
    • (1)SecurityContextHolder 被清空。
    • (2)RememberMeServices.loginFail 被调用,将调用 RememberMeServices 组件进行后续操作。
    • (3)AuthenticationFailureHandler 被调用。将调用 AuthenticationFailureHandler 组件进行后续操作。
  • 4)如果认证成功,AbstractAuthenticationProcessingFilter 将会做如下操作:
    • (1)SessionAuthenticationStrategy 被通知有新的登录。将调用 SessionAuthenticationStrategy 组件进行后续操作。
    • (2)SecurityContextHolder 将创建 SecurityContext,并将 Authentication 存入 SecurityContext,最后再将 SecurityContext 设置到 SecurityContextHolder 上。
    • (3)RememberMeServices.loginSuccess 被调用。将调用 RememberMeServices 组件进行后续操作。
    • (4)ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent 事件。
    • (5)AuthenticationSuccessHandler 被调用。将调用 AuthenticationSuccessHandler 组件进行后续操作。

AbstractAuthenticationProcessingFilter 的核心源码大致如下:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    // 调用了自身的doFilter
    doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    // 判断该请求是否需要进行后续的认证流程
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }
    try {
        // 调用attemptAuthentication方法进行认证,认证后返回一个Authentication对象
        Authentication authenticationResult = attemptAuthentication(request, response);
        this.sessionStrategy.onAuthentication(authenticationResult, request, response);
        // 认证成功
        if (this.continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        // 执行认证成功的流程
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    catch (InternalAuthenticationServiceException failed) {
        // 认证过程中内部问题导致的失败(例如代码异常等)
        this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
        unsuccessfulAuthentication(request, response, failed);
    }
    catch (AuthenticationException ex) {
        // 认证失败(例如用户名、密码错误登)
        unsuccessfulAuthentication(request, response, ex);
    }
}

认证失败的大致流程:

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                          AuthenticationException failed) throws IOException, ServletException {
    // 清空SecurityContextHolder
    SecurityContextHolder.clearContext();
    // 调用RememberMeServices
    this.rememberMeServices.loginFail(request, response);
    // 调用AuthenticationFailureHandler 
    this.failureHandler.onAuthenticationFailure(request, response, failed);
}

认证成功的大致流程:

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                        Authentication authResult) throws IOException, ServletException {
    // 创建SecurityContext
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    // 将Authentication设置到SecurityContext
    context.setAuthentication(authResult);
    // 将SecurityContext设置到SecurityContextHolder
    SecurityContextHolder.setContext(context);
    // 调用RememberMeServices 
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        // 调用ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    // 调用AuthenticationSuccessHandler 
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
(1)AuthenticationSuccessHandler

AuthenticationSuccessHandler 用于处理认证成功之后执行的流程。当一个请求在 AbstractAuthenticationProcessingFilter#attemptAuthentication方法认证成功后,将执行 successfulAuthentication(…) 方法进行后续的流程。在该方法中,使用了 AuthenticationSuccessHandler 组件来进行后续的流程。
AuthenticationSuccessHandler 接口如下:

public interface AuthenticationSuccessHandler {
    // 进行后续的认证处理(该Authentication是已经通过认证的)
    default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authentication) throws IOException, ServletException {
        onAuthenticationSuccess(request, response, authentication);
        chain.doFilter(request, response);
    }
    // 进行后续的认证处理(该Authentication是已经通过认证的)
    void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException;
}

默认情况下该接口的实现类为 SavedRequestAwareAuthenticationSuccessHandler,该类能够处理认证成功后的重定向逻辑。当用户尝试访问一个需要认证的资源时,如果用户还没有认证,ExceptionTranslationFilter 会将原始请求保存起来,并重定向用户到登录页面。一旦用户成功登录,SavedRequestAwareAuthenticationSuccessHandler 就会根据之前保存的请求来决定用户应该被重定向到哪个页面。
SavedRequestAwareAuthenticationSuccessHandler 处理请求的大致代码如下:

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws ServletException, IOException {
    // 获取在ExceptionTranslationFilter中保存的请求对象
    SavedRequest savedRequest = this.requestCache.getRequest(request, response);
    if (savedRequest == null) {
        // 说明是直接访问login请求,并没有上一个请求(那就重定向到默认的地址"/")
        super.onAuthenticationSuccess(request, response, authentication);
        return;
    }
    // 是否有设置登录成功后重定向的页面
    String targetUrlParameter = getTargetUrlParameter();
    if (isAlwaysUseDefaultTargetUrl()
        || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
        this.requestCache.removeRequest(request, response);
        super.onAuthenticationSuccess(request, response, authentication);
        return;
    }
    clearAuthenticationAttributes(request);
    // 获取该对象中保存的路径
    String targetUrl = savedRequest.getRedirectUrl();
    // 重定向到该路径
    getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
(2)AuthenticationFailureHandler

AuthenticationFailureHandler 用于处理用户认证失败的流程。当一个请求在 AbstractAuthenticationProcessingFilter#attemptAuthentication方法认证失败后(例如用户名或密码错误等),将执行 unsuccessfulAuthentication(…) 方法进行后续的流程。在该方法中,使用了 AuthenticationFailureHandler 组件来进行后续的流程。
AuthenticationFailureHandler 接口如下:

public interface AuthenticationFailureHandler {
    // 对认证失败的请求做处理
    void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException;
}

默认情况下该接口的实现类为 SimpleUrlAuthenticationFailureHandler,该类能够处理认证失败后的重定向逻辑,该类会根据配置执行相应的操作,如重定向到错误页面或返回 HTTP 错误状态码。
SimpleUrlAuthenticationFailureHandler 处理请求的大致代码如下:

public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                    AuthenticationException exception) throws IOException, ServletException {
    if (this.defaultFailureUrl == null) {
        // 如果没有设置登录失败的重定向页面那就重定向到一个失败的页面
        response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        return;
    }
    // 将出现的异常保存起来(默认是保存到session中,key为:SPRING_SECURITY_LAST_EXCEPTION)
    saveException(request, exception);
    if (this.forwardToDestination) {
        this.logger.debug("Forwarding to " + this.defaultFailureUrl);
        request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
    }
    else {
        // 默认重定向到/login页面进行重新登录
        this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
    }
}

3.1.5 ExceptionTranslationFilter

ExceptionTranslationFilter 是 Spring Security 的核心过滤器中的其中一个,它位于过滤器链中的较后位置。其主要职责是处理认证和授权过程中抛出的异常,并将这些异常转化为适当的响应,例如重定向到登录页面或者返回一个 HTTP 状态码来表示访问被拒绝。
ExceptionTranslationFilter 过滤器主要的作用是针对认证和授权失败之后的处理:

  • 认证失败处理:当用户尝试访问受保护的资源但未通过认证时,ExceptionTranslationFilter 会捕获相应的认证异常(AuthenticationException),并根据配置决定如何响应,比如重定向到登录页面。
  • 授权失败处理:如果用户已经通过认证但没有足够的权限访问某些资源,ExceptionTranslationFilter 会捕获授权异常(AccessDeniedException),并采取适当的措施,如显示一个访问被拒绝的错误页面。

Tips:对于一般的认证异常(AuthenticationException),在 AbstractAuthenticationProcessingFilter#unsuccessfulAuthentication 已经处理了,ExceptionTranslationFilter 用户处理那些在认证过程中未处理的认证异常。

其代码大致如下:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    try {
        chain.doFilter(request, response);
    }
    catch (IOException ex) {
        throw ex;
    }
    catch (Exception ex) {
        // 获取认证/授权过程中的SpringSecurityException异常
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
        RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
            .getFirstThrowableOfType(AuthenticationException.class, causeChain);
        if (securityException == null) {
            securityException = (AccessDeniedException) this.throwableAnalyzer
                .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        }
        ...
        // 进行后续处理   
        handleSpringSecurityException(request, response, chain, securityException);
    }

}
// 处理SpringSecurityException异常
private void handleSpringSecurityException(HttpServletRequest request, 
                                           HttpServletResponse response,
                                           FilterChain chain, RuntimeException exception) 
    throws IOException, ServletException {
    
    if (exception instanceof AuthenticationException) {
        // 处理认证异常
        handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
    }
    else if (exception instanceof AccessDeniedException) {
        // 处理授权异常(权限不足)
        handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
    }
}

某个请求不具备当前资源的访问权限时,在后续的授权过程中就会出现异常,这个异常将被 ExceptionTranslationFilter 捕获到,它首先检查当前请求是否具备访问权限。如果没有,它会检查是否需要认证,并决定是否应该启动认证过程。handleAuthenticationException(…) 和 handleAccessDeniedException(…)方法中都调用了 sendStartAuthentication(…) 方法,该方法正是处理认证或授权失败的后续流程的核心逻辑,代码大致如下:

protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, 
                                       FilterChain chain, AuthenticationException reason) 
    throws ServletException, IOException {
    // 创建了一个SecurityContext
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    // 并将SecurityContext设置到SecurityContextHolder中
    SecurityContextHolder.setContext(context);
    // 将当前请求保存到requestCache中,等到认证成功后需要重定向到这个请求来
    this.requestCache.saveRequest(request, response);
    // 交由AuthenticationEntryPoint组件进行后续流程
    this.authenticationEntryPoint.commence(request, response, reason);
}
1)AuthenticationEntryPoint

AuthenticationEntryPoint 接口用于处理未认证的 HTTP 请求,通常由 ExceptionTranslationFilter 调用。当一个用户尝试访问受保护的资源但尚未通过认证时,Spring Security 会调用 AuthenticationEntryPoint 来处理这种情况。
AuthenticationEntryPoint 接口如下:

public interface AuthenticationEntryPoint {
    // 根据需要来做出响应给前端
    void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException;
}

Spring Security 默认采用 DelegatingAuthenticationEntryPoint 作为实现类来处理未登录的请求,在该类中存在一个 AuthenticationEntryPoint 的Map集合,key 为 RequestMatcher 类型,值为 AuthenticationEntryPoint,也就是说一个 RequestMatcher 对应一个 AuthenticationEntryPoint。实际上后续的逻辑将委派给这些 AuthenticationEntryPoint。
默认情况下最终将交由 LoginUrlAuthenticationEntryPoint 来处理未认证的 HTTP请求,该类将未认证的请求重定向到/login请求中。

2)RequestMatcher

RequestMatcher 接口用于对当前请求进行判断,之后返回一个布尔值。也就是说 RequestMatcher 代表一种特定的请求,AuthenticationEntryPoint 代表一种特定的处理方式。如默认情况下 Spring Security 加载两个 RequestMatcher ,分别为 AndRequestMatcher 和 OrRequestMatcher。
DelegatingAuthenticationEntryPoint 的核心代码大致如下:

// 自身的所有AuthenticationEntryPoint(每一种AuthenticationEntryPoint代表一种特定的处理方式)
private final LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints;

// 默认的
private AuthenticationEntryPoint defaultEntryPoint;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
                     AuthenticationException authException) throws IOException, ServletException {
    
    // 迭代所有的RequestMatcher,查看当前的RequestMatcher是否支持处理当前请求
    for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {
        // 判断当前的requestMatcher是否支持处理当前的请求,
        if (requestMatcher.matches(request)) {
            // 如果支持,则获取requestMatcher对应的AuthenticationEntryPoint
            AuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher);
            // 调用AuthenticationEntryPoint来处理后续流程
            entryPoint.commence(request, response, authException);
            return;
        }
    }
    // 没有合适的AuthenticationEntryPoint就采用默认的
    this.defaultEntryPoint.commence(request, response, authException);
}

3.2 对默认的认证源码分析

3.2.1 Spring Security 认证流程分析

了解完 Spring Security 中的重要组件后,我们开始对 Spring Security 的认证流程做一个整体的分析。Spring Security 认证流程图如下图所示。

(1)浏览器发送请求最先来到 AbstractAuthenticationProcessingFilter,将执行该过滤器的 doFilter(…)方法,在 doFilter(…)方法中调用了attemptAuthentication(…)方法。
(2)UsernamePasswordAuthenticationFilter 作为 AbstractAuthenticationProcessingFilter 的实现类,实现了attemptAuthentication(…)方法,并在方法中调用了AuthenticationManager 接口的 authenticate(…)方法。
(3)ProviderManager 作为 AuthenticationManager 的实现类,实现了 authenticate(…)方法。在方法中将任务委派给了 AuthenticationProvider 接口。
(4)AbstractUserDetailsAuthenticationProvider(抽象类)实现了 AuthenticationProvider 接口的 authenticate(…)方法,并在方法中调用了 retrieveUser(…)方法。
(5)DaoAuthenticationProvider 实现了 AbstractUserDetailsAuthenticationProvider 中的 retrieveUser(…)方法,并调用了 UserDetailsService 的 loadUserByUsername(…)方法。
(6)UserDetailsManager 作为 UserDetailsService 的子接口规范了许多管理用户信息方法,最终 InMemoryUserDetailsManager 实现了 UserDetailsManager 接口,重写了 loadUserByUsername(…)方法。

3.2.1 Spring Security 认证源码分析

(1)我们之前在 SpringBootWebSecurityConfiguration 配置类中,查看到该配置类往 SpringIOC 容器中注入了一个 SecurityFilterChain(实现类为DefaultSecurityFilterChain) ,并添加了一些默认配置,如下。

@Configuration(proxyBeanMethods = false)			// 标注当前是配置类
@ConditionalOnWebApplication(type = Type.SERVLET)	// 当前运行是是Servlet容器
class SpringBootWebSecurityConfiguration {
    // 如果当前IOC容器没有WebSecurityConfigurerAdapter和SecurityFilterChain对象才会生效
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        @Bean
        @Order(SecurityProperties.BASIC_AUTH_ORDER)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(			// 开启认证
                (requests) -> 
                requests.anyRequest()			// 所有请求
                .authenticated()				// 都必须认证
            );
            http.formLogin(withDefaults());		// 可以表单认证
            http.httpBasic(withDefaults());		// 可以httpBasic认证
            return http.build();				// 返回的是DefaultSecurityFilterChain对象
        }
    }
    ...
}

(2)我们点开 formLogin() 方法,在 FormLoginConfigurer 类中配置了 UsernamePasswordAuthenticationFilter 过滤器。

public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
    // 配置了一个FormLoginConfigurer类
    formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
    return HttpSecurity.this;
}

// 打开FormLoginConfigurer,发现创建了一个UsernamePasswordAuthenticationFilter过滤器
public FormLoginConfigurer() {
    super(new UsernamePasswordAuthenticationFilter(), null);
    usernameParameter("username");
    passwordParameter("password");
}

(3)UsernamePasswordAuthenticationFilter 并没有重写父类的 doFilter 方法,但是父类的 doFilter 中调用了 attemptAuthentication 方法,所以我们直接查看 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 即可。

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    // 获取提交的用户名
    String username = obtainUsername(request);
    username = (username != null) ? username : "";
    username = username.trim();
    // 获取提交的密码
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    
    // 创建了一个UsernamePasswordAuthenticationToken(用户凭证)
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);
    
    // 获取认证管理器,调用认证管理器中的 authenticate 方法进行认证
    return this.getAuthenticationManager().authenticate(authRequest);
}

(4)UsernamePasswordAuthenticationFilter 默认使用的认证管理器为 ProviderManager ,然后调用了 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法进行认证。

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        // 判断当前的AuthenticationProvider是否支持对当前Authentication认证。
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
            ....
            try {
                // 调用AuthenticationProvider的authenticate方法进行认证
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            ....
        }
        ....
    }

(5)在 AbstractUserDetailsAuthenticationProvider 类中调用了 retrieveUser 方法进行认证

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                        () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                                                       "Only UsernamePasswordAuthenticationToken is supported"));
    String username = determineUsername(authentication);
    boolean cacheWasUsed = true;
    // 先从内置的缓存里面查询用户信息
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;
        try {
            // 如果缓存为null,再调用后续的流程去查询用户信息
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException ex) {
            if (!this.hideUserNotFoundExceptions) {
                throw ex;
            }
            // 用户名错误则抛出BadCredentialsException异常,该异常是一个AuthenticationException类型的异常(说明认证失败)
            throw new BadCredentialsException(...);
        }
    }
    try {
        this.preAuthenticationChecks.check(user);
        /* 
            user: 系统里面查询到的用户信息
            authentication: 用户输入的(前端传递的)用户信息
            对比用户输入的和系统里面查询到的用户信息,如果对比失败则抛出BadCredentialsException异常
        */
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException ex) {
        if (!cacheWasUsed) {
            throw ex;
        }
        cacheWasUsed = false;
        // 如果认证失败,再认证一次,有可能上一次是缓存还没更新
        user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        this.preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    
    ...
    if (!cacheWasUsed) {
        // 将user存入缓存
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    // 根据user来创建一个Authentication
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

(6)最终来到了 DaoAuthenticationProvider 类调用该类的 retrieveUser方法,在方法中调用了 UserDetailsService 的 loadUserByUsername 方法进行认证:

@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
    ....
    try {
        // 调用 InMemoryUserDetailsManager 的loadUserByUsername方法进行认证
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }
    ....
}

(7)最终调用的是 InMemoryUserDetailsManager 类的 loadUserByUsername 方法

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 读取配置文件里面的用户信息
    UserDetails user = this.users.get(username.toLowerCase());
    if (user == null) {
        // 用户名错误则抛出一个异常,该异常继承与AuthenticationException,说明是一个认证异常
        throw new UsernameNotFoundException(username);
    }
    // 封装成一个 UserDetails
    return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
                    user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}

(8)在 DaoAuthenticationProvider 还提供了 additionalAuthenticationChecks(…) 方法,该方法用于对比用户的其他信息,如:密码。

protected void additionalAuthenticationChecks(UserDetails userDetails,
                                              UsernamePasswordAuthenticationToken authentication) 
    throws AuthenticationException {
    // 如果用户输入的密码为null则抛出BadCredentialsException异常
    if (authentication.getCredentials() == null) {
        throw new BadCredentialsException(this.messages.getMessage("..."));
    }
    // 获取用户输入的密码
    String presentedPassword = authentication.getCredentials().toString();
    // 将用户输入的密码与系统查询到的密码对比
    if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        // 如果不一致也抛出BadCredentialsException异常
        throw new BadCredentialsException(this.messages.getMessage("..."));
    }
}

3.3 自动配置类

2.2.1 SpringBootWebSecurityConfiguration

默认情况下,当 SpringBoot 工程启动后,在 SpringBootWebSecurityConfiguration 配置类中向 SpringIOC 容器中注入了一个 SecurityFilterChain,该 SecurityFilterChain 的默认实现为 DefaultSecurityFilterChain,源码如下:

@Configuration(proxyBeanMethods = false)			// 标注当前是配置类
@ConditionalOnWebApplication(type = Type.SERVLET)	// 当前运行是是Servlet容器
class SpringBootWebSecurityConfiguration {

    // 如果当前IOC容器没有SecurityFilterChain对象才会生效
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {

        @Bean
        @Order(SecurityProperties.BASIC_AUTH_ORDER)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests(			// 开启认证
                (requests) -> 
                requests.anyRequest()			// 所有请求
                .authenticated()				// 都必须认证
            );
            http.formLogin(withDefaults());		// 可以表单认证
            http.httpBasic(withDefaults());		// 可以httpBasic认证
            return http.build();				// 返回的是DefaultSecurityFilterChain对象
        }
    }

    @Configuration(proxyBeanMethods = false)
    // 当前IOC容器必须没有一个名为"springSecurityFilterChain"的Bean时才会生效
    @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
    @ConditionalOnClass(EnableWebSecurity.class)
    @EnableWebSecurity
    static class WebSecurityEnablerConfiguration {

    }
}

而在 WebSecurityConfiguration 配置类中,SpringBoot 会向 SpringIOC 容器中注入一个名字为 springSecurityFilerChain 的过滤器,该过滤器就是 FilterChainProxy 的实例对象,该 FilterChainProxy 中默认包含了一个 SecurityFilerChain(实现类为 DefaultSecurityFilterChain,该类就是在 SpringBootWebSecurityConfiguration 自动配置类中注入的那个类),该 SecurityFilerChain 中默认包含16个过滤器,这些过滤器也是 Spring Security 的核心过滤器。
我们可以翻开源码如图所示:

默认的 SecurityFilterChain 会接收客户端发送的所有请求并进入 Spring Security 的认证流程,这就是为什么在引入了 Spring Security 的场景启动器后,没有任何配置情况下,请求会被拦截的核心原因。

3.3.1 UserDetailServiceAutoConfigutation

关于 InMemoryUserDetailsManager 的配置在 UserDetailsServiceAutoConfiguration 自动配置类中,如下:

@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
        "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
        "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
@ConditionalOnBean(ObjectPostProcessor.class)
/*
    当IOC容器中没有AuthenticationManager、AuthenticationProvider、
    UserDetailsService、AuthenticationManagerResolver等Bean时才会生效
*/
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
        AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder")
public class UserDetailsServiceAutoConfiguration {

    // 默认的密码前缀
    private static final String NOOP_PASSWORD_PREFIX = "{noop}";
    private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
    private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(
        // 从IOC容器中获取SecurityProperties、ObjectProvider
            SecurityProperties properties,ObjectProvider<PasswordEncoder> passwordEncoder) {
        // 获取SecurityProperties中配置的用户信息
        SecurityProperties.User user = properties.getUser();
        // 获取SecurityProperties中配置的权限信息
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(User.withUsername(user.getName())
            .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
            .roles(StringUtils.toStringArray(roles))
            .build());
    }

    // 返回加密后的密码(前提有设置加密组件)
    private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.warn(...);
        }
        if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
            return password;
        }
        return NOOP_PASSWORD_PREFIX + password;
    }
}

SecurityProperties 的属性配置类代码如下:

// 该属性配置类的配置前缀
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
    .....
    private final Filter filter = new Filter();
    private final User user = new User();
    ....
    public static class User {
        // 默认的用户名
        private String name = "user";
        // 默认的密码
        private String password = UUID.randomUUID().toString();
        // 默认的权限(空)
        private List<String> roles = new ArrayList<>();
        private boolean passwordGenerated = true;
        ....
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2098789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智慧理财新纪元:蚂蚁集团“支小宝”智能理财助理深度体验与测评

2023年9月8日&#xff0c;蚂蚁集团宣布推出面向消费者的智能金融助理“支小宝2.0”&#xff0c;这是该公司首个基于金融大模型的应用产品。该产品在语言力、知识力、专业力和安全力方面展现出卓越性能&#xff0c;旨在为用户提供包括行情分析、持仓诊断、资产配置和投资教育陪伴…

MS SQL Server partition by 函数实战三 成绩排名

目录 需求 范例运行环境 视图样本设计 功能实现 基础数据展示 SQL语句 继续排序 小结 需求 假设有若干已更新考试成绩的考生&#xff0c;考试成绩包括总成绩、分项成绩1、分项成绩2&#xff0c;其它信息包括应聘岗位名称、姓名等信息。现希望根据总成绩计算排名&#…

C语言代码练习(第十一天)

今日练习&#xff1a; 25、用指针方法对10个整数按由大到小顺序排序 26、有一个班&#xff0c;3个学生&#xff0c;各学习4门课&#xff0c;计算总平均分数以及第 n 个学生的成绩 27、有一个3 * 4的二维数组&#xff0c;要求用指向元素的指针变量输出二维数组个元素的值。 用指…

mar3d模型文件问题

上一章使用mars3D模型库 遗留一个问题 部分资源不完整 如果模型没有其他依赖文件会正常加载 若有其他依赖就会报错 正常获取到的 缺少文件的 经过观察在gltf文件中发现缺失的是这几个文件 还是通过脚本下载 脚本实例 const fs require(fs); const path require(path); co…

Idea中修改Jsp文件的头部注释模版

文章目录 方法1&#xff0c;启动idea&#xff0c;单击“file”&#xff0c;选择“settings”2&#xff0c;选择Editor——File and Code Templates——other——Jsp files——jsp File.jsp。此时编辑如下图所示的右上区域即可修改模板。 每天学一个小技巧 方法 1&#xff0c;启…

FL Studio 24汉化破解版 v24.4.1.4285(附汉化破解补丁)

FL Studio 24.4.1.4285(汉化破解版是一款无比专业的音乐制作软件&#xff0c;它无论是在功能上还是用户界面都是在业内数一数二的。就拿它的用户界面来说&#xff0c;是目前同类软件中唯一做到100%矢量化&#xff0c;这样能更好地用在4K&#xff0c;5K甚至8K显示器上&#xff0…

力扣面试经典算法150题:Z 字形变换

Z 字形变换 今天的题目是力扣面试经典150题中的数组的中等难度题&#xff1a; Z 字形变换。 题目链接&#xff1a;https://leetcode.cn/problems/zigzag-conversion/description/?envTypestudy-plan-v2&envIdtop-interview-150 题目描述 将一个给定字符串 s 根据给定的行…

Jmeter 的 jar 包开发环境搭建

01 JDK环境变量配置 JAVA_HOMEE:\Program Files\Java\jdk1.8.0_102 Path%JAVA_HOME%\bin; CLASSPATH.;%JAVA_HOME%\lib\dt.jar;.;%JAVA_HOME%\lib\tools.jar 验证环境变量有没有生效&#xff1a;看到如下信息说明 JDK 环境变量已经生效 02 配置 maven 环境 1、下载地址&…

js中数组的定义及使用

数组的定义 Array 存储按位存储&#xff0c;紧密结构,在数组中查找元素&#xff0c;就必须要遍历数组&#xff0c;查找速度慢。 优点可以根据当前元素找到前一个或者后一个元素&#xff0c;可以排序 数组引用对象的是强引用&#xff0c;存储的是她的引用地址 length属性可读可…

TF | SD 卡出现无法删除的文件,乱码文件该如何处理 macOS

TF | SD 卡出现无法删除的文件&#xff0c;乱码文件该如何处理 macOS 一、问题描述 最近手头有张用在 Miyoo 掌机上的游戏 TF 卡&#xff0c;在macOS 系统下在回收站中出现了几个特殊文件名的文件&#xff0c;始终无法删除。 二、试着解决 我试过了网上的所有方法都无法删除…

rv1126-rv1109-mkcramfs-mkfs.cramfs-打包文件系统

事情是这样的: 定制了文件系统打包功能;然后我是根据这个指令 fakeroot mkfs.cramfs rootfs_glibc_rv1126/ rootfs.img mkfs.cramfs rootfs_glibc_rv1126/ rootfs.img 起因就是这个fakeroot; 不加的话打出来的rootfs.img是没有用户权限的 然后我根据fakeroot mkfs.cramfs ro…

米壳AI:跨境电商日本市场必备神器!

各位跨境电商小伙伴们&#xff0c;在开拓日本市场的过程中&#xff0c;产品图片的翻译问题是不是常常让你感到困扰呢&#xff1f; 别着急&#xff0c;今天就给大家推荐一个超棒的在线翻译工具--米壳AI&#xff0c;它必将成为你跨境之路上的得力助手&#xff0c;让你如虎添翼。…

网络-多路io

了 fcntl 函数来操作文件描述符的状态标志&#xff0c;其中主要是为了设置非阻塞模式。下面是对 fcntl 函数及其参数的详细解释&#xff1a; fcntl 函数 fcntl 是一个用于操作文件描述符的系统调用&#xff0c;可以用来设置或获取文件描述符的各种属性。其原型如下&#xff1…

Django学习实战篇二(适合略有基础的新手小白学习)(从0开发项目)

前言&#xff1a; 从这一章开始&#xff0c;我们来创建项目typeidea&#xff0c;我把它放到了GitHub上。强烈建议你也到GitHub上注册一个账号&#xff08;如果没有的话&#xff09;&#xff0c;然后创建这样的项目。当然&#xff0c;你也可以起一个属于自己的名称。这个项目就是…

tensorrt plugin

自定义plugin 流程 首先明确要开发的算子&#xff0c;最好是 CUDA 实现&#xff1b;继承 IPluginV2DynamicExt / IPluginV2IOExt类实现一个Plugin 类&#xff0c;在这里调用前面实现的算子&#xff1b;继承 IPluginCreator 类实现一个 PluginCreator 类&#xff0c;用于创建插…

【数据结构取经之路】布隆过滤器BloomFilter原理、误判率推导、代码实现

目录 背景介绍 简介 布隆过滤器的实现思路 布隆过滤器的作用 布隆过滤器误判率推导过程 布隆过滤器的实现 布隆过滤器的删除问题 布隆过滤器的优缺点 布隆过滤器的应用 背景介绍 在一些场景下面&#xff0c;有大量数据需要判断是否存在&#xff0c;而这些数据不是整…

物联网之MQTT

一&#xff0c;MQTT 及其在物联网中的应用 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的消息传输协议&#xff0c;设计用于低带宽、延迟高、不稳定的网络环境&#xff0c;特别适合物联网&#xff08;IoT&#xff09;应用。它采用了发布/订…

案例 | 稳石机器人赋能日化企业内部物流提质增效

近年来&#xff0c;日化产业高速发展&#xff0c;以“清洁类”及“化妆品类”为主的日化品在人们生活中扮演着不可或缺的角色。然而在发展过程中&#xff0c;诸多难点也开始显头&#xff0c;招工难用工贵、生产速度受到掣肘等难题&#xff0c;都对日化企业可持续发展构成挑战。…

智慧安防EasyCVR视频监控汇聚管理平台云端录像时间轴拖动不跳转,是什么原因?

视频汇聚EasyCVR视频智能管理系统以其强大的拓展性、灵活的部署方式、高性能的视频能力和智能化的分析能力&#xff0c;为各行各业的视频监控需求提供了优秀的解决方案。EasyCVR平台支持多种视频流的外部分发&#xff0c;如RTMP、RTSP、HTTP-FLV、WebSocket-FLV、HLS、WebRTC、…

国产SaaS的挑战与未来:探索用户增长的新路径

在数字化转型的浪潮中&#xff0c;SaaS&#xff08;软件即服务&#xff09;行业扮演着至关重要的角色&#xff0c;为企业提供了灵活、高效的数字化解决方案。然而&#xff0c;国产SaaS行业在快速发展的同时&#xff0c;也面临着诸多挑战&#xff0c;包括客户定制化需求高、市场…