Получите механизм обработки исключений Spring Security в одной статье!

Spring Boot
Получите механизм обработки исключений Spring Security в одной статье!

Сегодня поговорим с моими друзьями о механизме обработки исключений в Spring Security.

В цепочке фильтров Spring Security фильтр ExceptionTranslationFilter специально используется для обработки исключений. В ExceptionTranslationFilter мы видим, что исключения делятся на две категории: исключения аутентификации и исключения авторизации. Эти два исключения обрабатываются разными функциями обратного вызова. Разберитесь с этим Сегодня Сун Гэ поделится с вами правилами и положениями здесь.

1. Классификация аномалий

Исключения в Spring Security можно разделить на две категории: исключение аутентификации и исключение авторизации.

Исключением аутентификации является AuthenticationException, которое имеет множество классов реализации:

Видно, что здесь довольно много классов реализации исключений, все из которых являются исключениями, связанными с аутентификацией, то есть исключениями для неудачных входов в систему. Некоторые из этих исключений были представлены вам в предыдущих статьях, например следующий код (выдержка из:Spring Security разделяет внешний и внутренний интерфейсы, давайте не будем переходить по страницам! Все взаимодействия JSON):

resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error(e.getMessage());
if (e instanceof LockedException) {
    respBean.setMsg("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
    respBean.setMsg("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
    respBean.setMsg("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
    respBean.setMsg("账户被禁用,请联系管理员!");
} else if (e instanceof BadCredentialsException) {
    respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();

Другой тип — это исключение авторизации AccessDeniedException.Существует меньше классов реализации исключений авторизации, поскольку меньше возможных причин сбоя авторизации.

2.ExceptionTranslationFilter

ExceptionTranslationFilter – это фильтр в Spring Security, специально отвечающий за обработку исключений. По умолчанию этот фильтр автоматически загружается в цепочку фильтров.

Некоторые друзья могут не знать, как он загружается, я немного расскажу здесь.

Когда мы используем Spring Security, если нам нужно настроить логику реализации, мы все наследуемся от WebSecurityConfigurerAdapter для расширения, а сам WebSecurityConfigurerAdapter выполняет часть операции инициализации. Давайте посмотрим на процесс инициализации HttpSecurity в нем:

protected final HttpSecurity getHttp() throws Exception {
	if (http != null) {
		return http;
	}
	AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
	localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
	AuthenticationManager authenticationManager = authenticationManager();
	authenticationBuilder.parentAuthenticationManager(authenticationManager);
	Map<Class<?>, Object> sharedObjects = createSharedObjects();
	http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
			sharedObjects);
	if (!disableDefaults) {
		http
			.csrf().and()
			.addFilter(new WebAsyncManagerIntegrationFilter())
			.exceptionHandling().and()
			.headers().and()
			.sessionManagement().and()
			.securityContext().and()
			.requestCache().and()
			.anonymous().and()
			.servletApi().and()
			.apply(new DefaultLoginPageConfigurer<>()).and()
			.logout();
		ClassLoader classLoader = this.context.getClassLoader();
		List<AbstractHttpConfigurer> defaultHttpConfigurers =
				SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
		for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
			http.apply(configurer);
		}
	}
	configure(http);
	return http;
}

Как видите, в конце метода getHttp вызовconfigure(http);, когда мы используем Spring Security, пользовательский класс конфигурации наследуется от WebSecurityConfigurerAdapter и переписывает метод configure(HttpSecurity http) здесь вызывается, другими словами, когда мы переходим к настройке HttpSecurity, он уже завершил волну инициализации.

Во время инициализации HttpSecurity по умолчанию вызывается метод exceptionHandling, который настраивает ExceptionHandlingConfigurer и, наконец, вызывает метод ExceptionHandlingConfigurer#configure для добавления ExceptionTranslationFilter в цепочку фильтров Spring Security.

Давайте посмотрим на исходный код метода ExceptionHandlingConfigurer#configure:

@Override
public void configure(H http) {
	AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
	ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
			entryPoint, getRequestCache(http));
	AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
	exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
	exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
	http.addFilter(exceptionTranslationFilter);
}

Как видите, создаются два объекта, которые передаются в ExceptionTranslationFilter:

  • AuthenticationEntryPoint Используется для обработки исключений аутентификации.
  • AccessDeniedHandler Используется для обработки исключений авторизации.

Конкретная логика обработки находится в ExceptionTranslationFilter, давайте посмотрим:

public class ExceptionTranslationFilter extends GenericFilterBean {
	public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
			RequestCache requestCache) {
		this.authenticationEntryPoint = authenticationEntryPoint;
		this.requestCache = requestCache;
	}
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		try {
			chain.doFilter(request, response);
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			if (ase == null) {
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}
			if (ase != null) {
				if (response.isCommitted()) {
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				}
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}
				throw new RuntimeException(ex);
			}
		}
	}
	private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException {
		if (exception instanceof AuthenticationException) {
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		}
		else if (exception instanceof AccessDeniedException) {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
				sendStartAuthentication(
						request,
						response,
						chain,
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			}
			else {
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			}
		}
	}
	protected void sendStartAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain,
			AuthenticationException reason) throws ServletException, IOException {
		SecurityContextHolder.getContext().setAuthentication(null);
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
		authenticationEntryPoint.commence(request, response, reason);
	}
}

