Spring MVC 拦截器、Filter、CORS 跨域:从零写到配置
拦截器怎么写?Filter 和拦截器有什么区别?跨域 CORS 怎么配?这三个问题一篇讲完,附带完整的 WebMvcConfigurer 配置类。
Spring MVC 拦截器、Filter、CORS 跨域:从零写到配置
前言
这篇文章是"从问答题到能写出来"的学习记录。从不会写拦截器,到 Filter 区别,到 CORS 跨域,一路写下来整理成这篇。
一、拦截器(HandlerInterceptor)
拦截器是什么
拦截器是 Spring MVC 的组件,在请求到达 Controller 之前、之后、以及视图渲染完之后,插入自定义逻辑。
拦截器接口 HandlerInterceptor 有三个方法:
public interface HandlerInterceptor {
// Controller 执行前。返回 false 就拦截下来,不放行
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler);
// Controller 执行后,视图渲染前
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView);
// 请求完成后(视图渲染完)
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
写一个登录检查拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
response.setStatus(401);
response.getWriter().write("未登录");
return false; // 不放行
}
try {
Long userId = jwtUtil.parseToken(token);
request.setAttribute("userId", userId); // 存到 request 域,给 Controller 用
return true; // 放行
} catch (Exception e) {
response.setStatus(401);
response.getWriter().write("token 无效");
return false;
}
}
}
注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**") // 要拦截的路径
.excludePathPatterns("/api/login"); // 白名单
}
}
二、Servlet 基础回顾
拦截器和 Filter 里经常会用到 Servlet 原生 API,这里整理最常用的几个:
HttpServletRequest
// 取请求参数(用户传的)
request.getParameter("id"); // 单个参数
request.getParameterValues("hobby"); // 多个值
request.getParameterMap(); // 所有参数
// 存/取/删属性(服务端内部传数据)
request.setAttribute("key", value);
request.getAttribute("key");
request.removeAttribute("key");
getParameter 和 getAttribute 的区别:
| getParameter | getAttribute | |
|---|---|---|
| 数据来源 | 浏览器(URL/表单) | 服务端代码 |
| 能否设值 | 只读 | 可以 set |
| 类型 | String | 任意对象 |
HttpServletResponse
// 状态码
response.setStatus(401);
response.setStatus(404);
response.setStatus(500);
// 写响应体
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{"code": 401}");
// 重定向
response.sendRedirect("/login");
// 设置响应头
response.setHeader("Access-Control-Allow-Origin", "*");
在 Spring Boot 里这些 API 用得少(因为有 @RequestParam、@RequestBody、return Result),但在拦截器和 Filter 里还是得手写。
三、Filter(过滤器)
Filter 是什么
Filter 是 Servlet 规范的组件,比 Spring MVC 更底层。请求一进入服务器就经过 Filter,然后才到 Spring MVC 的 DispatcherServlet。
位置关系:
浏览器请求 → Filter → DispatcherServlet → Interceptor → Controller
Filter 能拦截所有请求(包括静态资源),Interceptor 只能拦截 Controller。
写一个请求日志 Filter
@Component
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
long start = System.currentTimeMillis();
System.out.println("请求开始: " + req.getRequestURI());
chain.doFilter(request, response); // 放行
long cost = System.currentTimeMillis() - start;
System.out.println("请求结束,耗时: " + cost + "ms");
}
}
Filter 没有三个方法,只有 doFilter 一个方法。chain.doFilter() 之前是请求阶段,之后是响应阶段。
Filter vs Interceptor
| Filter | Interceptor | |
|---|---|---|
| 所属 | Servlet 规范 | Spring MVC |
| 拦截范围 | 所有请求(含静态资源) | 只拦截 Controller |
| 接口方法 | doFilter | preHandle / postHandle / afterCompletion |
| 能否拿到 Controller 信息 | 不能 | 能(handler 参数) |
| 能否操作 ModelAndView | 不能 | 能 |
四、CORS 跨域
跨域是什么
前端在 localhost:5173,后端在 localhost:8080,浏览器发现端口不同,就会拦截响应。这不是后端的错,是浏览器的安全策略——请求其实到了后端,但浏览器看到响应头没有 Access-Control-Allow-Origin,就把数据丢弃了。
方案一:自己写 Filter 解决
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest) request;
resp.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
resp.setHeader("Access-Control-Allow-Credentials", "true");
// 预检请求直接返回
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
resp.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
}
OPTIONS 请求叫预检请求。浏览器发非简单请求(如带 Authorization 头的请求)之前,先发一个 OPTIONS 问问服务器"你允许吗",再发真正的请求。
方案二:Spring Boot 内置配置(推荐)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
// 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login");
}
// 跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 允许跨域的路径
.allowedOriginPatterns("*") // 允许的来源(开发用 *)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
生产环境要把 allowedOriginPatterns("*") 改成具体域名。
addCorsMappings 比自己写 Filter 好在哪
- 不需要手动处理 OPTIONS 预检请求
- 不需要手动 set 响应头
- Spring MVC 内部已经封装好了
五、完整配置类一览
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
一个 WebMvcConfigurer 接口可以同时配置拦截器、跨域、静态资源映射,按需重写里面的默认方法就行。