exceptionHandling()配置的
这里的配置主要是为了统一处理Spring Security的异常,
Spring Security 中的异常可以分为两大类,一种是认证异常,一种是授权异常。
分别是AuthenticationException 、 AccessDeniedException
简介
1、AuthenticationException
AuthenticationException 异常实现类还是蛮多的,都是都是认证相关的异常,也就是登录失败的异常
2、AccessDeniedException
授权异常 AccessDeniedException,授权异常的实现类比较少,因为授权失败的可能原因比较少
大部分都是自定义实现授权异常
源码
public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractHttpConfigurer<ExceptionHandlingConfigurer<H>, H> {
private AuthenticationEntryPoint authenticationEntryPoint;
private AccessDeniedHandler accessDeniedHandler;
private LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap<>();
private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
public ExceptionHandlingConfigurer() {
}
@Override
public void configure(H http) {
//身份验证入口点(驱动应用开始进行身份验证),用于启动身份验证方案(默认:Http403ForbiddenEntryPoint)
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
//处理Filter链中抛出的AccessDeniedException与AuthenticationException类型的异常;
//它提供了Java异常和HTTP响应之间的桥梁
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
entryPoint, getRequestCache(http));
//默认为AccessDeniedHandlerImpl【响应403】
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
if (entryPoint == null) {
entryPoint = createDefaultEntryPoint(http);
}
return entryPoint;
}
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
if (this.defaultEntryPointMappings.isEmpty()) {
return new Http403ForbiddenEntryPoint();
}
if (this.defaultEntryPointMappings.size() == 1) {
return this.defaultEntryPointMappings.values().iterator().next();
}
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
this.defaultEntryPointMappings);
entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator()
.next());
return entryPoint;
}
AccessDeniedHandler getAccessDeniedHandler(H http) {
AccessDeniedHandler deniedHandler = this.accessDeniedHandler;
if (deniedHandler == null) {
deniedHandler = createDefaultDeniedHandler(http);
}
return deniedHandler;
}
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
if (this.defaultDeniedHandlerMappings.isEmpty()) {
return new AccessDeniedHandlerImpl();
}
if (this.defaultDeniedHandlerMappings.size() == 1) {
return this.defaultDeniedHandlerMappings.values().iterator().next();
}
return new RequestMatcherDelegatingAccessDeniedHandler(
this.defaultDeniedHandlerMappings,
new AccessDeniedHandlerImpl());
}
}
1、AuthenticationEntryPoint
AuthenticationEntryPoint | 说明 | 验证过滤器 |
---|---|---|
LoginUrlAuthenticationEntryPoint | 可以对请求进行重定向 例如将http转换成https请求; 将请求重定向到配置的登录界面(可以服务器端或客户端重定向) |
UsernamePasswordAuthenticationFilter |
DigestAuthenticationEntryPoint | 摘要式身份验证入口点 添加响应头“WWW-Authenticate” 并响应401【未经授权】 |
DigestAuthenticationFilter |
BasicAuthenticationEntryPoint | 基础的摘要式身份验证入口点 添加响应头“WWW-Authenticate” 并响应401【未经授权】 |
BasicAuthenticationFilter |
2、ExceptionTranslationFilter
该过滤器的作用是捕获Spring Security的Filter链执行时抛出来的异常,然后根据异常类型来发起验证的流程
默认情况下,这个过滤器已经被自动加载到过滤器链中
当我们使用 Spring Security 的时候,如果需要自定义实现逻辑,都是继承自 WebSecurityConfigurerAdapter 进行扩展,
WebSecurityConfigurerAdapter 中本身就进行了一部分的初始化操作,我们来看下它里边 HttpSecurity 的初始化过程
1、WebSecurityConfigurerAdapter 部份源码
protected final HttpSecurity getHttp() throws Exception {
if (this.http != null) {
return this.http;
} else {
AuthenticationEventPublisher eventPublisher = this.getAuthenticationEventPublisher();
this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = this.authenticationManager();
this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
Map<Class<?>, Object> sharedObjects = this.createSharedObjects();
this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
if (!this.disableDefaults) {
this.applyDefaultConfiguration(this.http);
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
Iterator var6 = defaultHttpConfigurers.iterator();
while(var6.hasNext()) {
AbstractHttpConfigurer configurer = (AbstractHttpConfigurer)var6.next();
this.http.apply(configurer);
}
}
this.configure(this.http);
return this.http;
}
}
在 getHttp 方法的最后,调用了this.configure(http);
说明我们在使用 Spring Security 时,自定义配置类继承自 WebSecurityConfigurerAdapter ,并重写的 configure(HttpSecurity http) 方法就是在这里调用的,换句话说,当我们去配置 HttpSecurity 时,其实它已经完成了一波初始化了
在默认的 HttpSecurity 初始化的过程中,调用了 exceptionHandling 方法,这个方法会将 ExceptionHandlingConfigurer 配置进来,最终调用 ExceptionHandlingConfigurer#configure 方法将 ExceptionTranslationFilter 添加到 Spring Security 过滤器链中
2、ExceptionHandlingConfigurer
public void configure(H http) {
AuthenticationEntryPoint entryPoint = this.getAuthenticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, this.getRequestCache(http));
AccessDeniedHandler deniedHandler = this.getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
exceptionTranslationFilter = (ExceptionTranslationFilter)this.postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
可以看到,这里构造了两个对象传入到 ExceptionTranslationFilter 中:
- AuthenticationEntryPoint 这个用来处理认证异常。
- AccessDeniedHandler 这个用来处理授权异常。
具体的处理逻辑则在 ExceptionTranslationFilter 中
3、ExceptionTranslationFilter
/*
* Copyright 2004-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.access;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
/**
* Handles any <code>AccessDeniedException</code> and <code>AuthenticationException</code>
* thrown within the filter chain.
* <p>
* This filter is necessary because it provides the bridge between Java exceptions and
* HTTP responses. It is solely concerned with maintaining the user interface. This filter
* does not do any actual security enforcement.
* <p>
* If an {@link AuthenticationException} is detected, the filter will launch the
* <code>authenticationEntryPoint</code>. This allows common handling of authentication
* failures originating from any subclass of
* {@link org.springframework.security.access.intercept.AbstractSecurityInterceptor}.
* <p>
* If an {@link AccessDeniedException} is detected, the filter will determine whether or
* not the user is an anonymous user. If they are an anonymous user, the
* <code>authenticationEntryPoint</code> will be launched. If they are not an anonymous
* user, the filter will delegate to the
* {@link org.springframework.security.web.access.AccessDeniedHandler}. By default the
* filter will use
* {@link org.springframework.security.web.access.AccessDeniedHandlerImpl}.
* <p>
* To use this filter, it is necessary to specify the following properties:
* <ul>
* <li><code>authenticationEntryPoint</code> indicates the handler that should commence
* the authentication process if an <code>AuthenticationException</code> is detected. Note
* that this may also switch the current protocol from http to https for an SSL
* login.</li>
* <li><tt>requestCache</tt> determines the strategy used to save a request during the
* authentication process in order that it may be retrieved and reused once the user has
* authenticated. The default implementation is {@link HttpSessionRequestCache}.</li>
* </ul>
*
* @author Ben Alex
* @author colin sampaleanu
*/
public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
private RequestCache requestCache = new HttpSessionRequestCache();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint) {
this(authenticationEntryPoint, new HttpSessionRequestCache());
}
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint, RequestCache requestCache) {
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
Assert.notNull(requestCache, "requestCache cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}
@Override
public void afterPropertiesSet() {
Assert.notNull(this.authenticationEntryPoint, "authenticationEntryPoint must be specified");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
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) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
private void rethrow(Exception ex) throws ServletException {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return this.authenticationEntryPoint;
}
protected AuthenticationTrustResolver getAuthenticationTrustResolver() {
return this.authenticationTrustResolver;
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
this.logger.trace("Sending to authentication entry point since authentication failed", exception);
sendStartAuthentication(request, response, chain, exception);
}
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
this.accessDeniedHandler.handle(request, response, exception);
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
this.accessDeniedHandler = accessDeniedHandler;
}
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must not be null");
this.authenticationTrustResolver = authenticationTrustResolver;
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
Assert.notNull(throwableAnalyzer, "throwableAnalyzer must not be null");
this.throwableAnalyzer = throwableAnalyzer;
}
/**
* @since 5.5
*/
@Override
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* Default implementation of <code>ThrowableAnalyzer</code> which is capable of also
* unwrapping <code>ServletException</code>s.
*/
private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
/**
* @see org.springframework.security.web.util.ThrowableAnalyzer#initExtractorMap()
*/
@Override
protected void initExtractorMap() {
super.initExtractorMap();
registerExtractor(ServletException.class, (throwable) -> {
ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
return ((ServletException) throwable).getRootCause();
});
}
}
}
过滤器最核心的当然是 doFilter 方法,我们就从 doFilter 方法看起。
这里的 doFilter 方法中过滤器链继续向下执行,
ExceptionTranslationFilter 处于 Spring Security 过滤器链的倒数第二个,最后一个是 FilterSecurityInterceptor,
FilterSecurityInterceptor 专门处理授权问题,
在处理授权问题时,就会发现用户未登录、未授权等,进而抛出异常,抛出的异常,最终会被 ExceptionTranslationFilter#doFilter 方法捕获。
当捕获到异常之后,接下来通过调用 throwableAnalyzer.getFirstThrowableOfType 方法来判断是认证异常还是授权异常,
判断出异常类型之后,进入到 handleSpringSecurityException 方法进行处理;
如果不是 Spring Security 中的异常类型,则走 ServletException 异常类型的处理逻辑。
进入到 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
自定义处理
前面和大家介绍了 Spring Security 中默认的处理逻辑,实际开发中,我们可以需要做一些调整,
在 exceptionHandling 上进行配置即可
1、自定义异常处理器
首先自定义认证异常处理类和授权异常处理类:
- 认证异常处理类
```java
@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());
- 授权异常处理类
```java
@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());
}
}
2、配置
在 SecurityConfig 中进行配置
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
配置完成后,重启项目,认证异常和授权异常就会走我们自定义的逻辑了