Web核心开发:拦截器、过滤器、跨域、统一返回

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);
}

这样全局统一返回就稳了,接口层只管业务逻辑,返回格式交给框架兜底。

相关推荐
dsyuan0011 小时前
基于 Vue3 + ElementPlus 极简搭建仿钉钉流程设计器,简洁架构、极易扩展,纯div+css布局
css·架构·钉钉
隔窗听雨眠2 小时前
无辅助损失函数的负载均衡:DeepSeek MoE架构的核心突破
运维·架构·负载均衡
ZGIAI11 小时前
ZGI 让那些"等你去处理"的事,真正跑起来
人工智能·架构
ZGIAI11 小时前
ZGI:别再做Agent Demo了,先问问它在业务里能不能撑过下周三
人工智能·架构
黑马程序员毕设15 小时前
基于Java的医院药品管理系统的优化设计与实现
java·开发语言·spring boot·小程序·架构·课程设计·毕设
头茬韭菜15 小时前
第 01 篇:「架构鸟瞰与进程启动链路」—— JobManager / TaskManager 从零长出来的完整调用链
架构·flink
CaseyWei15 小时前
Harness 架构 Multi‑Agent(多智能体)完整深度解析
人工智能·ai·架构·harness
源代码•宸17 小时前
前置准备:定时微服务背景和现状
开发语言·经验分享·后端·微服务·云原生·架构·golang
天远数科17 小时前
零信任架构实战:基于天远身份证OCR构建自动化高并发移动支付网关
人工智能·架构·自动化·ocr