Исходный код ExceptionTranslationFilter относительно длинный, я перечислю основные части и проанализирую их здесь:

  1. Ядром фильтра, конечно же, является метод doFilter, мы начнем с метода doFilter. В методе doFilter здесь цепочка фильтров продолжает выполняться вниз. ExceptionTranslationFilter является предпоследним в цепочке фильтров Spring Security, а последним является FilterSecurityInterceptor. FilterSecurityInterceptor занимается проблемами авторизации. Unauthorized и т. д., а затем генерирует исключение. , созданное исключение в конечном итоге будет перехвачено методом ExceptionTranslationFilter#doFilter.
  2. Когда исключение поймано, следующим шагом будет вызовthrowableAnalyzer.getFirstThrowableOfTypeметод, чтобы определить, является ли это исключением проверки подлинности или исключением авторизации.После оценки типа исключения он переходит к методу handleSpringSecurityException для обработки; если это не тип исключения в Spring Security, логика обработки типа исключения ServletException следует.
  3. После входа в метод handleSpringSecurityException оно все еще оценивается в соответствии с типом исключения.Если это исключение, связанное с аутентификацией, перейдите к методу sendStartAuthentication, который окончательно обрабатывается методом authenticationEntryPoint.commence, если это связано с авторизацией исключение, перейдите к методу accessDeniedHandler.handle для обработки.

Классом реализации AuthenticationEntryPoint по умолчанию является LoginUrlAuthenticationEntryPoint, поэтому логика обработки исключений аутентификации по умолчанию — это метод LoginUrlAuthenticationEntryPoint#commence, как показано ниже.

public void commence(HttpServletRequest request, HttpServletResponse response,
		AuthenticationException authException) throws IOException, ServletException {
	String redirectUrl = null;
	if (useForward) {
		if (forceHttps && "http".equals(request.getScheme())) {
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		if (redirectUrl == null) {
			String loginForm = determineUrlToUseForThisRequest(request, response,
					authException);
			RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
			dispatcher.forward(request, response);
			return;
		}
	}
	else {
		redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
	}
	redirectStrategy.sendRedirect(request, response, redirectUrl);
}

Как видите, это перенаправление, перенаправление на страницу входа (то есть, когда мы обращаемся к ресурсу, для доступа к которому требуется вход без входа, он будет автоматически перенаправлен на страницу входа).

Классом реализации AccessDeniedHandler по умолчанию является AccessDeniedHandlerImpl, поэтому исключения авторизации обрабатываются по умолчанию в методе AccessDeniedHandlerImpl#handle:

public void handle(HttpServletRequest request, HttpServletResponse response,
		AccessDeniedException accessDeniedException) throws IOException,
		ServletException {
	if (!response.isCommitted()) {
		if (errorPage != null) {
			request.setAttribute(WebAttributes.ACCESS_DENIED_403,
					accessDeniedException);
			response.setStatus(HttpStatus.FORBIDDEN.value());
			RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
			dispatcher.forward(request, response);
		}
		else {
			response.sendError(HttpStatus.FORBIDDEN.value(),
				HttpStatus.FORBIDDEN.getReasonPhrase());
		}
	}
}

Как видите, именно здесь сервер возвращается к 403.

3. Пользовательская обработка

Логика обработки по умолчанию в Spring Security была введена ранее.В реальной разработке нам может потребоваться внести некоторые коррективы.Это очень просто.Его можно настроить на exceptionHandling.

Сначала настройте класс обработки исключений аутентификации и класс обработки исключений авторизации:

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.getWriter().write("login failed:" + authException.getMessage());
    }
}
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(403);
        response.getWriter().write("Forbidden:" + accessDeniedException.getMessage());
    }
}

Затем настройте его в SecurityConfig следующим образом:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                ...
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(myAccessDeniedHandler)
                .and()
                ...
                ...
    }
}

После завершения настройки перезапустите проект, исключения аутентификации и исключения авторизации будут следовать нашей пользовательской логике.

4. Резюме

Что ж, сегодня я в основном поделился с друзьями механизмом обработки исключений в Spring Security, желающие могут попробовать~

Адрес загрузки кода в тексте:GitHub.com/Len VE/Судный день, ты…

Публичный аккаунт [Jiangnan A Little Rain] ответил на запрос springsecurity в фоновом режиме и получил более 40 полных статей из серии Spring Security~