Spring MVC拦截器异常处理与跨域详解
定位:第 04 篇,讲透拦截器三回调与过滤器对比、全局异常处理体系、CORS 跨域原理与配置
适用版本:Spring Framework 6.x(JDK 17+)
目录
一、拦截器体系
1.1 三个回调
java
public class AuthInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest req, ...) {
// 执行前:鉴权检查;返回 false 则中断,不再执行后续与 Handler
return true;
}
public void postHandle(...) {
// Handler 执行后、视图渲染前:可修改 Model
}
public void afterCompletion(...) {
// 完成后(含异常):清理资源(如解绑 ThreadLocal)
}
}
执行时序(洋葱模型):
preHandle → Handler → postHandle → 视图/响应 → afterCompletion
preHandle 返回 false:后续拦截器与 Handler 都不执行
1.2 注册与顺序
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry reg) {
reg.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/login") // 排除
.order(1); // 多个拦截器的顺序
}
}
1.3 拦截器与过滤器的对比(高频)
| 维度 | Filter(过滤器) | HandlerInterceptor(拦截器) |
|---|---|---|
| 所属层 | Servlet 规范(容器层) | Spring MVC(框架层) |
| 位置 | 在 DispatcherServlet 之外 | 在 DispatcherServlet 之内 |
| 能拿到 | 原始请求/响应 | 还能拿到 Handler/方法信息 |
| 典型用途 | 编码、鉴权(粗)、压缩、跨域 | 登录校验、权限、日志、埋点 |
| 生效范围 | 所有请求(含静态) | 进入 MVC 处理的请求 |
记忆:过滤器在外圈管"请求",拦截器在内圈管"处理器"。
二、全局异常处理
2.1 异常解析链
DispatcherServlet 捕获到 Handler 抛出的异常后,交给 HandlerExceptionResolver 链:
① ExceptionHandlerExceptionResolver → 找 @ExceptionHandler(方法级/全局)
② ResponseStatusExceptionResolver → @ResponseStatus 注解
③ DefaultHandlerExceptionResolver → 标准 Spring 异常映射(404/405 等)
都处理不了 → 转发 /error(Boot)
2.2 @RestControllerAdvice 全局处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) // 校验
ResponseEntity<ApiError> invalid(MethodArgumentNotValidException e) {
return badRequest(字段级错误明细);
}
@ExceptionHandler(BizException.class) // 业务
ResponseEntity<ApiError> biz(BizException e) {
return status(e.httpStatus()).body(ApiError.of(e.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class) // 兜底
ResponseEntity<ApiError> fallback(Exception e) {
log.error("unhandled", e); // 记录,不外泄堆栈
return internal(ApiError.of("SYSTEM_ERROR", "系统繁忙"));
}
}
2.3 分层处理策略
| 异常类别 | 状态码 | 响应内容 | 是否告警 |
|---|---|---|---|
| 参数校验 | 400 | 字段级明细 | 否 |
| 业务异常 | 400/409 等 | 业务错误码 + 消息 | 视情况 |
| 认证/授权 | 401/403 | 标准消息 | 否/监控 |
| 未知异常 | 500 | 通用消息(不回传堆栈) | 是,告警 |
纪律:异常分类处理、堆栈只进日志不进响应、未知异常必须告警。
三、CORS 跨域
3.1 同源策略
浏览器的安全约束:脚本只能请求同源 (协议 + 域名 + 端口一致)的资源;跨源请求默认被拦截。注意:这是浏览器的行为,服务端间调用不存在跨域问题。
3.2 CORS 机制
CORS 是服务端通过响应头声明"允许哪些跨源访问":
简单请求(GET/部分 POST 等):直接发,靠响应头判定
非简单请求:先发预检(OPTIONS)询问 → 服务端允许后才发真实请求
关键头:
| 头 | 含义 |
|---|---|
Access-Control-Allow-Origin |
允许的源(* 或具体域名) |
Access-Control-Allow-Methods |
允许的方法 |
Access-Control-Allow-Headers |
允许的自定义头 |
Access-Control-Allow-Credentials |
是否允许带 Cookie |
3.3 Spring 的三种配置
① 注解:@CrossOrigin(origins = "https://web.example.com") // 单接口
② 全局:WebMvcConfigurer#addCorsMappings
registry.addMapping("/api/**")
.allowedOrigins("https://web.example.com")
.allowedMethods("*");
③ CorsFilter(过滤器,位置最外,适合与安全框架共存)
注意:Allow-Credentials=true 时 Allow-Origin 不能是 *,必须指定具体域名。
四、总结
- 拦截器:preHandle(可中断)/postHandle/afterCompletion 三回调;注册指定路径与顺序;与过滤器分层不同------过滤器在 Servlet 外圈,拦截器在 MVC 内圈。
- 异常处理:HandlerExceptionResolver 链;@RestControllerAdvice 全局按类别转统一错误响应;未知异常记日志告警、不外泄堆栈。
- CORS :浏览器同源策略的例外机制,靠响应头声明;简单请求直发、非简单先预检(OPTIONS);配置三种方式,带凭证时不能
*。
五、常见高频面试题
1. HandlerInterceptor 的三个方法分别在什么时候执行?
要点:preHandle 在 Handler 执行前调用,返回 false 则中断(后续拦截器与 Handler 都不执行),常用于鉴权;postHandle 在 Handler 执行后、视图渲染前,可修改 Model;afterCompletion 在请求完全完成后(含异常),用于清理资源(如解绑 ThreadLocal)。多个拦截器按注册顺序形成链,整体是洋葱模型。
2. 过滤器(Filter)和拦截器(Interceptor)的区别?
要点:所属层与位置不同。Filter 是 Servlet 规范,在容器层、DispatcherServlet 之外,对所有请求生效(含静态资源),只能拿到原始请求响应,常用于编码、压缩、粗粒度鉴权、跨域;HandlerInterceptor 是 Spring MVC 层,在 DispatcherServlet 之内,能拿到 Handler/方法信息,作用于进入 MVC 的请求,常用于登录校验、权限、埋点。简记:过滤器在外圈管请求,拦截器在内圈管处理器。
3. Spring MVC 的异常处理机制是怎样的?
要点:Handler 抛出的异常被 DispatcherServlet 交给 HandlerExceptionResolver 链:先找 @ExceptionHandler(方法级,再到 @ControllerAdvice 全局),再看 @ResponseStatus,最后 DefaultHandlerExceptionResolver 映射标准异常(如 NoHandlerFound→404);都处理不了转发 /error。实践用 @RestControllerAdvice 统一处理,按异常类型返回统一错误响应。
4. @ControllerAdvice 的原理和用途?
要点:@ControllerAdvice 是一个特殊的 @Component,通过 AOP 横切所有 @Controller,提供全局的异常处理(@ExceptionHandler)、数据绑定(@ModelAttribute)、数据预处理(@InitBinder)。配合 @ResponseBody 的 @RestControllerAdvice 是全局异常处理的标准实现,把各类异常统一转为规范错误响应,避免每个控制器重复写异常逻辑。
5. 什么是 CORS?为什么会有跨域问题?
要点:浏览器同源策略限制脚本跨源(协议+域名+端口不同)访问资源,是安全机制。跨源 XHR/fetch 默认被拦截。CORS 让服务端用响应头声明允许的跨源访问:Access-Control-Allow-Origin 等。注意跨域是浏览器行为,服务端间调用无此问题。非简单请求会先发 OPTIONS 预检,服务端放行后才发真实请求。
6. 简单请求和非简单请求的区别?预检请求是什么?
要点:简单请求满足:方法为 GET/HEAD/部分 POST,且头与 Content-Type 在安全集合内------直接发出,靠响应头判定是否允许。非简单请求(如 PUT/DELETE、带自定义头、application/json)会先发 OPTIONS 预检,携带将要用的方法与头,服务端以 Access-Control-Allow-* 响应;允许后浏览器才发真实请求。预检可被缓存(Access-Control-Max-Age)减少开销。
7. 跨域时携带 Cookie 要注意什么?
要点:需要服务端 Access-Control-Allow-Credentials: true,且请求带 withCredentials=true;此时 Access-Control-Allow-Origin 不能是通配符 *,必须指定具体域名(浏览器强制)。另外 SameSite Cookie 属性也会影响跨站携带。安全上要谨慎:允许跨源携带凭证等于把接口暴露给该源的前端,确保源可信。
8. 全局异常处理时,未知异常(Exception)该怎么处理?
要点:兜底捕获但严格处理:① 记录完整堆栈到日志并触发告警(这是唯一能发现未预期错误的信号);② 响应返回通用友好消息(如"系统繁忙"+ 错误码/追踪 ID),绝不回传堆栈(安全+体验);③ 可返回追踪 ID 便于用户反馈与日志关联。区分对待:业务异常返回具体码,校验异常返回字段明细,未知异常通用化。
9. 登录校验、权限控制应该放在过滤器还是拦截器?
要点:取决于层次与需求。粗粒度/与容器能力相关的(如 Token 解码、跨域、编码)放过滤器;与业务路由相关的(按路径的登录态校验、细粒度权限、访问日志埋点)放拦截器------它能拿到 Handler 信息(如方法上的权限注解)。若用 Spring Security,则由过滤器链统一承担认证授权。关键:避免同一逻辑两处重复。
10. afterCompletion 和 postHandle 的区别?资源清理放哪个?
要点:postHandle 在 Handler 执行后、视图/响应处理前调用------若 Handler 抛异常则不执行;afterCompletion 在请求完全完成后调用,无论是否异常都会执行。所以资源清理(解绑 ThreadLocal、释放上下文)必须放 afterCompletion------放 postHandle 会在异常路径漏清理,造成内存泄漏或上下文串扰。
