SpringBoot HandlerInterceptor实战

1.拦截器介绍

HandlerInterceptorAdapter 是 Spring MVC 中的拦截器(Interceptor)类,用于拦截请求的处理流程,包括请求的预处理、后处理和渲染视图等操作。它可以用于实现一些全局性的处理逻辑,例如日志记录、权限验证、请求参数预处理等

1.1 接口方法说明

1.1.1 preHandle

预处理回调方法,实现处理器的预处理。可以在这里进行权限验证、请求参数处理等。

  • 返回值:true表示继续流程;false表示流程中断,不会继续调用其他的拦截器或处理器

1.1.2 postHandle

后处理回调方法,实现处理器(controller)的后处理,但在渲染视图之前,此时我们可以通过modelAndView对模型数据进行处理或对视图进行处理

1.1.3 afterCompletion

整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中

1.2 基本使用示例

1.2.1 实现自定义拦截器

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 在请求处理之前执行,可以进行权限验证等操作
        System.out.println("Pre Handle method is Calling");
        return true; // 返回true表示继续执行后续操作,返回false将中断请求处理流程
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           org.springframework.web.servlet.ModelAndView modelAndView) throws Exception {
        // 在请求处理之后、视图渲染之前执行,可以修改数据模型等操作
        System.out.println("Post Handle method is Calling");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        // 在整个请求完成之后执行,可以用于清理资源等操作
        System.out.println("After Completion method is Calling");
    }
}

在上述示例中,MyInterceptor 继承了 HandlerInterceptorAdapter 类,并重写了其三个方法(preHandle、postHandle 和 afterCompletion)

1.2.2 注册拦截器

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 {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
    }
}

在上述配置中,addInterceptors 方法中注册了 MyInterceptor 拦截器,并设置了拦截的路径为 “/**”,表示拦截所有请求。你可以根据实际需求修改拦截的路径

2. 实战

2.1 实战一:通过拦截器获取controller的方法注解鉴权

2.1.1 定义权限注解

@Retention(RUNTIME)
@Target(METHOD)
public @interface ApiPermission {
    String value() default "";//默认为空,因为名字是value,实际操作中可以不写"value="
}

2.1.2 实现权限拦截器(实现HandlerInterceptor接口)

@Component
public class PermissionInterceptor implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(PermissionInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        LOGGER.info("进入拦截器");
        if(handler instanceof HandlerMethod) {
            HandlerMethod h = (HandlerMethod)handler;
         ApiPermission apiPermission= h.getMethodAnnotation(ApiPermission.class);
        if (easyLog != null) {
        	//TODO 判断当前用户是否拥有权限信息
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            // 创建一个JSON对象
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("code", "403");
            jsonObject.put("message", "用户无权限操作");

            // 设置响应内容类型为JSON,使用UTF-8编码(不使用中文会乱码)
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().print(jsonObject.toString());
            return false;
        }
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

}

获取controller类方法注解: h.getMethod().getDeclaringClass().getAnnotation(ApiPermission.class);

2.1.3 配置拦截器

@Component
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private PermissionInterceptor permissionInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(permissionInterceptor).addPathPatterns("/**");;
    }
}

2.1.4 controller使用权限注解

@Controller
@RequestMapping("/api/user")
public class UserController {

	@Autowired
	private UserService userService;

	@ApiPermission("user:list")
	@RequestMapping(value = "/list", method=RequestMethod.GET)
	@ResponseBody
	public ApiResult<PageResult<Page<User>>> listByPage(User user, PageResult pageResult) {
		Page<User> page = this.userService.findListByPage(user, new Page(pageResult.getPageNum(), pageResult.getPageSize()));
		return ApiResult.wrapPage(page);
	}
}	

2.2 实战二:通过拦截器实现第三方HTTP请求签名

签名是在Web开发中常用的一种安全验证方式,用于确保请求的来源和完整性

2.2.1 接口签名工具类

方法内部的步骤如下:

  1. 构建待签名字符串 toSign: 将HTTP请求的方法、URI和时间戳拼接成一个字符串。
  2. 初始化MAC算法: 使用HmacSHA256算法(一种基于哈希函数的消息认证码算法)进行签名。在这里,使用Mac类来初始化HmacSHA256算法,传入SecretKeySpec对象作为密钥。
  3. 计算签名: 将待签名字符串 toSign 使用UTF-8编码转换为字节数组,然后通过HMAC-SHA1算法进行计算,生成签名的字节数组。
  4. Base64编码: 将生成的签名字节数组进行Base64编码,得到最终的签名字符串。
  5. 返回签名结果: 返回生成的签名字符串。

最终,该方法返回一个基于给定HTTP请求方法、URI、时间戳和密钥生成的签名字符串。该签名字符串可以用于在服务器端验证请求的合法性,确保请求的来源和完整性,防止恶意请求和数据篡改。

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
 * 服务认证签名工具类
 */
