Bootstrap

Spring Security执行原理流程

基本配置使用

(1)创建配置类

创建一个配置类SecurityConfig继承自WebSecurityConfigurerAdapter,重写里面的configure(HttpSecurity http)这个方法,配置好需要认证的登录url,以及提交表单的url,这里除了登录url不需要认证之外,其他的url都需要认证才能访问,并且formLogin表名这是一个表单提交,loginprocessingUrl中是设置的提交登录表单的url。

@Override
    protected void configure(HttpSecurity http) throws Exception {           
                .formLogin()
                .loginPage("/authentication/require")   //指定没有认证时跳转到的认证url
                .loginProcessingUrl("/authentication/form")   //提交登录表单的url
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require").permitAll()              
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable();
    }

(2)获取认证信息

我们需要继承一个UserDetailSevice接口并且加入到容器中,实现loadUserByUsername方法,里面的逻辑通常是从数据库查找出对应用户名的密码然后构造一个UserDetail对象,spring security会根据返回的这个带有正确用户信息的对象和前台传过来的用户名密码进行比对来判断是否认证通过。

/**
 * 表单登录的时候会调用loadUserByUsername来验证前端传过来的账号密码是否正确
 */
@Component
public class MyUserDetailService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(MyUserDetailService.class);

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("登录用户名===========" + username);
        //这里需要去数据库查询用户的账号密码来比对是否正确,以及账号是否过期等等
        String password = passwordEncoder.encode("123456");
        logger.info("数据库密码是==============" + password);
        return new User(username, password,
                true,
                true,
                true,
                true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

这样配置好的话,比如说我们访问一个一个http://localhost:8080/user的接口,那么spring security 发现配置中这个接口是需要拦截的,并且当前的请求还没有通过认证,就会重定向到loginPage设置的这个接口或者页面中去。当我们调用loginProcessingUrl这个接口去提交表单的时候,如果通过了认证,那么就会重定向到原来我们想要访问的接口中去了,如果认证不成功,那么就出现认证失败等信息。那么对于这个过程,spring security内部的流程到底是怎样的尼?我们先通过一张图来看。

我们可以看到在spring security内部其实是通过一个过滤器链来实现认证流程的,比如说这里的UsernamePasswordAuthenticationFilter就是拦截我们通过表单提交接口提交的用户名和密码,如果是Basic提交的话,就会被BasicAuthenticationFilter拦截,最后的橙色FilterSecurityInterceptor是首先判断我们当前请求的url是否需要认证,如果需要认证,那么就看当前请求是否已经认证,是的话就放行到我们要访问的接口,否则重定向到认证页面。

认证流程

这里已表单提交为例,当我们提交表单时,UsernamePasswordAuthenticationFilter首先会拦截请求,而UsernamePasswordAuthenticationFilter是继承于AbstractAuthenticationProcessingFilter的,在这个抽象类中已经定义好了doFilter的方法,而里面有一个attemptAuthentication方法是由子类实现的。所以当提交表单时spring security会发现这个一个表单提交,然后就调用了UsernamePasswordAuthenticationFilter的doFilter方法

然后我们来看 UsernamePasswordAuthenticationFilter里面重写的这个方法

然后我们可以看到首先是创建了一个UsernamePasswordAuthenticationToken对象,把用户名和密码传进去,再调用了getAuthenticationManager().authentication()方法,并把 UsernamePasswordAuthenticationToken对象传进去,那么这个AuthenticationManager是什么尼,其实它是一个接口

我们这里要看的是它的实现类ProviderManager,进去找到它的authenticate方法

其实这个ProviderManager就是管理所有的Provider对象的,通过for循环遍历找到适合的provider对象来调用其authenticate方法,那么provider对象又是什么东西?其实它也是一个接口

可以看到它的实现类有很多,我们这里主要来看DaoAuthenticationProvider这个实现类,但是在这个类中并没有找到authenticate方法,那么我们来它的父类AbstractUserDetailAuthenticationProvider里面来看下

里面调用了一个子类实现的方法,我们看这个子类的这个方法l

里面拿到我们之前加入到容器中的userDetailService然后调用loadUserByUsername方法拿到我们包含数据库用户信息的UserDetail对象,并且捕获用户名找不到等异常,所以这个方法就是来获取我们之前定义的userDetailService返回的UserDetail。

回到其父类中去,父类拿到了UserDetail对象,对这个对象进行了一系列的判断

 首先我们来看下前置判断

里面就是调用了UserDetail对象一些实现方法进行判断,比如说账户是否锁定,账户是否能用,账户是否过期的判断,判断通过后来到了密码判断

在判断了密码是否正确之后,后置判断就是来判断密码是否过期了,

前置判断判断账户异常问题,然后到密码判断,最后到密码是否过期判断,因为你登录先看你账户是否有异常才能,有异常的话就不进行密码判断了,密码正确也要看密码是够过期 ,所以这三个操作合乎常理。

判断都没问题之后,最后返回了一个authentication对象,这里把用户名,密码,权限都传到UsernamePasswordAuthenticationToken中去,因为所有判断都没问题,这些信息都是正确的了。

认证结果如何在多个线程中共享?

首先,我们还是先来看一张图

当请求过来的时候都会最先经过一个叫SecurityContextPersistenceFilter的过滤器,这个过滤器的作用就是在请求经过的时候检查session中是否有认证的authentication对象,如果有的话就把它放进一个叫securityContext ,然后再交给securityContextHolder处理

看一下securityContext

它是一个接口,由子类SecurityContextImpl实现,里面就是一个authentication属性,所以它的作用就是包装了authentication

接下来再看一下securityContextHolder

里面的getContext,clearContext方法都是使用的一个叫 SecurityContextHolderStrategy对象,点进去看一下这个对象

我们发现它又是一个接口,我们看它的实现类ThreadLocalSecurityContextHolderStrategy

里面只有一个ThreadLocal对象,放的是SecurityContext,我们知道ThreadLocal有线程隔离的作用,每一个对象在ThreadLocal中都是线程级别的。

接着我们回到securityContextpersistenceFilter中去看里面的doFilter方法

有一个loadContext方法是加载出来SecurityContext对象的,那么它是怎么加载的?点进去

它是一个接口,看它的实现类HttpSessionSecurityContextRepository

所以上面请求经过securityContextpersistenceFilter就是先从判断session是否有SecurityContext,有的话就放进当前线程中

那么响应的时候做什么?这里直接给出

 那么认证成功后,是谁把认证对象放在放在当前请求中的?答案就是FilterSecurityInterceptor,FilterSecurityInterceptor会判断当前请求的url是否要拦截,要拦截的话如果当前用户没有经过认证,那么就跳转到认证页面,如果认证了就直接放行,并且把认证信息放在当前线程中。

总结

一个请求过来时通过各个过滤器,最后通过FilterSecurityInterceptor来判断这个请求url是否是不需要验证的,如果是就直接访问到我们的接口api,如果不是的话,再判断当前请求线程中是否有authentication的认证对象,如果有就放行,如果没有就返回登录页面(比如这里我们设置的是登录表单的方式),来到登录页面输入账号密码登录后就会来到 UsernamePasswordAuthenticationFilter,经过一系列的操作,最后验证成功就会把认证对象authentication放进securityContext中,然后FilterSecurityInterceptor判断到当前请求线程中这个认证对象就放行,返回的时候最后会通过securityContextpersistenceFilter,判断当前线程是否有securityContext,如果有就放进session,那么下次再请求这个url的时候会首先通过securityContextpersistenceFilter这个过滤器,判断session中是否有securityContextduxiiang,如果有就放进当前请求线程中,然后最后经过FilterSecurityInterceptor时再判断当前请求线程是否有认证对象,由于最前面经过securityContextpersistenceFilter,已经从session中把认证对象放进了当前请求线程中,所以FilterSecurityInterceptor会直接放行,这样就访问到我们的接口api

;