SpringSecurity认证的执行流程【完整流程图在文章结尾】

SpringSecurity认证的执行流程

在Spring Security 中,与认证、授权相关的校验其实都是利用一系列的过滤器来完成的,这些过滤器共同组成了一个过滤器链,如下图所示:
在这里插入图片描述

当开启SpringSecurity的debug模式时,可以看到控制台打印出来的各个过滤器执行的顺序:

@EnableWebSecurity(debug=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
1、 WebAsyncManagerIntegrationFilter;
2、 SecurityContextPersistenceFilter;
3、 HeaderWriterFilter;
4、 CsrfFilter;
5、 LogoutFilter;
6、 UsernamePasswordAuthenticationFilter;
7、 DefaultLoginPageGeneratingFilter;
8、 DefaultLogoutPageGeneratingFilter;
9、 RequestCacheAwareFilter;
10、 SecurityContextHolderAwareRequestFilter;
11、 AnonymousAuthenticationFilter:如果之前的认证机制都没有更新SecurityContextHolder拥有的Authentication,那么一个AnonymousAuthenticationToken将会设给SecurityContextHolder;
12、 SessionManagementFilter;
13、 ExceptionTranslationFilter:用于处理在FilterChain范围内抛出的AccessDeniedException和AuthenticationException,并把它们转换为对应的Http错误码返回或者跳转到对应的页面;
14、 FilterSecurityInterceptor:负责保护WebURI,并且在访问被拒绝时抛出异常;

在上面图中所展示的一系列的过滤器中,和认证授权直接相关的过滤器是 AbstractAuthenticationProcessingFilter 和 UsernamePasswordAuthenticationFilter

但是你可能又会问,怎么没看到 AbstractAuthenticationProcessingFilter 这个过滤器呢?这是因为它是一个抽象的父类,其内部定义了认证处理的过程,UsernamePasswordAuthenticationFilter 就继承自 AbstractAuthenticationProcessingFilter

接下来详细介绍下这两个过滤器

AbstractAuthenticationProcessingFilter

在AbstractAuthenticationProcessingFilter 中定义了认证处理过程,具体如下:
在这里插入图片描述

由上图可知,在执行过滤器的时候会对请求进行校验requiresAuthentication(),如果校验通过后才会执行具体的认证逻辑。那这里校验的什么?这里先不继续说,后续会再次介绍。

当校验通过后,则会执行具体具体的认证逻辑attemptAuthentication,该方法是个抽象方法,具体的实现逻辑都是在子类中实现的,也就是UsernamePasswordAuthenticationFilter.

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter的一个子类,认证与授权的相关校验均在此类中定义的。具体实现如下:

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	// ……

    // ~ Constructors
    // ===================================================================================================

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(usernameParameter);
    }

    protected void setDetails(HttpServletRequest request,
                              UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

	// ......其他略......
}

通过上述代码可以分析出如下结论

  1. 从构造方法中得知 该过滤器只针对**post请求的/login接口生效**, 在上面曾经说过在执行attemptAuthentication,之前会先调用requiresAuthentication()进行请求校验,校验的规则就是根据该构造器来决定的。
  2. 过滤器中,再利用obtainUsername和obtainPassword方法,提取出请求里边的用户名/密码,提取方式就是request.getParameter,这也是为什么SpringSecurity中默认的表单登录要通过key/value的形式传递参数,而不能传递JSON参数如果像传递JSON参数,我们可以通过修改这里的代码来进行实现;
  3. 获取到请求里传递来的用户名/密码之后,接下来会构造一个UsernamePasswordAuthenticationToken对象传入username和password其中username对应了UsernamePasswordAuthenticationToken中的principal属性,而password则对应了它的credentials属性;
  4. 接下来再利用setDetails方法给details属性赋值,UsernamePasswordAuthenticationToken本身是没有details属性的,这个属性是在它的父类AbstractAuthenticationToken中定义的details是一个对象,这个对象里边存放的是WebAuthenticationDetails实例,该实例主要描述了请求的remoteAddress以及请求的sessionId这两个信息;
  5. 最后一步,就是利用AuthenticationManager对象来调用authenticate()方法去做认证校验
AuthenticationManager与ProviderManager

在上面 UsernamePasswordAuthenticationFilter类的 attemptAuthentication() 方法中得知,该方法的最后一步会进行关于认证的校验,而要进行认证操作首先要获取到一个 AuthenticationManager 对象,这里默认拿到的是AuthenticationManager的子类ProviderManager ,如下图所示:
在这里插入图片描述

进入到 ProviderManager 的 authenticate()方法中,来看看认证到底是怎么实现的。比较重要的

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

