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

本文涉及的产品
云原生网关 MSE Higress,422元/月
应用实时监控服务-应用监控,每月50GB免费额度
可观测监控 Prometheus 版,每月50GB免费额度
简介: 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 类具体进行实现,也就是基于内存的实现。

相关文章
|
7月前
|
前端开发 安全 Java
SpringBoot 实现登录验证码(附集成SpringSecurity)
SpringBoot 实现登录验证码(附集成SpringSecurity)
438 0
|
26天前
|
安全 Java 数据安全/隐私保护
如何使用Spring Boot进行表单登录身份验证:从基础到实践
如何使用Spring Boot进行表单登录身份验证:从基础到实践
43 5
|
6月前
|
Java 数据安全/隐私保护 Spring
SpringSecurity6从入门到实战之默认用户的生成流程
该文档介绍了SpringSecurity6中默认用户的生成流程。在`SecurityAutoConfiguration`源码中,通过`SecurityProperties`配置类,系统默认创建了一个名为&quot;user&quot;的用户,其密码是一个随机生成的UUID。这个用户是在没有在`application.properties`中设置相关配置时自动创建的。
|
4月前
|
缓存 前端开发 小程序
SpringBoot+uniapp实现网页授权获取用户基本信息
本文详细介绍了微信网页授权登录的实现过程,作者原本计划在一个项目中使用该功能,但由于公众号未认证而放弃。文中分享了如何通过测试公众号实现网页授权并获取用户基本信息,包括配置授权回调域名、获取access_token及刷新等步骤。适合初次接触或希望回顾此流程的读者。此外,还提供了常见错误解决方案,强调了正确配置回调域名的重要性,并提醒避免重复使用已失效的code。通过本文,读者可以更好地理解和应用微信网页授权。
68 0
SpringBoot+uniapp实现网页授权获取用户基本信息
|
4月前
ElementUI——表单使用自定义验证导致无法提交
ElementUI——表单使用自定义验证导致无法提交
32 2
|
6月前
|
前端开发 Java
SpringSecurity6从入门到实战之默认登录页面的生成
本文介绍了SpringSecurity在SpringBoot项目中如何自动生成默认登录页面的过程。当访问如`/hello`的受保护路由时,请求会经过多个过滤器。在AuthorizationFilter中,未认证的请求会被拦截并抛出AccessDeniedException。接着,ExceptionTranslationFilter捕获此异常并启动身份验证,调用LoginUrlAuthenticationEntryPoint的commence方法,重定向到/login。DefaultLoginPageGeneratingFilter拦截/login请求,生成并返回默认的登录页面。
|
6月前
|
前端开发 JavaScript 程序员
程序员必知:完成登录与注册页面的前端
程序员必知:完成登录与注册页面的前端
26 0
|
安全 Java 数据库
SpringSecurity-4-认证流程源码解析
SpringSecurity-4-认证流程源码解析
83 0
|
安全 Java 数据库连接
四.SpringSecurity基础-自定义登录流程
SpringSecurity基础-自定义登录流程
|
JSON 安全 搜索推荐
​SpringSecurity-5-自定义登录验证
​SpringSecurity-5-自定义登录验证
170 0