public class ServiceSignUtil {

    public static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
    public static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
    // 定义其他算法常量...

    private ServiceSignUtil() {
    }

    /**
     * 生成服务认证签名
     *
     * @param httpMethod  HTTP请求方法,例如GET、POST等
     * @param uri         请求的URI(路径)
     * @param utc         请求的时间戳(通常是UTC格式的时间戳)
     * @param secretKey   用于签名的密钥
     * @param algorithm   签名算法,使用上述定义的常量
     * @return            返回生成的签名字符串
     * @throws NoSuchAlgorithmException  如果指定的算法不存在
     * @throws InvalidKeyException       如果密钥无效
     */
    public static String signature(String httpMethod, String uri, String utc, String secretKey, String algorithm)
            throws NoSuchAlgorithmException, InvalidKeyException {
        String toSign = httpMethod + uri + utc;
        Mac mac = Mac.getInstance(algorithm);
        mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), algorithm));
        byte[] signatureBytes = mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signatureBytes).trim();
    }
}

2.2.2 实现安全拦截器(实现HandlerInterceptor接口)

public class ApiSecurityInterceptor extends HandlerInterceptorAdapter {
    private static final Logger LOGGER = LoggerFactory.getLogger(ApiSecurityInterceptor.class);

    private String accessKey = "ak";
    private String secretKey = "sk";

 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  {
         
        String accessId = request.getHeader(RemoteCallConstants.HEADER_ACCESS_ID);
        String signature = request.getHeader(RemoteCallConstants.HEADER_SIGNATURE);
        String utc = request.getHeader(RemoteCallConstants.HEADER_TIMESTAMP);
        // 微服务标识暂时不用
        //String appService = request.getHeader(RemoteCallConstants.HEADER_APP_SERVICE);
        // 鉴权信息为空则认为时非法请求
        if (StringUtils.isBlank(accessId) || StringUtils.isBlank(signature) || StringUtils.isBlank(utc)) {
            throw new RuntimeException("MessageEnum.ILLEGAL_REQUEST");
        }
 
        DateTime dateTime = DateUtil.parseUTC(utc);
        // 5分钟内请求有效,超过则认为请求过期
        if (DateTime.now().between(dateTime).between(DateUnit.MINUTE) >= 5L) {
            throw new RuntimeException("MessageEnum.REQUEST_HAS_EXPIRED");
        }
 
        // access-key不匹配则为鉴权失败
        if (!StringUtils.equals(accessKey, accessId)) {
            throw new RuntimeException("MessageEnum.API_AUTHENTICATION_FAILED");
        }
 
        String uri = request.getServletPath();
        String httpMethod = request.getMethod();
 
        try {
            String currentSignature = ServiceSignUtil.signature(httpMethod, uri, utc, secretKey, "HmacSHA256");
            // 签名不一致
            if (!StringUtils.equals(currentSignature, signature)) {
                throw new RuntimeException("MessageEnum.API_AUTHENTICATION_FAILED");
            }
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            // 签名生成失败
            throw new RuntimeException("MessageEnum.API_AUTHENTICATION_FAILED");
        }
 
        return true;
    }

