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 类具体进行实现,也就是基于内存的实现。