SpringSecurity表单登录过程中的异常

DaoAuthenticationProvider#retrieveUser

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

可以看到,这里在自定义的UserDetailsService#loadUserByUsername中抛出的任何异常都会被catch捕获到,最终抛出的异常只能是UsernameNotFoundExceptionInternalAuthenticationServiceException,向调用者AbstractUserDetailsAuthenticationProvider抛出。

AbstractUserDetailsAuthenticationProvider#authenticate

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
			() -> messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.onlySupports",
					"Only UsernamePasswordAuthenticationToken is supported"));

	// Determine username
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();

	boolean cacheWasUsed = true;
	UserDetails user = this.userCache.getUserFromCache(username);

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

		try {
			user = retrieveUser(username,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (UsernameNotFoundException notFound) {
			logger.debug("User '" + username + "' not found");

			if (hideUserNotFoundExceptions) {
				throw new BadCredentialsException(messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.badCredentials",
						"Bad credentials"));
			}
			else {
				throw notFound;
			}
		}

		Assert.notNull(user,
				"retrieveUser returned null - a violation of the interface contract");
	}

	try {
		preAuthenticationChecks.check(user);
		additionalAuthenticationChecks(user,
				(UsernamePasswordAuthenticationToken) authentication);
	}
	catch (AuthenticationException exception) {
		if (cacheWasUsed) {
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			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);
}

#retrieveUser这里如果抛出了InternalAuthenticationServiceException,则会抛给ProviderManager

ProviderManager#authenticate

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	Class<? extends Authentication> toTest = authentication.getClass();
	AuthenticationException lastException = null;
	AuthenticationException parentException = null;
	Authentication result = null;
	Authentication parentResult = null;
	boolean debug = logger.isDebugEnabled();

	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}

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

		try {
			result = provider.authenticate(authentication);

			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		catch (AccountStatusException | InternalAuthenticationServiceException e) {
			prepareException(e, authentication);
			// SEC-546: Avoid polling additional providers if auth failure is due to
			// invalid account status
			throw e;
		} catch (AuthenticationException e) {
			lastException = e;
		}
	}

#provider.authenticate如这里的异常只可能是BadCredentialsException
InternalAuthenticationServiceException,观察代码可以发现,这两种异常都会继续向上抛出。

AbstractAuthenticationProcessingFilter#doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
		throws IOException, ServletException {

	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;

	if (!requiresAuthentication(request, response)) {
		chain.doFilter(request, response);

		return;
	}

	if (logger.isDebugEnabled()) {
		logger.debug("Request is to process authentication");
	}

	Authentication authResult;

	try {
		authResult = attemptAuthentication(request, response);
		if (authResult == null) {
			// return immediately as subclass has indicated that it hasn't completed
			// authentication
			return;
		}
		sessionStrategy.onAuthentication(authResult, request, response);
	}
	catch (InternalAuthenticationServiceException failed) {
		logger.error(
				"An internal error occurred while trying to authenticate the user.",
				failed);
		unsuccessfulAuthentication(request, response, failed);

		return;
	}
	catch (AuthenticationException failed) {
		// Authentication failed
		unsuccessfulAuthentication(request, response, failed);

		return;
	}

	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}

	successfulAuthentication(request, response, chain, authResult);
}

#attemptAuthentication这里的异常是InternalAuthenticationServiceExceptionBadCredentialsException,这两种异常都会执行#unsuccessfulAuthentication
分析到这里,得出第一个结论,AbstractUserDetailsAuthenticationProvider#authenticate#retrieveUser这里抛出的任何异常都会执行#unsuccessfulAuthentication
继续分析可以得出第二个结论,在AbstractUserDetailsAuthenticationProvider#authenticate的这里,

try {
	preAuthenticationChecks.check(user);
	additionalAuthenticationChecks(user,
			(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
	if (cacheWasUsed) {
		// There was a problem, so try again after checking
		// we're using latest data (i.e. not from the cache)
		cacheWasUsed = false;
		user = retrieveUser(username,
				(UsernamePasswordAuthenticationToken) authentication);
		preAuthenticationChecks.check(user);
		additionalAuthenticationChecks(user,
				(UsernamePasswordAuthenticationToken) authentication);
	}
	else {
		throw exception;
	}
}

抛出的异常也都会执行AbstractAuthenticationProcessingFilter#unsuccessfulAuthentication

结论:

SpringSecurity默认的表单登录,或者是自定义UserDetailsService,抛出的任何异常都会走认证失败方法#unsuccessfulAuthentication,进一步就会执行#failureHandler.onAuthenticationFailure

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security配置基于表单登录,通常需要以下几个步骤: 1. **配置`WebSecurityConfigurerAdapter`**:您需要扩展`WebSecurityConfigurerAdapter`类并重写其`configure(HttpSecurity http)`方法。在这个方法,您可以定义安全策略,包括哪些URL应该被保护以及如何进行身份验证。 2. **启用表单登录**:在`configure(HttpSecurity http)`方法,使用`.formLogin()`启用表单登录。这会启用默认的登录页面和登录处理机制。 3. **自定义登录页面**:如果您想要自定义登录页面,可以使用`.loginPage("/custom-login.html")`来指定自定义登录页面的URL。这样,当用户尝试访问受保护的资源时,他们将被重定向到这个自定义的登录页面。 4. **允许所有用户访问登录页面**:使用`.permitAll()`方法确保所有用户都可以访问登录页面,即使他们尚未登录。 5. **配置登录成功和失败的处理**:您可以使用`.successHandler()`和`.failureHandler()`来分别配置登录成功和失败后的处理逻辑。 6. **配置用户名和密码参数**:默认情况下,Spring Security期望从请求获取名为`username`和`password`的参数。如果您的表单使用了不同的参数名称,您需要在`configure(HttpSecurity http)`方法使用`.usernameParameter("yourUsernameField")`和`.passwordParameter("yourPasswordField")`来指定这些参数的名称。 7. **配置登录POST请求的URL**:默认情况下,登录数据的提交地址是`/login`。如果您想要更改这个URL,可以使用`.loginProcessingUrl("/custom-login-processing-url")`来进行配置。 8. **配置退出登录**:使用`.and().logout()`启用退出登录功能,并且可以进一步配置退出登录的处理逻辑。 9. **配置记住我服务**:如果您想要提供“记住我”功能,可以使用`.rememberMe()`方法来启用它。 10. **配置异常处理**:您可以使用`.exceptionHandling()`来配置如何处理认证过程异常情况。 通过以上步骤,您就可以在Spring Security配置基于表单登录了。此外,还需要确保您的应用程序有一个接收用户名和密码的表单,并且该表单的`action`属性指向正确的登录处理URL。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值