在使用spring security时,如果没有查询到访问权限会抛AccessDeniedException
由于spring security采用filter方式进行链式校验,而自定义的权限过滤器在ExceptionTranslationFilter
异常解析过滤器后面,因此当抛出上述异常时会被前一个ExceptionTranslationFilter
捕获,并根据异常类型进行解析,状态码403,如果在WebSecurityConfigurerAdapter
中定义了http.exceptionHandling().accessDeniedPage("/");
则会直接通过请求转发到响应的错误页面,否则会通过tomcat错误页面进行转发
StandardHostValve
//如果是web.xml中定义了errorPage页面,则会被加载进去,直接通过状态码获取
ErrorPage errorPage = context.findErrorPage(statusCode);
if (errorPage == null) {
// Look for a default error page
//springBoot在启动时如果没有定义错误页面会自动生成一个0的错误页面 访问地址默认为/error
errorPage = context.findErrorPage(0);
}
if (errorPage != null && response.isErrorReportRequired()) {
response.setAppCommitted(false);
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
Integer.valueOf(statusCode));
String message = response.getMessage();
if (message == null) {
message = "";
}
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
errorPage.getLocation());
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
DispatcherType.ERROR);
Wrapper wrapper = request.getWrapper();
if (wrapper != null) {
request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
wrapper.getName());
}
request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
request.getRequestURI());
//请求转发
if (custom(request, response, errorPage)) {
response.setErrorReported();
try {
response.finishResponse();
} catch (ClientAbortException e) {
// Ignore
} catch (IOException e) {
container.getLogger().warn("Exception Processing " + errorPage, e);
}
}
}
tomcat最终会转发到ErrorPage中定义的location指定的地址并将错误信息等传递过去(转发默认不会经过filter),
最终会通过DispatcherServlet执行Controller逻辑
因此我们只需要实现一个/error的Controller即可,而在springBoot系统默认实现
BasicErrorController
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//SpringBoot 4xx 5xx通用错误页面,就是因为没有定义错误页面它会取默认的error页面
//而error页面定义就是在ErrorMvcAutoConfiguration中定义的
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
@Override
public String getErrorPath() {
return this.errorProperties.getPath();
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
}
有两种视图模式 一个是页面 一个是json,如果需要自定义格式或者页面的话 需要重写上述两个方法
所有错误状态码的请求在没有定义错误页面时都会通过BasicErrorController
默认情况下 只需要将404.html等放到下面目录下的
error
文件夹下面即可
[/META-INF/resources/, /resources/, /static/, /public/]
server.error.path
用来指定errorPage的父级路径
如果项目中采用spring security,在自定义错误页面时getErrorPath()
一定要this.errorProperties.getPath();
否则会被拦截;
题外话,
- 如何添加springBoot ErrorPage页面呢?
TomcatServletWebServerFactory
- 手动声明一个TomcatServletWebServerFactory 重写包含Context的方法 在Context添加(不推荐)
- 实现TomcatContextCustomizer接口
protected void configureContext(Context context, ServletContextInitializer[] initializers) {
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(starter, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
context.addLifecycleListener(lifecycleListener);
}
for (Valve valve : this.contextValves) {
context.getPipeline().addValve(valve);
}
for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
for (MimeMappings.Mapping mapping : getMimeMappings()) {
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
configureSession(context);
new DisableReferenceClearingContextCustomizer().customize(context);
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}
注意,需要使用springBoot自身定义的ErrorPage接口
3 实现ErrorPageRegistrar接口
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
protected ErrorPageCustomizer(ServerProperties properties) {
this.properties = properties;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
+ this.properties.getError().getPath());
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
4 实现WebServerFactoryCustomizer接口 (推荐在对Servlet容器自定义时使用)
额外补充
- 通过自动导入
ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
//对serlvet容器进行额外扩展,因此也可以实现WebServerFactoryCustomizer接口添加ErrorPage页面
registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
WebServerFactoryCustomizerBeanPostProcessor.class);
//ErrorPageRegistrar接口生效的原因就是因为注册了该BeanPostProcessor
registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor",
ErrorPageRegistrarBeanPostProcessor.class);
}
上述源码基于SpringBoot 2.2.5版本
参考:官方文档