Spring Security密码认证源码级讲解

一、前言

本文根据我的项目进行Security密码认证的源码级别讲解,我们将通过localhost:9090访问来开始进行Debug说明,我已经在源码中打了很多个端点,基本能讲到Security用户名密码认证的全部流程,主要是给自己加深印象,其次分享给大家,如果讲解过程中有什么错误,也请大家不吝指正,谢谢!代码基于springboot2.2.1、security5、jdk8、mysql8.0、maven构建。

二、debug启动springboot应用

1、由于我们在MvcConfig配置文件中进行如下配置,所以访问localhost:9090会跳转home.html

/**
 * @author yunqing
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        //注意:这里配置了 / 跳转home.html页面
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}

1.1、application.yml中配置了端口为9090

server:
  port: 9090

1.2、我在WebSecurityConfig中配置了不需要认证就可以访问的页面,其中包含 / 和/home

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // 所有用户均可访问的资源
                .antMatchers("/css/**", "/js/**","/images/**", "/webjars/**", "**/favicon.ico", "/index").permitAll()
                .antMatchers(HttpMethod.POST, "/user/registration").permitAll()
                .antMatchers("/", "/home","/user/registration","/hello").permitAll()
                //剩下的任何请求都需要进行认证
                .anyRequest().authenticated()
                .and()
                //表单登录
                .formLogin()
                //登录请求页面
                .loginPage("/login")
                //自定义登录成功和失败处理器
                .successHandler(ajaxAuthSuccessHandler)
                .failureHandler(ajaxAuthFailHandler)
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

2、看完上面的基本介绍,接下来我们进入了第一个断点

image.png

可以看到进了断点停在了抽象类AbstractAuthenticationToken,并且权限信息传入ROLE_ANONYMOUS参数,匿名角色,按下一步发现进入了如下AnonymousAuthenticationToken类,可以发现是anonymousUser匿名用户封装了一个匿名认证的Token,通过this.setAuthenticated(true0)设置认证通过。

image.png

所以得出结论:WebSecurityConfig中我们设置不需认证的资源或者路径,实际上Security使用匿名用户进行认证访问。

2.1、这里扩展一下spring security过滤器链

image.png

如上图所示:SecurityContextPersistenceFilter过滤器位于过滤器链的最前端,请求先进这个过滤器,当请求进入,检查session中是否有securityContext,如果有则拿出来放到线程中,请求响应回来最后一个也经过此过滤器,检查线程中是否有SecurityContext,如果有则拿出来存到Session中。这样就可以完成认证结果在多个请求之间共享。

2.2、通过获取共享在多个请求之间的用户信息

/**
 * @author yunqing
 * @Date 2019/12/15 16:44
 */
@Slf4j
@RestController
@RequestMapping("/api/account")
public class SecurityController {

    @GetMapping("/me")
    public Object getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

记得在SecurityConfig中设置/api/account/me不需要认证,即匿名登录。

image.png

2.3、获取当前认证用户信息

直接跳过断点localhost:9090访问到 home.html页

image.png

访问localhost:9090/api/account/me获取当前认证用户信息,可以看到当前确实是匿名用户

#返回结果,证明当前认证成功的是匿名用户

{"authorities":[{"authority":"ROLE_ANONYMOUS"}],"details":
{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"45C0AA46462B773A0606D24C91D70722"},
"authenticated":true,"principal":"anonymousUser","keyHash":431445726,"credentials":"",
"name":"anonymousUser"}

3、正式开始讲解数据库中的用户认证

image.png

3.1、点击Sign In登录跳转到第一个断点UsernamePasswordAuthenticationFilter

image.png

断点停到UsernamePasswordAuthenticationFilter,判断当前请求是否是POST请求→获取表单提交的用户密码→根据用户名密码构造一个UsernamePasswordAuthenticationToken→进入我们发现两个构造函数如下图

image.png

可以发现走的第一个两个参数的构造函数,因为并没有传入authorities角色信息,通过this.setAuthenticated(false)设置未经过认证→返回未认证的token到UsernamePasswordAuthenticationFilter

image.png

通过this.setDetails(request,authRequest);设置ip、sessionId等信息到UsernamePasswordAuthenticationToken中,如下图所示:

image.png

返回UsernamePasswordAuthenticationToken到this.getAuthenticationManager().authenticate()进行处理,如下图所示:

image.png

之后进入ProviderManager中的断点,介绍一下,ProviderManager实现了AuthenticationManager接口,可以看到如下图,var8是一个Providers集合,循环遍历这个集合,找到适合处理UsernamePassword的Provider进行处理用户名密码认证,可以看到当前是处理匿名用户的AnonymousAuthenticationProvider,继续下一步

image.png

循环直到当前Provider是DaoAuthenticationProvider这个是专门处理用户名密码认证的Provider,然后执行到result = provider.authenticate(authentication);这个断点的时候,会进入到AbstractUserDetailsAuthenticationProvider 抽象类,它实现了 AuthenticationProvider接口,而DaoAuthenticationProvider又继承了这个抽象类。

image.png

接下来重点讲解DaoAuthenticationProvider和AbstractUserDetailsAuthenticationProvider 这两个类,主要的认证方法写在了这个抽象类中,如下图断点中this.retrieveUser()方法获取到了一个UserDetails实例,这个this.retrieveUser()方法也是一个抽象方法,他的实现写在了DaoAuthenticationProvider中。

image.png
image.png

接下来看this.retrieveUser()方法的实现,终于在实现中看到了调用loadUserByUsername()方法。


image.png

可以看到确实进入了我们自定义的MyUserDetailsService类,这个类就不多解释了,从数据库里取数据验证而已

image.png

回到AbstractUserDetailsAuthenticationProvider可以看到三个检查,三个检查的实现就在本类中,可以进行查看

image.png

preAuthenticationCheck检查

image.png

this.additionalAuthenticationChecks密码检查,实现类在DaoAuthenticationProvider中

image.png

this.postAuthenticationChecks()还是对UserDetails接口中剩下的一个布尔值进行检查

image.png

所有检查通过之后,认为认证成功,拿着认证成功的这些信息进入this.createSuccessAuthentication()

image.png
image.png

可以看到走的确实是三个参数的构造函数,如下图,通过super.setAuthenticated(true)设置认证状态为成功

image.png

然后DaoAuthenticationProvider返回一个认证成功的Authentication,经过认证的Authentication会沿着认证的流程返回去,一直返回到UsernamepasswordAuthenticationFilter.

image.png
image.png
image.png

接下来就调用认证成功处理器进行处理,认证信息设置到线程中,用与session共享认证结果

image.png
image.png

登陆成功

image.png

你可能感兴趣的:(Spring Security密码认证源码级讲解)