SpringSecurity(五):认证流程

Spring Security整个流程如上图所示,下面我们分别介绍一下各个组件

AbstracAuthenticationProcessingFilter

AbstracAuthenticationProcessingFilter是一个抽象类,如果使用账号密码的方式登录,AbstracAuthenticationProcessingFilter的实现类就是UsernamePasswordAuthenticationFilter,这个抽象类会构造出一个Authentication对象,UsernamePasswordAuthenticationFilter构造出的是UsernamePasswordAuthenticationToken

我们以UsernamePasswordAuthenticationFilter为例,看下登录的流程

(1)用户提交登陆请求时,UsernamePasswordAuthenticationFilter会从HttpServletRequest中取出登录名和密码,创建出一个UsernamePasswordAuthenticationToken对象
(2)UsernamePasswordAuthenticationToken对象被传入ProviderManager进行认证
(3)认证失败则会清除SecurityContextHolder相关信息,进行认证失败处理
(4)认证成功则进行登录信息的存储,认证成功跳转等。

AuthenticationManager

AuthenticationManager是一个认证管理器,它定义了Spring Security如何执行认证操作,如果认证成功后,AuthenticationManager会返回一个Authentication对象设置到SecurityContextHolder中。AuthenticationManager是一个接口,它的默认实现类也是我们使用最多的实现类是ProviderManager

ProviderManager

在Spring Security中由于系统可能同时支持多种认证方式,,不同的认证方式对应不同的AuthenticationProvider,,因此在ProviderManager中存在一个AuthenticationProvider列表,ProviderManager会对列表中每一个AuthenticationProvider执行认证,最终得到认证结果,ProviderManager也可以配置一个parent,可以使任意类型的AuthenticationProvider,当列表中所有的AuthenticationProvider均认证不通过是,由parent认证。但通常都是由ProviderManager自己扮演parent

我们重点看一下authenticate方法

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
                //首先获取authentication的类型
		Class<? extends Authentication> toTest = authentication.getClass();
                //当前认证的异常
		AuthenticationException lastException = null;
                //parent认证的异常
		AuthenticationException parentException = null;
                //当前认证的结果
		Authentication result = null;
               //parent认证的结果
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
                //遍历所有provider
		for (AuthenticationProvider provider : getProviders()) {
                        //如果该AuthenticationProvider不支持该authentication类型
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {   
                                //获得认证结果
				result = provider.authenticate(authentication);

				if (result != null) {
                                        //给authentication的details属性赋值
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}
                //没有认证结果 且parent认证器不为空

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}
                //如果结果不为空,则把result中的凭证擦除,防止泄露
		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an 
			// AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager 
			// already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish 
		// an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent 
		// AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

接下来我们来理解下AuthenticationProvider

AuthenticationProvider就是对不同的身份类型执行不同的认证方法,源码如下:

public interface AuthenticationProvider {
	// 具体认证流程
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;	
    // supports函数用来指明该Provider是否适用于该类型的认证,如果不合适,则寻找另一个Provider进行验证处理。	
	boolean supports(Class<?> authentication);
}

当使用账号密码的方式登录时,实现类就是DaoAuthenticationProvider,DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider,具体方法比较多,我们就不一一分析了,但有一个比较有意思的点

在获取用户对象时


protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }
    catch (UsernameNotFoundException ex) {
        mitigateAgainstTimingAttack(authentication);
        throw ex;
    }
    catch (InternalAuthenticationServiceException ex) {
        throw ex;
    }
    catch (Exception ex) {
        throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    }
}

会调用prepareTimingAttackProtection这个方法对常量USER_NOT_FOUND_PASSWPRD进行加密并保存,如果没有找到用户名则调用 mitigateAgainstTimingAttack方法将这个加密结果与用户传来的密码进行匹配。


private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
    if (authentication.getCredentials() != null) {
        String presentedPassword = authentication.getCredentials().toString();
        this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
    }

}

为什么都不存在这个用户名还需要比对密码?这不是注定失败吗?
主要是为了防止旁道攻击,在spring security中默认是隐藏了用户名不存在的异常(在AbstractUserDetailsAuthenticationProvider中有一个hideUserFoundException属性,默认为true)。当我们用户名不存在也进行密码检验时,用户就不能根据请求耗费的时长来判断该用户名是否存在了!

AbstractAuthenticationProcessingFilter

作为过滤器链的一环,AbstractAuthenticationProcessingFilter可以用来处理任何提交给它的身份认证。

AbstractAuthenticationProcessingFilter是一个抽象类,如果使用用户名/密码的方式登陆,那它对应的实现类就是UsernamePasswordAuthenticationFilter,构造出来的Authentication对象就是UsernamePasswordAuthenticationToken。至于AuthenticationManager,前面已经说过了一般情况下是ProviderManager

我们来梳理下大致流程:
(1)当用户提交登录请求时,UsernamePasswordAuthenticationFilter会从当前请求HttpServletRequest中提取出登录用户名/密码。然后创建出一个UsernamePasswordAuthenticationToken对象
(2)UsernamePasswordAuthenticationToken对象将会被传入ProviderManager中进行认证操作
(3)如果认证失败,则SecurityContextHolder中相关信息将被清除,登陆失败回调也会被调用
(4)如果认证成功,则会进行登录信息存储,Session并发处理,登陆成功事件发布以及登陆成功方法回调等

我们看下AbstractAuthenticationProcessingFilter的源码

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
    	// 判断该filter是否能处理该次请求,即请求的路径和该filter配置要处理的url是否match
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		Authentication authResult;
		try {
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				//没有得到认证结果,表明子类实现中无法处理该类型的认证
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		// 认证成功后,通过设置属性值 continueChainBeforeSuccessfulAuthentication
    	// 可以跳过认证成功后逻辑的处理
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
    	//认证成功后处理
		successfulAuthentication(request, response, chain, authResult);
	}

protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();
    	// 子类可以配置 rememberMeServices
		rememberMeServices.loginFail(request, response);
    	// 子类可以配置 failureHandler
		failureHandler.onAuthenticationFailure(request, response, failed);
	}


protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		SecurityContextHolder.getContext().setAuthentication(authResult);
		// 子类可以配置 rememberMeServices
		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event 出发成功事件,如果有订阅者,则可以接受到该事件
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		// 子类可以配置 successHandler
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

我们可以看到关键的代码在调用attemptAuthentication方法来获取一个经过认证后的Authentication对象,该方法是一个抽象方法,具体实现在子类中。

我们接下来看下UsernamePasswordAuthenticationFilter源码


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);
}

(1)通过obtainUsername和obtainPassword获取请求中的用户名和密码字段,这两个方法实际就是request.getParameter方法,注意默认用户名字段为username,密码字段为password
(2)拿到用户名传来的账号密码后,构造出一个UsernamePasswordAuthenticationToken,然后调用getAuthenticationManager().authenticate方法进行认证,进入到我们前面说的ProviderManager的流程中了

posted @   刚刚好。  阅读(1122)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示