Spring Security身份认证、权限验证、异常处理、自动登录流程整理

SpringSecurity是一系列filter,我们看功能实现,看你具体Filter就可以了。
我们需要关注的过滤器大致有4个:

1、UsernamePasswordAuthenticationFilter

在类说明我们可以看到如下内容:

  • 该过滤器处理表单提交的身份验证
  • 使用该过滤器需要两个参数:用户名和密码,默认参数名称包含在静态字段SPRING_SECURITY_FORM_USERNAME_KEY和SPRING_SECURITY_FORM_PASSWORD_KEY 。 也可以在配置文件设置usernameParameter和passwordParameter属性来更改参数名称。
  • 默认情况下,此过滤器只响应URL /login

doFilter()方法: 当前方法为父类AbstractAuthenticationProcessingFilter实现,我们可以将doFilter()方法划分下面4个部分:

	private void doFilter(HttpServletRequest request, HttpServletResponse response, 
                          FilterChain chain) throws IOException, ServletException {
        // 1. 判断当前路径是否需要响应
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
        
		try {
            // 2. 认证过程
			Authentication authenticationResult = attemptAuthentication(request, response);
			...
            // 3. 认证成功
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (AuthenticationException ex) {
			...
            // 4. 认证失败
			unsuccessfulAuthentication(request, response, failed);
		}
	}

示意图如下:

在这里插入图片描述

1、判断当前路径是否响应

我们可以从如下代码看到当前请求是否响应就是调用一个RequestMatcher进行匹配,RequestMatcher在父类中只有引用,没有实现,在子类中才有默认的RequestMatcher实现。

public abstract class AbstractAuthenticationProcessingFilter {
    
	private RequestMatcher requiresAuthenticationRequestMatcher;
    
	protected boolean requiresAuthentication(HttpServletRequest request, 
                                              HttpServletResponse response) {
		if (this.requiresAuthenticationRequestMatcher.matches(request)) {
			return true;
		}
		return false;
	}
}

子类:UsernamePasswordAuthenticationFilter

public class UsernamePasswordAuthenticationFilter{
    
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = 
        new AntPathRequestMatcher("/login","POST");
    
    public UsernamePasswordAuthenticationFilter() {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

}

我们可以通过自定义子类或者配置文件的方式来修改匹配路径,例如formLogin().loginProcessingUrl("登录路径")

// 这个authFilter会指向UsernamePasswordFilter	
public T loginProcessingUrl(String loginProcessingUrl) {
		this.loginProcessingUrl = loginProcessingUrl;
		this.authFilter.setRequiresAuthenticationRequestMatcher(
            			createLoginProcessingUrlMatcher(loginProcessingUrl));
		return getSelf();
	}

示意图如下:

在这里插入图片描述

2、认证过程

方法由子类实现

public class UsernamePasswordAuthenticationFilter{
	public Authentication attemptAuthentication(HttpServletRequest request, 
                            HttpServletResponse response)throws AuthenticationException { 
        // 1. 从子类中获取参数值:request.getParameter(this.passwordParameter)
		String username = obtainUsername(request);
		...
		String password = obtainPassword(request);
		...
        // 2. 根据用户名和密码生成Token    
		UsernamePasswordAuthenticationToken authRequest = 
            			new UsernamePasswordAuthenticationToken(username, password);
		setDetails(request, authRequest);
        // 3. 进行下一步认证
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

UsernamePasswordAuthenticationToken我们可以把它看成有如下5个参数类。

public class UsernamePasswordAuthenticationToken {

	private final Object principal;

	private Object credentials;
    // GrantedAuthority可以简单的认为String类型
    private final Collection<GrantedAuthority> authorities;

	private Object details;

	private boolean authenticated = false;
}

this.getAuthenticationManager().authenticate(authRequest)会跳转到如下类ProviderManager

public class ProviderManager implements AuthenticationManager{
    // 存放AuthenticationProvider的集合
    private List<AuthenticationProvider> providers = Collections.emptyList();
    
    public Authentication authenticate(Authentication authentication) 
        										throws AuthenticationException {
        // 1. 获取authentication的class对象
		Class<? extends Authentication> toTest = authentication.getClass();
        // 2. 遍历集合
		for (AuthenticationProvider provider : getProviders()) {
            // 判断当前认证器是否支持,判断的方式就是当前authentication是否为适当的子类实现类。
			if (!provider.supports(toTest)) {
				continue;
			}
			// 进入认证流程
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
        // 调用parent的authnticate()方法
		if (result == null && this.parent != null) {
			
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
        // 返回结果
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication 
                	&& (result instanceof CredentialsContainer)) {
				((CredentialsContainer) result).eraseCredentials();
			}
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

	}
}
    

authenticate()方法流程如下:

  • 获取当前authentication的class对象
  • 遍历authenticationProvider的集合,先判断是否支持该authentication,后再进行认证
  • 如果result = null且parent != null,那就调用parent的authenticate()方法,parent也是一个ProviderManager。
  • 如果result != null,清空密码后(credenticals = null)返回。

判断当前认证器是否支持的方法,从下面的方法看出来,还是比较简单的。

	public boolean supports(Class<?> authentication) {
		return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
	}
	public boolean supports(Class<?> authentication) {
		return (RememberMeAuthenticationToken.class.isAssignableFrom(authentication));
	}
	public boolean supports(Class<?> authentication) {
	    return (UsernamePasswordAuthenticationToken.class
                				.isAssignableFrom(authentication));
	}

流程细节如下:

在这里插入图片描述

AuthenticationProvider的认证方法:

如果我们不去主动添加,常见的认证器有三种:AnonymousAuthenticationProviderRememberMeAuthenticationProviderDaoAuthenticationProvider

我们主要来看下第三种:DaoAuthenticationProvider,把多余的代码去掉只下面这样

	public Authentication authenticate(Authentication authentication) 
        										throws AuthenticationException {
		// 1. 获取用户名
		String username = determineUsername(authentication);
        ...
        // 2. 获取信息,本质上就是调用UserDetailsService.loadUserByUsername(username)
		user = retrieveUser(username, (UsernamePasswordAuthenticationToken) 
		...
        // 3. 提交上来的信息与查询得到的信息进行对比             
        additionalAuthenticationChecks(user, 
                                (UsernamePasswordAuthenticationToken) authentication);
        ...                    
        // 4. 创建一个Token                    
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

示意图如下:

在这里插入图片描述

认证过程就是这样的!!!还是比较简单的。。。。

3、认证成功

还是从doFilter()方法开始看起,下面来

	successfulAuthentication(request, response, chain, authenticationResult);

	protected void successfulAuthentication(HttpServletRequest request, 
                             HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
        // 1. 保存authentication
		SecurityContextHolder.getContext().setAuthentication(authResult);
		// 2. rememberMeServices的操作
		this.rememberMeServices.loginSuccess(request, response, authResult);
		// 3. 继续调用认证成功处理器
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
	}

1、保存authentication

SecurityContextHolder.getContext().setAuthentication(authResult);

我们根据源码向下找,SecurityContextHolder的默认配置是ThreadLocalSecurityContextHolderStrategy

一看ThreadLocal,其实一切都明白了,

// SecurityContext是一个只有一个Authentication的类型
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

2、rememberMeServices的操作

如果开启了rememeberMe功能,那RememberMeServices的默认实现为TokenBasedRememberMeServices

	public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication) {
        // 1. 从authentication中获取用户名和密码
		String username = retrieveUserName(successfulAuthentication);
		String password = retrievePassword(successfulAuthentication);
        ...
		int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
		long expiryTime = System.currentTimeMillis();
		expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
        // 2. 根据用户名、密码、过期时间和自定义的key来生成Token签名保存在cookie中。
		String signatureValue = makeTokenSignature(expiryTime, username, password);
        // 3. 向response中设置cookie
		setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, 
                  										tokenLifetime, request,response);
	}

3、继续调用

AuthenticationSuccessHandler的默认实现为SavedRequestAwareAuthenticationSuccessHandler

	public void onAuthenticationSuccess(HttpServletRequest request, 
                       HttpServletResponse response,Authentication authentication) 
        									throws ServletException, IOException {
        // 1. 从缓存中取出缓存的请求
		SavedRequest savedRequest = this.requestCache.getRequest(request, response);
		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
        // 2. 从配置文件取出数据
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()|| (targetUrlParameter != null &&                                      		StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
            
			this.requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
        // 3. 删除在身份验证过程中可能已存储在会话中的与身份验证有关的临时数据
		clearAuthenticationAttributes(request);
		String targetUrl = savedRequest.getRedirectUrl();
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}

	// 上面转到这里
	public void onAuthenticationSuccess(HttpServletRequest request,
                       HttpServletResponse response,Authentication authentication)
      								     	throws IOException, ServletException {
		handle(request, response, authentication);
		clearAuthenticationAttributes(request);
	}

具体功能如下:

  • 从缓存中获取SavedRequest,如果有则取出targetUrl并重定向请求。
  • 从配置中读取targetUrlParameter,如果不为空则重定向,可以通过formLogin().defaultSuccessUrl()来进行配置。

示意图如下:

在这里插入图片描述

4、认证失败

老规矩,从doFilter()方法开始看,接下面

	protected void unsuccessfulAuthentication(HttpServletRequest request, 
                                              HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
        // 1. 清除保存的authentication
		SecurityContextHolder.clearContext();
		// 2. rememberMeServices操作
		this.rememberMeServices.loginFail(request, response);
        // 3. 继续调用
		this.failureHandler.onAuthenticationFailure(request, response, failed);
	}

第一步不说了把,请求ThreadLocal中的保存的SecurityContext

2、rememberMeServices操作

	public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
        // 1、将cookie的maxAge设置为0
		cancelCookie(request, response);
        // 2、暂时没有实现
		onLoginFail(request, response);
	}

3、继续调用

failureHandler的默认实现为SimpleUrlAuthenticationFailureHandler

	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
        // 1. 通过配置文件设置formLogin().failureUrl("xxxx")
		if (this.defaultFailureUrl == null) {
			response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
			return;
		}
		saveException(request, exception);
        // 默认为false
		if (this.forwardToDestination) {
			request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
		}
		else {
			this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
		}
	}

实现功能如下:

  • 获取配置文件配置的defaultFailureUrl,再通过判断forwardToDestination来决定是转发还是重定向。

示意图如下:
在这里插入图片描述

2、FilterSecurityInterceptor

过滤器说明:

  • 该过滤器负责资源的安全性处理,也就是权限验证。
public class FilterSecurityInterceptor{
    ...
    // 入口方法
	public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)throws IOException, ServletException {
		invoke(new FilterInvocation(request, response, chain));
	}

	// 接着上面:filterInvocation = new FilterInvocation(request, response, chain);
	public void invoke(FilterInvocation filterInvocation)
        								throws IOException, ServletException {
        // 1. 判断当前请求是不是处理过
		if (isApplied(filterInvocation) && this.observeOncePerRequest) {
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
			return;
		}
		// 2. 给当前请求打个标记
		if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
			filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
        // 3. 权限认证
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
        
        // 4. 过滤链传递
		try {
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(),
                                                filterInvocation.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}
    ...
}

认证过程:

	// object = new FilterInvocation(request, response, chain)
	protected InterceptorStatusToken beforeInvocation(Object object) {
		// 1. 从配置文件中读取配置
		Collection<ConfigAttribute> attributes = 
                   this.obtainSecurityMetadataSource().getAttributes(object);
		// 2. 判断是否重新用户身份认证
		Authentication authenticated = authenticateIfRequired();

		// 3. 进行权限认证
		attemptAuthorization(object, attributes, authenticated);
		...
		return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

	}

上面接下来:

	private void attemptAuthorization(Object object,
                  Collection<ConfigAttribute> attributes,Authentication authenticated) {
		...
		this.accessDecisionManager.decide(authenticated, object, attributes);
		...
	}

accessDecisionManager的默认实现为AffirmativeBased

public class AffirmativeBased{
    private List<AccessDecisionVoter<?>> decisionVoters;
    // 遍历投票器集合进行投票
	public void decide(Authentication authentication, Object object, 
      Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);
			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;
			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;
				break;
			default:
				break;
			}
		}

		checkAllowIfAllAbstainDecisions();
	}
}

AccessDecisionVoter默认实现为WebExpressionVoter

示意图如下:

在这里插入图片描述

3、ExceptionTranslationFilter

过滤器说明:

  • 该过滤器处理过滤链中引发的所有AccessDeniedException和AuthenticationException 。
  • 如果检测到AuthenticationException ,则过滤器将启动authenticationEntryPoint 。
  • 如果检测到AccessDeniedException ,则筛选器将确定该用户是否为匿名用户。 如果他们是匿名用户,则将启动authenticationEntryPoint 。 如果他们不是匿名用户,则过滤器将委派给AccessDeniedHandler 。 默认情况下,过滤器将使用AccessDeniedHandlerImpl 。

入口方法:

	private void doFilter(HttpServletRequest request, HttpServletResponse response, 
                          FilterChain chain) throws IOException, ServletException {
		try {
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// 1. 获得异常抛出链
			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
            // 2. 尝试获取AuthenticationException或者AccessDeniedException
			RuntimeException securityException = 
                	(AuthenticationException) this.throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			if (securityException == null) {
				securityException = (AccessDeniedException) this.throwableAnalyzer
				.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
			}
            // 3. 没有这两个异常则抛出
			if (securityException == null) {
				rethrow(ex);
			}
			// 4. 处理异常
			handleSpringSecurityException(request, response, chain, securityException);
		}
	}

	private void handleSpringSecurityException(HttpServletRequest request, 
        HttpServletResponse response,FilterChain chain, RuntimeException exception) 
        									throws IOException, ServletException {
        // 1. 根据异常类型的不同调用不同的处理器
		if (exception instanceof AuthenticationException) {
			handleAuthenticationException(request, response, chain, 
                                          (AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			handleAccessDeniedException(request, response, chain,
                                        (AccessDeniedException) exception);
		}
	}

AuthenticationException:

	private void handleAuthenticationException(HttpServletRequest request,   	
                                      HttpServletResponse response,FilterChain chain, 
            AuthenticationException exception) throws ServletException, IOException {
		// 1. 开始处理
		sendStartAuthentication(request, response, chain, exception);
	}

	protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		// 1. 清除当前线程保存的authentication
		SecurityContextHolder.getContext().setAuthentication(null);
        // 2. 保存当前请求
		this.requestCache.saveRequest(request, response);
        // 3. 调用处理器
		this.authenticationEntryPoint.commence(request, response, reason);
	}

authenticationEntryPoint的默认实现为LoginUrlAuthenticationEntryPoint

public class LoginUrlAuthenticationEntryPoint {
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
        // userForward默认为false
		if (!this.useForward) {
			String redirectUrl = buildRedirectUrlToLoginPage(request, response, 
                                                                 authException);
			this.redirectStrategy.sendRedirect(request, response, redirectUrl);
			return;
		}
        
		String redirectUrl = null;
        // forceHttps 默认为false
		if (this.forceHttps && "http".equals(request.getScheme())) {
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl != null) {
			this.redirectStrategy.sendRedirect(request, response, redirectUrl);
			return;
		}      
		String loginForm = determineUrlToUseForThisRequest(request, response, 
                                                              authException);
		RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
		dispatcher.forward(request, response);
		return;
	}
}

AccessDeniedException:

AccessDeniedHandler默认为AccessDeniedHandlerImpl

	private void handleAccessDeniedException(HttpServletRequest request, 
                       	  HttpServletResponse response,FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
        
        // 1. 获取authentication
		Authentication authentication = SecurityContextHolder.getContext()
            							.getAuthentication();
        // 2. 判断是否匿名,匿名/rememberMe情况要转给AuthenticationException情况处理
		boolean isAnonymous = this.authenticationTrustResolver
            					.isAnonymous(authentication);
		if (isAnonymous || this.authenticationTrustResolver
            							.isRememberMe(authentication)) {

			sendStartAuthentication(request, response, chain,
					new InsufficientAuthenticationException(xxx));
		}
		else {
			// 3. GO
			this.accessDeniedHandler.handle(request, response, exception);
		}
	}

具体处理:

	public void handle(HttpServletRequest request, HttpServletResponse response,
									AccessDeniedException accessDeniedException)
        					           	throws IOException, ServletException {
        // 1. 响应是否提交
		if (response.isCommitted()) {
			return;
		}
        // 2. 错误响应页面是否配置
		if (this.errorPage == null) {
			response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
			return;
		}
		// 3. 配置错误信息,重定向到错误页面
		request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
		response.setStatus(HttpStatus.FORBIDDEN.value());
		request.getRequestDispatcher(this.errorPage).forward(request, response);
	}

示意图如下:

在这里插入图片描述

4、RememberMeAuthenticationFilter

过滤器说明: 实现自动登录的一个过滤器,逻辑很简单,判断内存中有没有保存authentication,有则认证,没有则将过滤链传递下去。

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
        // 1. 判断内存中是否有保存authentication
		if (SecurityContextHolder.getContext().getAuthentication() != null) {
			chain.doFilter(request, response);
			return;
		}
        // 2. 取出请求中的cookie并解析成authentication
		Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, 
                                                                        response);
        // 3. authentication不为空,则进行身份验证
		if (rememberMeAuth != null) {
			try {
				rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
				SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
				onSuccessfulAuthentication(request, response, rememberMeAuth);
					return;
				}
			}
			catch (AuthenticationException ex) {
				this.rememberMeServices.loginFail(request, response);
				onUnsuccessfulAuthentication(request, response, ex);
			}
		}
		chain.doFilter(request, response);
	}

