Spring Security 表单配置(二)

文章详细介绍了SpringSecurity的过滤器链,特别是UsernamePasswordAuthenticationFilter在用户认证过程中的作用。当认证成功时,它会设置安全上下文并触发成功的认证事件。而认证失败时,会抛出异常或者重定向到预设的错误页面。整个流程涉及到ProviderManager的多层委托认证机制。
摘要由CSDN通过智能技术生成

Spring Security 表单配置(二)

架构

Spring Security的整体架构,官网文档有介绍:https://docs.spring.io/spring-security/reference/5.7/servlet/architecture.html

友情提示:可以使用Edge浏览器打开,翻译一下,帮助理解,英文阅读能力好的话忽略此提示。

在Spring Security的过滤器中有一套过滤器链,官网把他们按照顺序列出来了

ForceEagerSessionCreationFilter
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter

这些Filter的顺序很重要,上面排列的顺序也是他们调用的顺序。
其中

UsernamePasswordAuthenticationFilter

就是 使用用户名和密码认证时要通过的一个过滤器

认证过滤器

先看一下 UsernamePasswordAuthenticationFilter 这个类的方法结构
UsernamePasswordAuthenticationFilter

其中的 attemptAuthentication 方法就是认证逻辑

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

在之前的配置中,就有涉及这个地方的配置,比如登录的url、请求方法、用户名的参数名、密码的参数名。
这里会从request获取对应的用户名和密码,然后构建一个未认证的UsernamePasswordAuthentiactionToken ,最后一步进行认证。
这个认证逻辑也是有套路的,通过ProviderManager进行认证,自身拿不到认证结果就从parent那里拿,一层层往上委托认证,最后委托到DaoAuthenticationProvider,从我们自定义的UserDetailsService中获取认证对象,至于我们的UserDetailsService何时注入到这个对象中,那就是另一个故事了,可以在set方法里面打一个断点,启动时就可以看到方法栈,看到类构建过程,这里就不看了。

认证成功

当认证成功,会回到 AbstractPreAuthenticatedProcessingFilter 的 successfulAuthentication 方法
设置身份上下文,触发认证成功事件,调用成功重定向处理器

认证成功后处理逻辑
认证成功重定向url

可以看到认证成功重定向url就是之前我们设置的。
当然Spring Security的配置方式很多,配置组合也很多,这只是其中一种。

认证失败

ProviderManager的authenticate方法

@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// ... 省略一大堆认证逻辑
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// 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 ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.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 then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then 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;
	}

如果没有拿到认证结果,而且还有parent,向上委托认证,单亲委托
一直往上找,找到老祖宗还没拿到认证结果,而且没有异常产生
那么就会产生一个异常ProviderNotFoundException

如果parent没有异常,那就发出认证失败事件
最终会抛出lastException异常

在认证过程中也会抛出异常,毕竟常见的BadCredentialsException
DaoAuthenticationProvider中additionalAuthenticationChecks方法会抛出

protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}

最后赋值给lastException,抛出到上层,到异常处理器。
具体逻辑在 AbstractAuthenticationProcessingFilter 的 doFilter方法中的异常catch块
最终调用了 unsuccessfulAuthentication 方法

protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		this.logger.trace("Failed to process authentication request", failed);
		this.logger.trace("Cleared SecurityContextHolder");
		this.logger.trace("Handling authentication failure");
		this.rememberMeServices.loginFail(request, response);
		this.failureHandler.onAuthenticationFailure(request, response, failed);
	}

重定向的url也是之前配置的。

这里的成功和失败的执行逻辑和官网的逻辑流程图吻合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值