SpringSecurity6从入门到实战之登录表单的提交

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
注册配置 MSE Nacos/ZooKeeper,118元/月
函数计算FC,每月15万CU 3个月
简介: SpringSecurity6 教程探讨了登录表单提交的源码流程。当提交登录表单时,`UsernamePasswordAuthenticationFilter` 负责处理认证,它从请求中获取 `username` 和 `password` 参数。然后,`AuthenticationManager` 的 `authenticate()` 方法被调用,进一步委托给 `AuthenticationProvider`,通常是 `DaoAuthenticationProvider`。`DaoAuthenticationProvider` 使用 `UserDetailsService`(如 `InMemo

SpringSecurity6从入门到实战之登录表单的提交

文接上回,当SpringSecurity帮我们生成了一个默认对象.本文继续对登录流程进行探索,我们如何通过账号密码进行表单的提交,SpringSecurity在这过程中又帮助我们做了什么

登录表单的提交的源码分析

在之前了解了为什么所有的请求都会进行认证操作,我们也直接把目光放到源码中这个地方defaultSecurityFilterChain()

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
class SpringBootWebSecurityConfiguration {
    SpringBootWebSecurityConfiguration() {
    }
    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnMissingBean(
        name = {"springSecurityFilterChain"}
    )
    @ConditionalOnClass({EnableWebSecurity.class})
    @EnableWebSecurity
    static class WebSecurityEnablerConfiguration {
        WebSecurityEnablerConfiguration() {
        }
    }
    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        SecurityFilterChainConfiguration() {
        }
        @Bean
        @Order(2147483642)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)http.authorizeHttpRequests().anyRequest()).authenticated();
            //这里就是进行表单登录的入口方法了
            http.formLogin();
            http.httpBasic();
            return (SecurityFilterChain)http.build();
        }
    }
}

我们进入formLogin()中继续看,可以看到这个方法formLogin().这里创建了一个FormLoginConfigurer,我们继续顺着这个构造方法进去看看

public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
    return getOrApply(new FormLoginConfigurer<>());
  }
public FormLoginConfigurer() {
    super(new UsernamePasswordAuthenticationFilter(), null);
    usernameParameter("username");
    passwordParameter("password");
  }

这里可以看到FormLoginConfigurer调用了父类构造并且传了一个UsernamePasswordAuthenticationFilter对象,之前有介绍过这个过滤器是专门做账号密码认证的,那我们继续看向这个过滤器new 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());
        } else {
            //通过过去用户名和密码进行非空判断
            String username = this.obtainUsername(request);
            username = username != null ? username.trim() : "";
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            //组装成一个UsernamePasswordAuthenticationToken令牌对象,方便传递
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
            this.setDetails(request, authRequest);
            //这里将令牌对象传入,继续看看authenticate()做了什么
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }


最终发现这个authenticate()是一个接口下的抽象方法,实际执行的是 AuthenticationManager 接口实现类 ProviderManager 中的 authenticate() 方法,在该方法中调用 AuthenticationProvider 接口的authenticate() 方法:

我们继续看:

可以发现这里传入了authentication对象最终返回的还是authentication对象,说明这里肯定为这个对象的其他属性进行了操作,我们继续看到provider.authenticate().

实际执行的是 AuthenticationProvider 接口实现类 AbstractUserDetailsAuthenticationProvider 中的 authenticate() 方法,在该方法中调用 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;
        //第一次从缓存中获取user对象,肯定是找不到的
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
      cacheWasUsed = false;
      try {
                //将输入的用户名和token对象传入retrieveUser()方法,最终返回了UserDetails
        user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (UsernameNotFoundException ex) {
        this.logger.debug("Failed to find user '" + username + "'");
        if (!this.hideUserNotFoundExceptions) {
          throw ex;
        }
        throw new BadCredentialsException(this.messages
          .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
      }
      Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }
    try {
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException ex) {
      if (!cacheWasUsed) {
        throw ex;
      }
      // There was a problem, so try again after checking
      // we're using latest data (i.e. not from the cache)
      cacheWasUsed = false;
      user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    }
    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
      principalToReturn = user.getUsername();
    }
    return createSuccessAuthentication(principalToReturn, authentication, user);
  }

向下查询retrieveUser(),由于retrieveUser()是抽象方法而当前类有且只有一个子类所以直接看到AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider中实现的retrieveUser()

通过username去加载用户,也是看到这个this.getUserDetailsService().loadUserByUsername().可以看到loadUserByUsername还是一个接口

package org.springframework.security.core.userdetails;
/**
 * Core interface which loads user-specific data.
 * <p>
 * It is used throughout the framework as a user DAO and is the strategy used by the
 * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
 * DaoAuthenticationProvider}.
 *
 * <p>
 * The interface requires only one read-only method, which simplifies support for new
 * data-access strategies.
 *
 * @author Ben Alex
 * @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
 * @see UserDetails
 */
public interface UserDetailsService {
  /**
   * Locates the user based on the username. In the actual implementation, the search
   * may possibly be case sensitive, or case insensitive depending on how the
   * implementation instance is configured. In this case, the <code>UserDetails</code>
   * object that comes back may have a username that is of a different case than what
   * was actually requested..
   * @param username the username identifying the user whose data is required.
   * @return a fully populated user record (never <code>null</code>)
   * @throws UsernameNotFoundException if the user could not be found or the user has no
   * GrantedAuthority
   */
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

实际执行的是 UserDetailsService 接口实现类 InMemoryUserDetailsManager 中的 loadUserByUsername() 方法,在该方法中会在 users 集合变量中根据用户输入的帐号获取 UserDetails 信息:

@Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserDetails user = this.users.get(username.toLowerCase());
    if (user == null) {
      throw new UsernameNotFoundException(username);
    }
    return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
        user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
  }

类 InMemoryUserDetailsManager 是由内存 map 支持的接口实现类,基于内存存储,不需要后端数据库

最终结论

总结:1. 默认用户名 user 和 控制台的密码,是在 SpringSecurity 提供的 User 类中定义生成的;

           2.在表单认证时,基于 InMemoryUserDetailsManager 类具体进行实现,也就是基于内存的实现。

相关文章
|
Java 数据安全/隐私保护 Spring
SpringSecurity6从入门到实战之SpringSecurity整合自动装配详解(源码级别讲解,耐心)
该内容是关于SpringSecurity在SpringBoot中的自动配置详解。文章首先提出问题:SpringSecurity如何在引入依赖后使所有请求需认证并显示登录表单。接着,它探讨了SpringBoot的自动装配原理,从@SpringBootApplication注解开始,深入到@EnableAutoConfiguration的实现,解释了如何通过扫描jar包中的.imports文件来加载自动配置类。特别提到了SpringBootWebSecurityConfiguration类,该类默认设置为对所有请求进行认证,并支持表单和Basic认证。
|
XML Java API
SpringBoot3.x日志生产最佳实践原来是这样!
SpringBoot3.x日志生产最佳实践原来是这样!
408 0
|
安全 Java 数据安全/隐私保护
Spring Security 6.x 一文快速搞懂配置原理
本文主要对整个Spring Security配置过程做一定的剖析,希望可以对学习Spring Sercurity框架的同学所有帮助。
579 5
Spring Security 6.x 一文快速搞懂配置原理
|
安全 Java Spring
SpringSecurity6从入门到实战之Filter过滤器回顾
该文主要介绍了SpringSecurity框架中的过滤器Filter,探讨了在没有SpringSecurity时如何检查用户登录状态以保护资源。文中通过流程图展示了过滤器在HTTP请求处理中的位置,并提供了官方和中文文档链接。过滤器需实现Filter接口,用于拦截请求并进行预处理和后处理,例如强制登录检查。过滤器链FilterChain则是一系列Filter和资源的组合,通过doFilter方法逐个调用下一个过滤器或传递到目标资源。
|
存储 安全 Java
SpringSecurity6从入门到实战之初始用户如何存储到内存
Spring Security 在 SpringBoot 应用中默认使用 `UserDetailsServiceAutoConfiguration` 类将用户信息存储到内存中。当classpath有`AuthenticationManager`、存在`ObjectPostProcessor`实例且无特定安全bean时,此配置生效。`inMemoryUserDetailsManager()`方法创建内存用户,通过`UserDetails`对象填充`InMemoryUserDetailsManager`的内部map。若要持久化到数据库,需自定义`UserDetailsService`接口实
|
前端开发 Java
SpringSecurity6从入门到实战之默认登录页面的生成
本文介绍了SpringSecurity在SpringBoot项目中如何自动生成默认登录页面的过程。当访问如`/hello`的受保护路由时,请求会经过多个过滤器。在AuthorizationFilter中,未认证的请求会被拦截并抛出AccessDeniedException。接着,ExceptionTranslationFilter捕获此异常并启动身份验证,调用LoginUrlAuthenticationEntryPoint的commence方法,重定向到/login。DefaultLoginPageGeneratingFilter拦截/login请求,生成并返回默认的登录页面。
|
Java 数据安全/隐私保护 Spring
SpringSecurity6从入门到实战之SpringSecurity快速入门
这篇文章是关于使用SpringSecurity 6进行快速入门的教程。首先介绍了所需的环境配置,包括SpringSecurity 6.0.8、SpringBoot 3.0.12和JDK 17。接着,通过步骤展示了如何创建一个新的SpringBoot工程,并添加Web支持。然后,运行工程并测试了Hello接口,确保其正常工作。之后,引入SpringSecurity依赖后,无需额外配置,系统即实现了基础的认证功能,自动重定向到登录页面。文章通过截图详细说明了这个过程,包括控制台日志、登录页面以及登录后的资源访问。
|
8月前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
519 1
SpringBoot入门(7)- 配置热部署devtools工具
|
8月前
|
Web App开发 前端开发 JavaScript
揭秘!前端大牛们如何巧妙利用CSS3,打造炫酷视觉效果!
【10月更文挑战第31天】前端开发面临复杂布局的挑战,本文介绍了几种提升开发效率和代码质量的工具和技术。基础的HTML和CSS可以应对大部分布局需求,而Firefox开发者工具、VS Code、Vue、React等则能应对更复杂的布局,帮助开发者构建高性能、用户友好的网页应用。
194 4