以上四张截图就是ProviderManager认证相关的核心逻辑
Spring Security中关于认证的重要逻辑几乎都是在这里完成的

  1. 首先利用反射,获取到要认证的authentication对象的Class字节码
  2. 判断当前provider是否支持该authentication对象
    • 如果当前provider不支持该 authentication 对象,则退出当前判断,进行下一次判断。
    • 如果支持,则调用provider的authenticate方法开始做校验,校验完成后,会返回一个新的Authentication
  3. 这里如果 provider 的 authenticate 方法没能返回一个 Authentication 认证对象,则会调用 provider 的 parent 对象中的 authenticate 方法继续校验。
  4. 而如果通过了校验,返回了一个Authentication认证对象,则调用copyDetails()方法把旧Token的details属性拷贝到新的Token中,如下图;
  5. 接下来会调用eraseCredentials()方法来擦除凭证信息,也就是我们的密码,这个擦除方法比较简单,就是将Token中的credentials属性置空;
  6. 最后通过publishAuthenticationSuccess()方法将认证成功的事件广播出去;
  7. SpringSecurity会监听这个事件,接收到这个Authentication对象,进而调用SecurityContextHolder.getContext().setAuthentication(…)方法,将AuthenticationManager返回的Authentication对象,存储在当前的SecurityContext对象中;
AuthenticationProvider

AuthenticationManager是负责管理协调认证工作的,但并不负责认证功能的真正实现,认证功能的真正实现是由 AuthenticationManager 中引用的 AuthenticationProvider的子类来完成的,通过源码我们可以看到AuthenticationManager 中引用了一个providers列表

private List<AuthenticationProvider> providers = Collections.emptyList();

providers集合的泛型是*AuthenticationProvider接口,*AuthenticationProvider接口有多个实现子类,如下图:

在这里插入图片描述

DaoAuthenticationProvider类关系

在这里插入图片描述

AuthenticationProvider接口的一个直接子类是AbstractUserDetailsAuthenticationProvider,该类又有一个直接子类DaoAuthenticationProvider。平常我们使用用户名密码登录时,默认就是使用该Provider。
Spring Security中默认就是使用DaoAuthenticationProvider来实现基于数据库模型认证授权工作的!

DaoAuthenticationProvider 在进行认证的时候,需要调用 UserDetailsService 对象的loadUserByUsername() 方法来获取用户信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证方式,可以实现自己的 AuthenticationProvider;如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。

DaoAuthenticationProvider类中并没有重写 authenticate() 方法authenticate() 方法是在父类AbstractUserDetailsAuthenticationProvider中实现的

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		......

		//获取authentication中存储的用户名
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

    	//判断是否使用了缓存
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
                //retrieveUser()是一个抽象方法,由子类DaoAuthenticationProvider来实现,用于根据用户名查询用户。
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				......
		}

		try {
            //进行必要的认证前和额外认证的检查
			preAuthenticationChecks.check(user);
            
            //这是抽象方法,由子类DaoAuthenticationProvider来实现,用于进行密码对比
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				//在发生异常时,尝试着从缓存中进行对象的加载
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}
		
        //认证后的检查操作
		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		
        //认证成功后,封装认证对象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

查看源码我们可以知道大概的逻辑

  1. 首先会从Authentication提取出登录用户名

  2. 然后利用得到的username,先去缓存中查询是否有该用户

  3. 如果缓存中没有该用户,则去执行 retrieveUser() 方法获取当前用户对象。而这个retrieveUser()方法是个抽象方法,在AbstractUserDetailsAuthenticationProvider类中并没有实现,是由子类DaoAuthenticationProvider来实现的
    在这里插入图片描述

  4. DaoAuthenticationProvider类的retrieveUser()方法中,会调用getUserDetailsService()方法,得到UserDetailsService对象,执行我们自己在登录时候编写的loadUserByUsername()方法,然后返回一个UserDetails对象,也就是我们的登录对象如下图所示;
    在这里插入图片描述

  5. 接下来会继续往下执行preAuthenticationChecks.check()方法,检验user中各账户属性是否正常,例如账户是否被禁用、是否被锁定、是否过期等

  6. 接着会继续往下执行additionalAuthenticationChecks()方法,进行密码比对。而该方法也是抽象方法,也是由子类DaoAuthenticationProvider进行实现。我们在注册用户时对密码加密之后,Spring Security就是在这里进行密码比对的******。如下所示。

在这里插入图片描述

  1. 然后在postAuthenticationChecks.check()方法中检查密码是否过期,如下所示;
  2. 然后判断是否进行了缓存,如果未进行缓存,则执行缓存操作,这个缓存是由SpringCacheBasedUserCache类来实现的;
    • 我们这里如果没有对缓存做配置,则会执行默认的缓存配置操作。如果我们对缓存进行了自定义的配置,比如配置了RedisCache,就可以把对象缓存到redis中。
  3. 最后通过createSuccessAuthentication()方法构建出一个新的 UsernamePasswordAuthenticationToken对象

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值