前面章节我们介绍了如何通过 Spring Security 集成数据库实现登录认证,以及自定义登录页面、登录请求参数、成功处理、失败处理,以及 remember me 记住我等功能。
下面将跟踪 Spring Security 认证流程源码来分析它,让我们更深入的了解 Spring Security。
(1)用户在浏览器中随意输入一个 URL 地址,如:http://localhost:8080/hello
(2)Spring Security 会判断当前是否已经被认证(登录):
如果已经认证,正常访问URL。
如果没有被认证跳转到 loginPage() 对应的 URL 中,即登录页面。
上图中,访问 /hello 时返回了 302,重定向到了 /login 地址。
(3)用户输入用户名和密码点击“登录”按钮,发起登录请求。
(4)如果发起登录的 url 和 loginProcessingUrl() 返回的 url 一致,则执行登录流程。否则,需要重新认证。
(5)执行登录流程时首先被 UsernamePasswordAuthenticationFilter 进行过滤,取出用户名和密码,放入到容器中。如下图:
上图中,将根据 usernameParameter 和 passwordParameter 进行取用户名和密码,如果没有配置这两个方法,默认为请求参数名 username 和 password。如下图:
(6)执行自定义登录逻辑 UserDetailsService 的实现类。判断用户名是否存在和数据库中,如果不存在,直接抛出 UsernameNotFoundException。如果用户名存在,把从数据库中查询出来的密码通过org.springframework.security.core.userdetails.User 传递给 Spring Security。Spring Security 根据容器中配置的 Password encoder 示例把客户端传递过来的密码和数据库传递过来的密码进行匹配。如果匹配成功表示认证成功。
下图,尝试对传入的身份验证对象 Authentication 进行身份验证,如果成功,则返回一个完整填充的身份验证对象 Authentication(包括授予的权限) :
进入 ObservationAuthenticationManager 类的 authenticate() 方法,处理用户认证请求并返回认证结果:
跟踪进入 AbstractUserDetailsAuthenticationProvider 抽象类的 authenticate(Authentication authentication) 方法,调用 retrieveUser() 方法,该方法允许子类从特定位置获取UserDetails:它允许子类从特定于实现的位置实际检索UserDetails。并且,如果提供的凭据不正确,子类可以选择立即抛出AuthenticationException异常。这在需要以用户身份绑定到某个资源以获取或生成UserDetails的情况下特别有用。例如,在某些系统中,可能需要根据用户提供的凭证连接到数据库或其他资源来获取用户详细信息,如果凭证不正确,就直接抛出异常:
继续进入 DaoAuthenticationProvider 类的 retrieveUser() 方法,调用自定义 CustomUserDetailsService 类的 loadUserByUsername() 方法,根据用户名获取用户信息,以及用户权限信息:
进入到自定义的 CustomUserDetailsService 类的 loadUserByUsername() 方法:
到这里,用户信息和权限就获取完成了。如果用户名不存在,直接抛出 UsernameNotFoundException 异常。
(7)如果登录成功,跳转到 successForwardUrl(转发)/ successHandler(自己控制跳转方式)/ defaultSuccessUrl (重定向) 配置的 URL。如果登录失败,跳转到 failureForwardUrl / failureHandler / failureUrl 配置的地址。
继续,进入到 AbstractUserDetailsAuthenticationProvider 抽象类的 authenticate() 方法,this.preAuthentication().check() 进行用户校验:
采用默认的 DefaultPreAuthenticationChecks(前置检查),校验用户账号是否被锁定,是否启用,是否过期,如下:
进入 DaoAuthenticationProvider 中,在 additionalAuthenticationChecks() 方法中匹配用户密码是否匹配:
注意,使用前端表单传递的明文密码和我们返回 UserDetails 的密文,使用 PasswordEncoder 进行匹配:
如果密码匹配成功,则继续进行后续的检查:
使用默认 DefaultPostAuthenticationChecks 检查用户凭证是否过期:
如果未过期,则继续进入 AbstractAuthenticationProcessingFilter 抽象过滤器的 successfulAuthentication() 方法,方法最后一行调用了我们自定义的登陆成功处理器。如下:
进入到自定义的登陆成功处理器:
跳转到 /view/success 页面,整个流程就完成了。
下图是认证流程图: