SpringBoot之配置拦截器

  • A+
所属分类:Java片段

拦截器简介

拦截器的原理很简单,是 AOP(Aspect-Oriented Programming) 的一种实现,专门拦截对动态资源的后台请求,即拦截对控制层的请求,在在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。

拦截器是动态拦截Action调用的对象。它提供了一种机制可以使开发者可以定义在一个action执行的前后执行的代码,也可以在一个action执行前阻止其执行。同时也是提供了一种可以提取action中可重用的部分的方式

定义一个拦截器只需要实现HandlerInterceptor,重写其中的方法,再将其添加到WebMvcConfig中即可进行拦截。而对于HandlerInterceptor接口,其中有三个default关键字修饰的方法,

default修饰的方法也叫虚拟扩展方法。是指,在接口内部包含了一些默认的方法实现(也就是接口中可以包含方法体,这打破了Java之前版本对接口的语法限制),从而使得接口在进行扩展的时候,不会破坏与接口相关的实现类代码

HandlerInterceptor接口

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {

    /**
    *在业务处理器处理请求之前被调用
    */
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    /**
    *在业务处理器处理请求执行完成后,生成视图之前执行
    */
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }


    /**
    *在DispatcherServlet完全处理完请求后被调用,可用于清理资源等
    */
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

preHandle方法:

该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在这个方法执行之前。所以 preHandle() 方法可以决定是否将请求放行,这是通过返回值来决定的,返回 true 则放行,返回 false(拦截) 则不会向后执行。

postHandle方法:

该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在执行完了该方法,但是在DispatcherServlet 视图渲染之前。所以在这个方法中有个 ModelAndView 参数,可以在此做一些修改动作。

afterCompletion方法:

该方法是在整个请求处理完成后(包括视图渲染)执行,这时做一些资源的清理工作,这个方法只有在 preHandle() 被成功执行后并且返回 true才会被执行。

拦截器的定义

新建一个类,并实现HandlerInterceptor接口,重写里面的三个方法(一般重写preHandle即可),这里我们模拟一下用户未登录则进行拦截,用户登录成功后,将token存放到header中,每次请求时都带上token访问(sessioncookie实现方式大同小异)

package com.kjyfx.config;

import net.minidev.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2020/12/14 22:00
 * @Created by moyun
 */
@Component
public class LoginHandlerInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LoginHandlerInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getRequestURL().toString();
        logger.info("拦截到路径:{}",url);

        Map<String, Object> map = new HashMap<>();//失败后返回提示信息

        String authToken = request.getHeader("AuthToken");
        if (StringUtils.isEmpty(authToken)) {
            map.put("code", 401);
            map.put("msg", "登录超时!");
            map.put("state", false);
            setResponse(response,map);
            return false;
        }
        try {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            String methodName = handlerMethod.getMethod().getName();
            logger.info("拦截到方法:{}", methodName);
        }catch (ClassCastException e){
            map.put("code", 404);
            map.put("msg", "Not Found");
            map.put("state", false);
            setResponse(response,map);
            return false;
        }

        //此处进行token合法性校验
        return true;

    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }


    /**
     * 设置响应信息
     * @param response
     * @param map
     */
    private void setResponse(HttpServletResponse response,Map<String,Object> map){
        try {
            response.reset();
            response.setContentType("application/json");
            response.setHeader("Cache-Control", "no-store");
            response.setCharacterEncoding("UTF-8");
            PrintWriter pw = response.getWriter();
            String failMsg = JSONObject.toJSONString(map);
            pw.write(failMsg);
            pw.flush();
        }catch (IOException e){
            logger.error("登陆失效错误",e);
        }

    }

}

之后将我们自定义的拦截器LoginHandlerInterceptor,注册到WebMvcConfigurer

新建WebMvcConfig实现类,重写addInterceptors()方法,并使用@Configuration注解,交给spring容器管理

package com.kjyfx.config;

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;

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2020/12/14 22:39
 * @Created by moyun
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginHandlerInterceptor loginHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginHandlerInterceptor);//默认所有路径都被拦截
    }
}

情形一:

不带AuthToken,进行访问controller中有的接口,如:http://localhost:9000/base/sysUser/getUser

此接口是我之前写的,此时在浏览器访问接口,会得到如下信息

{
    "msg": "登录超时!",
    "code": 401,
    "state": false
}

因为我们在浏览器中访问接口时,并未带上AuthToken,或者说AuthToken为null或"";则被拦截

后端控制台打印的日志信息为:

2020-12-14 23:22:48.292  INFO 12300 --- [nio-9000-exec-9] c.kjyfx.config.LoginHandlerInterceptor   : 拦截到路径:http://localhost:9000/base/sysUser/getUser
2020-12-14 23:22:48.293  INFO 12300 --- [nio-9000-exec-9] c.kjyfx.config.LoginHandlerInterceptor   : 拦截到方法:getUser

情形二:

带上AuthToken,访问controller中有的接口,如:http://localhost:9000/base/sysUser/getUser

这里我们借助PostMan工具,给header带上一个AuthToken字符串

SpringBoot之配置拦截器

此时,成功访问到数据,说明带上AuthToken之后,接口并未被拦截器拦截

情形三:

访问controller中未有的接口,如:http://localhost:9000/base/sysUser/getUserAll

此时在浏览器访问接口,首先会得到如下提示信息,被拦截

{
    "msg": "登录超时!",
    "code": 401,
    "state": false
}

后端未定义/sysUser/getUserAll接口,如果此时我们在header中带上AuthToken再访问,则会提示404,

因为此接口未定义,在处理器转化时便会报转换异常,此时我们人为设置响应了404;

后端控制台打印的日志信息为:

2020-12-14 23:17:53.824  INFO 12300 --- [nio-9000-exec-4] c.kjyfx.config.LoginHandlerInterceptor   : 拦截到路径:http://localhost:9000/base/sysUser/getUserAll

情形四:

对于有的接口,为保证数据的安全性,必须要进行认证及验权之后,方能让用户进行访问,但有时,也需要提供匿名访问的接口给用户访问,最常见的就是用户登录接口及静态资源访问,接下来,我们来配置一下用户可以进行匿名访问的资源或接口

我在resources目录下创建了一个dist目录,dist目录下创建了index.html文件与js/common.js目录

SpringBoot之配置拦截器

application.yml中的静态资源配置如下:

spring:
  mvc:
    static-path-pattern: /**              #静态资源访问前缀,/**代表不需要加其他的前缀
  resources:                              #静态资源访问路径
    static-locations: file:E:/WeiXinDoc/kjyfx-base-server/src/main/resources/dist   #修改默认的静态资源路径

此时,我们在浏览器中输入:http://localhost:9000/base/index.html,

访问则会提示登陆超时

{
    "msg": "登录超时!",
    "code": 401,
    "state": false
}

对此,我们需要对拦截器进行改造,改造的方式有很多种,大同小异,代码如下:

package com.kjyfx.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2020/12/14 22:39
 * @Created by moyun
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginHandlerInterceptor loginHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        InterceptorRegistration interceptor = registry.addInterceptor(loginHandlerInterceptor);

        //放行路径
        interceptor.excludePathPatterns("/login/doLogin");
        interceptor.excludePathPatterns("/index.html");//对某个资源进行放行
        interceptor.excludePathPatterns("/js/**");//对某个路径下的所有资源放行

        //拦截所有路径
        interceptor.addPathPatterns("/**");
    }
}

此时,再次访问就能得到资源。也可以通过修改spring.mvc.static-path-pattern静态资源前缀的方式,对所有静态资源进行统一放行

SpringBoot之配置拦截器

配置多个拦截器

单个拦截器的业务使用场景很多,如用户登陆控制,那如果还需要进行权限控制呢?继续在拦截方法中加判断逻辑?

秉承着“高内聚、低耦合”的编程思想,一个拦截器只专注于一件事,此时我们便需要在SpringBoot项目中配置多个拦截器

常用场景

1、日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等

2、权限校验:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面;

3、性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);

4、通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现。

自定义权限注解

此处我们需要拦截Controller层的接口方法,因此自定义注解时使用@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)使其运行时有效

package com.kjyfx.annotation;

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


/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2021/1/7 22:04
 * @Created by moyun
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminRole {

    String[] roles() default {};

}

使用权限注解

在cntroller方法上使用自定义注解

    @RequestMapping("/getUser")
    @AdminRole(roles = {"Admin","testAdmin"})
    public BaseResponse getUser(){
        SysUser sysUser = new SysUser()
                .setId(1000L)
                .setAccount("moyun")
                .setUserName("墨云")
                .setCreateTime(LocalDateTime.now());
        return BaseResponse.renderSuccess("成功!",sysUser);
    }

自定义权限拦截器

此处为方便演示,我直接从header中获取用户角色(header名叫AuthRole

真实应用场景中使用时,可以根据实际情况,从Token、Session、Cookie中进行动态获取即可

package com.kjyfx.config;

import com.kjyfx.annotation.AdminRole;
import net.minidev.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2021/1/7 22:04
 * @Created by moyun
 */
@Configuration
public class AdminHandlerInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(AdminHandlerInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authRole = request.getHeader("AuthRole");
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        //获取接口方法上的权限注解
        AdminRole adminRoles = handlerMethod.getMethodAnnotation(AdminRole.class);
        if(adminRoles != null){
            //用户拥有访问此接口的权限
            if(Arrays.asList(adminRoles.roles()).contains(authRole)){
                return true;
            }else{
                HashMap<String, Object> map = new HashMap<>();
                map.put("code", 403);
                map.put("msg", "您没有权限访问此接口!");
                map.put("state", false);
                setResponse(response,map);
                return false;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }


    /**
     * 设置响应信息
     * @param response
     * @param map
     */
    private void setResponse(HttpServletResponse response, Map<String,Object> map){
        try {
            response.reset();
            response.setContentType("application/json");
            response.setHeader("Cache-Control", "no-store");
            response.setCharacterEncoding("UTF-8");
            PrintWriter pw = response.getWriter();
            String failMsg = JSONObject.toJSONString(map);
            pw.write(failMsg);
            pw.flush();
        }catch (IOException e){
            logger.error("权限失效错误",e);
        }

    }
}

注册拦截器

整改WebMvcConfig配置类,将权限拦截器注册进去

注意:先注册的拦截器会优先被执行,后注册的拦截器会后被执行,因此需要根据实际情况进行拦截器注册。

以下配置时,在同一个请求中会先执行登陆拦截器,而后执行权限拦截器

package com.kjyfx.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Description TODO
 * 微信公众号:云说Java、Java栈记
 * @Date 2020/12/14 22:39
 * @Created by moyun
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginHandlerInterceptor loginHandlerInterceptor;

    @Autowired
    private AdminHandlerInterceptor adminHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置登陆拦截器
        this.excludePathPatterns(registry.addInterceptor(loginHandlerInterceptor));

        //配置权限拦截器
        this.excludePathPatterns(registry.addInterceptor(adminHandlerInterceptor));

    }


    public void excludePathPatterns(InterceptorRegistration registration){
        //放行路径
        registration.excludePathPatterns("/login/doLogin");
        registration.excludePathPatterns("/index.html");
        registration.excludePathPatterns("/js/**");
        //拦截所有路径
        registration.addPathPatterns("/**");
    }
}

此时用postman请求接口:http://localhost:9000/base/sysUser/getUser

会出现以下信息提示,没有权限(AuthToken这个header是前面我们用于登陆拦截)

SpringBoot之配置拦截器

接下来,我们在header中添加一个名叫AuthRole的token

其值为@AdminRole(roles = {"Admin","testAdmin"})中的任意一个,即可进行方法的正常访问

SpringBoot之配置拦截器

  • 云说Java
  • 关注公众号获取更多资源
  • weinxin
  • Java栈记
  • 关注公众号获取更多资源
  • weinxin
墨云

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: