Web核心开发:拦截器、过滤器、跨域、统一返回
后端接口写多了就知道,业务逻辑只占三成,剩下七成是"防呆设计"------拦截没登录的、处理跨域的、统一返回格式的。这篇把Web层的"基础设施"一次搭好。
一、SpringBoot Web开发全景
1.1 DispatcherServlet流程
所有请求进入SpringBoot,第一站是 DispatcherServlet(前端控制器),核心流程如下:
markdown
客户端请求
→ DispatcherServlet
→ HandlerMapping(找到对应的Controller方法)
→ HandlerInterceptor(拦截器 preHandle)
→ Controller执行业务逻辑
→ HandlerInterceptor(拦截器 postHandle)
→ 视图渲染/结果处理
→ HandlerInterceptor(拦截器 afterCompletion)
→ 返回响应
注意过滤器(Filter)在DispatcherServlet之前就执行了,这是拦截器和过滤器的关键区别。
二、拦截器(Interceptor)
2.1 HandlerInterceptor接口
拦截器是Spring MVC提供的机制,核心接口:
java
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);
}
三个阶段的执行时机和用途:
| 方法 | 执行时机 | 典型用途 |
|---|---|---|
| preHandle | Controller前 | 登录校验、权限检查、日志记录 |
| postHandle | Controller后 | 统一修改返回数据 |
| afterCompletion | 请求结束 | 资源清理、异常处理 |
2.2 登录拦截器实战
java
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头获取token
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
return false;
}
// 校验token逻辑...
return true; // 放行
}
}
2.3 注册拦截器
光写了拦截器不够,得注册到Spring MVC里才能生效:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/login", // 排除登录接口
"/api/register",
"/api/captcha"); // 排除验证码接口
}
}
三、过滤器(Filter)
3.1 Filter接口
过滤器是Servlet规范中的机制,在请求到达Spring MVC之前就执行:
java
@Component
@WebFilter(urlPatterns = "/*", filterName = "requestLogFilter")
public class RequestLogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
long start = System.currentTimeMillis();
log.info("请求开始: {} {}", req.getMethod(), req.getRequestURI());
chain.doFilter(request, response); // 放行到下一个过滤器或目标资源
long cost = System.currentTimeMillis() - start;
log.info("请求结束, 耗时: {}ms", cost);
}
}
3.2 过滤链执行顺序
多个Filter组成一条链,按顺序执行。可以用 @Order 注解控制顺序:
java
@Component
@Order(1) // 数字越小越先执行
public class FirstFilter implements Filter { ... }
@Component
@Order(2)
public class SecondFilter implements Filter { ... }
四、拦截器 vs 过滤器对比
| 对比项 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 所属规范 | Servlet规范 | Spring MVC |
| 执行时机 | DispatcherServlet前 | DispatcherServlet内 |
| 能否访问Spring Bean | 需额外处理 | 直接注入 |
| 能否获取Handler信息 | 不能 | 能(知道哪个Controller方法) |
| 典型场景 | 编码设置、日志、安全过滤 | 登录校验、权限控制 |
简单记:过滤器管"请求进出",拦截器管"Controller前后"。
五、跨域问题解决
5.1 什么是跨域
浏览器同源策略:协议、域名、端口任一不同就是跨域。前端 localhost:5173 调后端 localhost:8080 就跨域了,浏览器会拦截响应。
5.2 三种解决方案
方案一:@CrossOrigin注解(粒度最细,单个接口)
java
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:5173")
public class ApiController { ... }
方案二:WebMvcConfigurer全局配置
java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true) // 允许携带Cookie
.maxAge(3600); // 预检缓存时间(秒)
}
}
方案三:CorsFilter全局过滤器(推荐,优先级最高)
java
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:5173");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
5.3 跨域常见坑点
坑一:OPTIONS预检请求被拦截
非简单请求(如POST+JSON)浏览器会先发OPTIONS请求"探路"。如果登录拦截器把OPTIONS请求拦了,前端直接报跨域错误。
解决:拦截器中放行OPTIONS:
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if ("OPTIONS".equals(request.getMethod())) {
return true; // 预检请求直接放行
}
// 正常校验逻辑...
}
坑二:Cookie跨域
跨域携带Cookie需要同时满足:
- 后端设置
allowCredentials(true) allowedOrigins不能用*,必须指定具体域名- 前端请求设置
withCredentials: true
六、统一返回封装
6.1 Result类设计
别让每个接口返回格式都不一样,统一规范:
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
}
6.2 ResponseAdvice全局统一返回
用 ResponseBodyAdvice 实现全局自动包装,Controller里直接返回业务数据,框架自动包成Result:
java
@RestControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 排除Result类型本身和String类型(避免序列化问题)
return !returnType.getParameterType().equals(Result.class)
&& !returnType.getParameterType().equals(String.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return Result.success(body);
}
}
Controller代码变得极简:
java
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getById(id); // 直接返回User,框架自动包装成Result
}
}
返回结果统一是:
json
{
"code": 200,
"message": "操作成功",
"data": { "id": 1, "name": "张三" }
}
6.3 String返回的坑
ResponseBodyAdvice 处理String返回值时会有序列化问题。因为Spring MVC对String类型默认用 StringHttpMessageConverter,而返回Result对象后会报类型转换异常。
解决方案:给String返回值单独处理,手动转成JSON:
java
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return JSONUtil.toJsonStr(Result.success(body));
}
return Result.success(body);
}
这样全局统一返回就稳了,接口层只管业务逻辑,返回格式交给框架兜底。