总体流程如下:

在这里插入图片描述

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security登录验证流程主要包括以下几个步骤: 1. 用户在客户端(如浏览器)输入用户名和密码提交登录请求。 2. 服务器端接收到登录请求后,将请求交给Spring Security进行处理。 3. Spring Security会调用配置好的AuthenticationManager进行身份验证。 4. AuthenticationManager会调用配置好的AuthenticationProvider进行身份认证。 5. AuthenticationProvider会根据用户提供的用户名和密码查询数据库或其他存储方式,获取用户的详细信息(如密码、角色、权限等)。 6. 如果用户信息验证成功,则AuthenticationProvider会返回一个Authentication对象,其中包含了用户的详细信息。 7. AuthenticationManager将Authentication对象返回给Spring Security进行后续处理。 8. 如果身份验证成功,则Spring Security会在服务器端建立一个SecurityContext对象,并将Authentication对象存储在其中。 9. 服务器端会生成一个Session ID并返回给客户端,同时将SecurityContext对象保存在服务器端的Session中,以便后续的请求能够使用该Session ID进行访问。 10. 登录成功后,客户端可以携带Session ID进行后续访问,服务器端会根据Session ID获取对应的SecurityContext对象,并使用其中的Authentication对象进行身份验证权限控制。 以上就是Spring Security登录验证流程。在实际开发中,我们需要根据具体的业务需求和安全策略,对相关的配置进行调整和优化,以确保系统的安全性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值