    public String getAccessKey() {
        return accessKey;
    }

    public void setAccessKey(String accessKey) {
        this.accessKey = accessKey;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }
}
  • 常量类
public class RemoteCallConstants {
 
    public static final String HEADER_ACCESS_ID = "accessId";
 
    public static final String HEADER_TIMESTAMP = "timestamp";
 
    public static final String HEADER_SIGNATURE = "signature";
 
    public static final String QUERY_LANG = "lang";
 
    public static final String UTC_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
 
    private RemoteCallConstants() {
    }
}

2.2.3 注册拦截器

@Configuration
public class InterceptorConfig {
    private static final Logger LOGGER = LoggerFactory.getLogger(InterceptorConfig.class);

    @Value("${easy-api.security.path-patterns:/**/api/**}")
    private String pathPatterns;
    /**
     * @ConfigurationProperties的第二种用法@Bean + @ConfigurationProperties
     * 注意!对应bean中的属性需要setter/getter方法,否则无法注入
     */
    @Bean
    @ConfigurationProperties(prefix = "easy-api.security")
    public ApiSecurityInterceptor apiSecurityInterceptor() {
        return new ApiSecurityInterceptor();
    }

    @Bean
    public WebMvcConfigurer apiSecurityInterceptorConfigurer() {
        LOGGER.info("【easy-mode】@EnableEasyApiSecurity[apiSecurityInterceptor]--拦截:{}", pathPatterns);
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(apiSecurityInterceptor()).addPathPatterns(pathPatterns);
            }
        };
    }
}

2.2.4 application.yml配置

easy-api:
  security:
    access-key: ak
    secret-key: sk
    path-patterns: /**

访问接口ak sk及鉴权接口 通过配置文件灵活定义。(这里演示方便就写死, 生产环境一般每个调用方一对)

3. 拓展

3.1拦截器(Interceptor)和过滤器(Filter)的执行顺序和区别

3.1.1 过滤器(Filter)

  • 容器:它依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
  • 技术:在实现上,基于函数回调。
  • 优缺点:它可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时init一次。
  • 使用过滤器的目的: 是用来做一些过滤操作,获取我们想要获取的数据,比如:在JavaWeb中,对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者Controller进行业务逻辑操作。
  • 常用的场景是:
    • 过滤: 在过滤器中修改字符编码(CharacterEncodingFilter)、在过滤器中修改HttpServletRequest的一些参数(XSSFilter(自定义过滤器)),如:过滤低俗文字、危险字符等。
    • 身份认证: 服务器提供的资源如果是受保护,在过滤器实现session认证, 比如shiro(权限框架)
    • 请求转发: 改变特定请求路径, 比如SSO的cas的客户端filter, 302转发cas登录页,登录后带ticket参数发起校验并获取用户信息。
    • 日志记录: 结合log4j的MDC记录请求的IP/用户等信息,创建requestId用于请求链路的追踪

3.1.2 拦截器(Interceptor)

  • 容器:依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架
  • 技术:在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。
  • 优缺点:由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。缺点:依赖于web框架
  • 使用拦截器目的: 一般filter会配置在执行web框架之前,控制范围比较大,基本filter能满足大部分需求,拦截器使用一般在filter实现起来不够友好地方。比如获取容器上下文信息
  • 常用的场景是:
    • 权限认证: 通过拦截器的HandlerMethod可以很容易获取controller类或者方法的注解,对于接口的权限控制十分便利。

3.1.3 总结

Interceptor结合容器更"靠近"业务, filter更倾向基础通用的配置。

4. 参考文章

  1. SpringMVC拦截器中获得Controller方法名和注解信息(用于验证权限)
  2. 拦截器(Interceptor)和过滤器(Filter)的执行顺序和区别
  3. 过滤器 和 拦截器 6个区别,别再傻傻分不清了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值