掌握Web组件(二):使用拦截器(Interceptor)实现认证与授权

摘要: 在上一章,我们学习了如何使用Filter作为请求的“前哨”来处理一些底层的、与业务无关的横切任务。然而,Filter的一大局限是它无法感知到Spring MVC的内部世界,比如它不知道请求将由哪个Controller的哪个方法来处理。当我们想实现更精细的控制,例如“只有管理员角色的用户才能调用deleteUser方法”时,Filter就显得力不从心了。为此,Spring MVC提供了自己的拦截机制——拦截器(HandlerInterceptor)。本章,我们将深入学习这个强大的组件,了解它如何工作在Spring MVC的核心流程中,并最终通过实战,用它来构建一套优雅、灵活的API认证与授权体系。


引言:当Filter“不够聪明”时

Filter像是一个忠诚但“眼神不太好”的门卫,他能检查所有进出的人(HTTP请求),但看不清他们具体要去哪个房间(Controller方法)。如果我们的需求是:“所有进入大楼的人都必须佩戴工牌”,Filter可以胜任。但如果需求变成:“只有VIP客户才能进入888号房间”,门卫就没辙了,因为不认识房间号。

这就是拦截器(Interceptor)大显身手的舞台。拦截器是Spring MVC框架自己提供的“楼层管理员”,他工作在DispatcherServlet之后,能清楚地知道每个请求的目的地——即哪个HandlerMethod(控制器方法)将会被执行。这使得拦截器能够实现基于业务逻辑的、更智能的拦截与处理。

一、拦截器 vs. 过滤器:更深层次的剖析

在投入实战之前,我们必须清晰地理解拦截器和过滤器的核心区别。

Spring MVC 框架
Servlet 容器 (Tomcat)
Interceptor 1: preHandle
Interceptor 2: preHandle
Controller Method
Interceptor 2: postHandle
Interceptor 1: postHandle
View Rendering
Interceptor 2: afterCompletion
Interceptor 1: afterCompletion
Filter 1
HTTP Request
Filter 2
DispatcherServlet
HTTP Response
特性过滤器 (Filter)拦截器 (Interceptor)
归属Java Servlet规范,Web容器管理Spring MVC框架一部分,Spring容器管理
执行时机DispatcherServlet之前DispatcherServlet之后,Controller方法执行前后
上下文感知只能访问原始的HttpServletRequest/Response能访问HandlerMethodModelAndView等Spring上下文
控制粒度粗粒度,基于URL Pattern拦截细粒度,可基于处理器方法、注解等进行拦截
典型用途字符编码、GZIP压缩、低级别安全过滤认证、授权、日志记录、修改模型数据

HandlerInterceptor接口的三个方法

  • preHandle(req, res, handler): 执行于Controller方法之前handler参数是关键,我们可以从中获取即将执行的方法信息。此方法返回true则继续执行,返回false则中断请求。认证授权的核心逻辑在此实现
  • postHandle(req, res, handler, mv): 执行于Controller方法之后,视图渲染之前。可以用来修改ModelAndView对象,向页面添加公共数据。
  • afterCompletion(req, res, handler, ex): 在整个请求处理完毕后(包括视图渲染)执行。通常用于资源清理工作。无论是否发生异常,只要preHandle返回true,此方法总会被调用。

二、实战:使用拦截器实现Token认证

假设我们的API需要一个简单的Token认证:所有/api/v1/secure/**路径下的请求,都必须在HTTP头中携带一个有效的X-Auth-Token

1. 创建认证拦截器

package com.example.myfirstapp.interceptor;

import com.example.myfirstapp.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

@Component // 虽然拦截器本身需要注册,但声明为Bean可以方便地在其中注入其他服务
public class AuthInterceptor implements HandlerInterceptor {

    private static final String VALID_TOKEN = "secret-token-12345";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        
        String token = request.getHeader("X-Auth-Token");

        if (!StringUtils.hasText(token) || !VALID_TOKEN.equals(token)) {
            // 认证失败
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
            response.setContentType("application/json;charset=UTF-8");
            
            // 手动构造一个标准的错误响应
            Result<Void> errorResult = Result.error(401, "Unauthorized: Invalid or missing token.");
            
            // 使用Jackson将对象转换为JSON字符串并写入响应
            ObjectMapper objectMapper = new ObjectMapper();
            response.getWriter().write(objectMapper.writeValueAsString(errorResult));
            
            return false; // 中断请求
        }

        // 认证成功,放行
        return true;
    }
}

2. 注册拦截器

与Filter不同,拦截器需要在一个实现了WebMvcConfigurer的配置类中显式注册。

package com.example.myfirstapp.config;

import com.example.myfirstapp.interceptor.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/v1/users/**") // 1. 指定要拦截的路径
                .excludePathPatterns("/api/v1/users/login"); // 2. 指定要排除的路径
    }
}

代码解读:

  1. addPathPatterns: 定义了此拦截器生效的URL模式。这里我们拦截所有用户相关的API。
  2. excludePathPatterns: 定义了需要排除在外的URL模式。通常,登录、注册等接口是不需要认证的。

3. 效果演示

  • 不带Token访问: curl -i http://localhost:8080/api/v1/users
    HTTP/1.1 401
    Content-Type: application/json;charset=UTF-8
    ...
    {"code":401,"message":"Unauthorized: Invalid or missing token.","data":null}
    
  • 带正确Token访问: curl -i -H "X-Auth-Token: secret-token-12345" http://localhost:8080/api/v1/users
    HTTP/1.1 200
    ...
    {"code":200,"message":"Success","data":[{"id":1,...}]}
    

三、进阶:结合自定义注解实现动态授权

认证(Authentication)解决了“你是谁”的问题,而授权(Authorization)解决的是“你能做什么”的问题。让我们实现一个更高级的功能:只有特定角色的用户才能调用某些方法。

1. 创建自定义注解@RequiresRole

package com.example.myfirstapp.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
    String value(); // 用于指定需要的角色,例如 "ADMIN"
}

2. 改造AuthInterceptor以支持授权

// 在AuthInterceptor中添加...
import org.springframework.web.method.HandlerMethod;
import com.example.myfirstapp.annotation.RequiresRole;

// ... preHandle方法修改如下
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
    
    // ... (前面的Token认证逻辑保持不变)

    // 检查是否需要角色授权
    if (handler instanceof HandlerMethod) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        RequiresRole requiresRole = handlerMethod.getMethodAnnotation(RequiresRole.class);

        if (requiresRole != null) {
            // 此方法需要角��验证
            String requiredRole = requiresRole.value();
            
            // 伪代码:从Token中解析用户角色,实际应从数据库或缓存获取
            String userRole = getUserRoleFromToken(token); 

            if (!requiredRole.equals(userRole)) {
                // 角色不匹配,授权失败
                response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
                response.setContentType("application/json;charset=UTF-8");
                Result<Void> errorResult = Result.error(403, "Forbidden: Insufficient privileges.");
                response.getWriter().write(new ObjectMapper().writeValueAsString(errorResult));
                return false;
            }
        }
    }
    
    return true; // 放行
}

// 伪代码方法
private String getUserRoleFromToken(String token) {
    // 在真实应用中,这里会解析JWT或查询数据库来获取用户角色
    // 为演示方便,我们简单处理
    if (VALID_TOKEN.equals(token)) {
        return "ADMIN"; // 假设这个token对应的用户是ADMIN
    }
    return "GUEST";
}

3. 在Controller中使用注解

// 在UserController的deleteUser方法上添加注解
@DeleteMapping("/{id}")
@RequiresRole("ADMIN") // 只有ADMIN角色的用户才能删除
public Result<Void> deleteUser(@PathVariable Long id) {
    // ... 删除逻辑
}

现在,即使用户提供了正确的Token,只要其角色不是ADMIN,也无法调用deleteUser方法,实现了精细到方法级别的授权控制。

总结

Spring MVC拦截器是实现动态、上下文感知拦截的利器,是构建Web应用安全体系的核心组件。

  • 拦截器 vs. 过滤器: 拦截器更“聪明”,能感知到Spring MVC的上下文,适合处理与业务相关的拦截,如认证和授权。
  • 核心方法: preHandle是实现拦截逻辑的关键,可以通过返回false来中断请求。
  • 注册与配置: 需要通过WebMvcConfigurer来注册,并可以灵活地配置拦截和排除的路径。
  • 高级用法: 结合自定义注解,可以实现极其灵活和声明式的授权控制,大大提升了代码的可读性和可维护性。

预告: 我们已经掌握了请求处理的“前置”和“环绕”技术(Filter和Interceptor)。但Spring MVC的灵活性远不止于此。如果我们想自定义Controller方法的参数本身是如何被创建和注入的呢?例如,能否创建一个@CurrentUser注解,直接在方法参数中获取当前登录的用户对象,而无需每次都从requestSecurityContext中手动提取?下一章,我们将探索另一个强大的Web组件:掌握Web组件(三):使用HandlerMethodArgumentResolver自定义参数解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨小威v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值