安全检验---过滤器与拦截器

发布时间 2023-12-24 17:30:31作者: 奕帆卷卷

过滤器

简介

什么是过滤器(Filter)

  • Filter表示过滤器,是JavaWeb三大组件(Servlet,Filter,Listener)之一
  • 过滤器可以把对资源的请求拦截下来,从而实现设置好的特殊功能
  • 使用了过滤器之后,想要访问Web服务器上的资源,需要先经过过滤器,过滤器处理完毕之后,才可以访问对应的资源。
  • 过滤器一般完成一些通用操作,列如登录校验,统一编码处理,敏感字符处理等

Filter入门程序

  1. 定义:定义一个类,实现Filter接口,并重写其所有方法
  2. 配置:Filter类上加上@WebFilter注解,配置拦截资源的路径,引导类上加@ServletComponentScan开启Servlet组件支持

1). 定义过滤器

public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init ...");
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截到了请求...");
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println("destroy ... ");
    }
}
  • init方法:过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。

  • doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。

  • destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。

2). 配置Filter

在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:@WebFilter,并指定属性urlPatterns,通过这个属性指定过滤器要拦截哪些请求

@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init ...");
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截到了请求...");
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println("destroy ... ");
    }
}

当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。

@ServletComponentScan //开启对Servlet组件的支持
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
}

注意:在过滤器中,如果不执行放行操作,将无法访问后面的资源

过滤器执行流程

在过滤器中,拦截到请求之后,如果希望继续访问后面的Web资源,就要执行放行操作,放行就是调用FilerChain对象当中的doFilter()方法,调用这个方法之前编写的代码就属于放行后的逻辑

@WebFilter(urlPatterns = "/*") 
public class DemoFilter implements Filter {
@Override //初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws ServletException {
    System.out.println("init 初始化方法执行了");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    System.out.println("DemoFilter   放行前逻辑.....");

    //放行请求
    filterChain.doFilter(servletRequest,servletResponse);

    System.out.println("DemoFilter   放行后逻辑.....");
    
}

@Override //销毁方法, 只调用一次
public void destroy() {
    System.out.println("destroy 销毁方法执行了");
}
}

拦截路径

我们来了解过滤器的拦截路径,Filter可以根据需求,配置不同的拦截资源路径:

拦截路径 urlPatterns值 含义
拦截具体路径 /login 只有访问 /login 路径时,才会被拦截
目录拦截 /emps/* 访问/emps下的所有资源,都会被拦截
拦截所有 /* 访问所有资源,都会被拦截

拦截器链

过滤器链指的就是在一个Web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链

--> (( -.-

比如我们在web服务器中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链

这个链上的过滤器会在执行的时候会一个一个执行(1后面2),直到执行到最后一个过滤器放行之后,才会访问对应的web资源。而后端响应数据的时候过滤器放行的顺序是反的(2然后1)

登录校验-Filter

我们接下来来看Filter过滤器在登录中如何校验

我们先来回顾一下登录校验的基本流程

  • 要进入到后台管理系统,我们必须先完成登录操作,此时就需要访问登录接口login

  • 登录成功之后,我们会在服务端生成一个JWT令牌,并且把JWT令牌返回给前端,前端会将JWT令牌存储下来

  • 在后续的每一次请求当中,都会将JWT令牌携带到服务器,请求到达服务器之后,要想去访问对应的业务功能,此时我们必须要先检验令牌的有效性

  • 对应校验令牌的这一块操作,我们使用登录校验的过滤器,在过滤器当中来校验令牌的有效性,如果令牌是无效的,就响应了一个错误的信息,也不会再去放行访问对应的资源,如果令牌存在,并且它是有效的,此时就会放行去访问对应的web资源,执行相应的业务操作

  • 思考

1.所有的请求,拦截到了之后,都需要校验令牌吗?

答:登录请求例外

2.拦截到请求之后,什么情况下才可以放行,执行业务操作?

有令牌,且令牌校验通过(合法);否则都返回未登录错误结果

基于业务,我们可以分析出具体的操作步骤:

  1. 获取请求url
  2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
  3. 获取请求头中的令牌
  4. 判断令牌是否存在,如果不存在,响应401
  5. 解析token,如果解析失败,响应401
  6. 放行

代码实现

下列是接口手册

  • 基本信息

    请求路径:/login
    
    请求方式:POST
    
    接口描述:该接口用于员工登录Tlias智能学习辅助系统,登录完毕后,系统下发JWT令牌。 
    
  • 请求参数

    参数格式:application/json

    参数说明:

    名称 类型 是否必须 备注
    username string 必须 用户名
    password string 必须 密码

    请求数据样例:

    {
    	"username": "jinyong",
        "password": "123456"
    }
    
  • 响应数据

    参数格式:application/json

    参数说明:

    名称 类型 是否必须 备注
    code number 必须 响应码, 1 成功 ; 0 失败
    msg string 非必须 提示信息
    data object 必须 返回的数据
    |- id number 必须 员工ID
    |- username string 必须 用户名
    |- name string 必须 姓名
    |- token string 必须 令牌

    响应数据样例:

    {
        "code": 1,
        "msg": "success",
        "data": {
            "id": 2,
            "username": "songjiang",
            "name": "宋江",
            "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MiwidXNlcm5hbWUiOiJzb25namlhbmciLCJleHAiOjE2OTg3MDE3NjJ9.w06EkRXTep6SrvMns3w5RKe79nxauDe7fdMhBLK-MKY"
        }
    }
    

登录校验过滤器:TokenFilter

/**

 * 令牌校验过滤器
   */
   @Slf4j
   @WebFilter(urlPatterns = "/*")
   public class TokenFilter implements Filter {

   @Override
   public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
       HttpServletRequest request = (HttpServletRequest) req;
       HttpServletResponse response = (HttpServletResponse) resp;
       //1. 获取请求url。
       String url = request.getRequestURL().toString();
//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){ //登录请求
    log.info("登录请求 , 直接放行");
    chain.doFilter(request, response);
    return;
}

//3. 获取请求头中的令牌(token)。
String jwt = request.getHeader("token");

//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){ //jwt为空
    log.info("获取到jwt令牌为空, 返回错误结果");
    response.setStatus(HttpStatus.SC_UNAUTHORIZED);
    return;
}

//5. 解析token,如果解析失败,返回错误结果(未登录)。
try {
    JwtUtils.parseJWT(jwt);
} catch (Exception e) {
    e.printStackTrace();
    log.info("解析令牌失败, 返回错误结果");
    response.setStatus(HttpStatus.SC_UNAUTHORIZED);
    return;
}

//6. 放行。
log.info("令牌合法, 放行");
chain.doFilter(request , response);
 * }

}

拦截器

介绍

什么是拦截器?

  • 是一种动态拦截方法调用的机制,类似于过滤器
  • 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行

拦截器的作用:

拦截请求,再指定方法调用前后,根据业务需要执行预先设定的代码

在拦截器中,我们通常也是做一些通用性的操作,比如我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器中,在校验过程中,如果发现用户登录了,就可以直接放行,去访问Spring当中的资源

步骤

  • 定义拦截器
  • 注册配置拦截器

自定义拦截器

实现HandlerInterceptor接口,并重写其所有方法

//自定义拦截器
@Component
public class DemoInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行。 返回true:放行    返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        
        return true; //true表示放行
    }

    //目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle ... ");
    }

    //视图渲染完毕后执行,最后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion .... ");
    }
}

preHandle方法:目标资源方法执行前执行,

返回true:放行 返回false: 不放行

postHandle方法:目标资源方法执行后执行

afterCompletion方法:视图渲染完毕后执行,最后执行

注册配置拦截器

创建一个配置类 WebConfig, 实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法

@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //自定义的拦截器对象
    @Autowired
    private DemoInterceptor demoInterceptor;

    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
    }
}

Interceptor详解

拦截路径

在注册配置拦截器的时候,我们要指定拦截器的拦截路径,通过addPathPatterns("要拦截路径")方法,就可以指定要拦截的哪些资源

在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用excludePathPatterns(不拦截路径)方法,指定哪些资源不需要拦截

@Configuration
public class WebConfig implements WebMvcConfigurer {

//拦截器对象
@Autowired
private DemoInterceptor demoInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    //注册自定义拦截器对象
    registry.addInterceptor(demoInterceptor)
            .addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
            .excludePathPatterns("/login");//设置不拦截的请求路径
}}

在拦截器中除了可以设置/**拦截所有资源外,还有一些常见拦截路径设置:

拦截路径 含义 举例
/* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1
/** 任意级路径 能匹配/depts,/depts/1,/depts/1/2
/depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1

执行流程

我们来介绍一些拦截器的执行流程,通过执行流程,我们就能够清晰的知道过滤器和拦截器的执行时机

  • 当我们打开浏览器访问web应用时,此时我们所定义的过滤器会拦截这次请求,然后会先执行放行前的逻辑,然后再执行放行操作。访问我们所定义的controller当中的接口方法
  • Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller
  • 我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住,执行preHandle()方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;日光返回false,则不会放行
  • 在controller中的方法执行之后,再回国了执行postHandle()这个方法以及agterCompletion()方法,然后再返回给DipatcherServket,最终再执行过滤器当中放行后的这一部分逻辑的逻辑,执行完毕之后,最终给浏览器响应数据

拦截器和过滤器的区别

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

登录校验-Interceptor

通过拦截器来完成实际业务当中的登录校验功能

代码实现

1). Token校验拦截器

@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 获取请求url。
        String url = request.getRequestURL().toString();

        //2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
        if(url.contains("login")){ //登录请求
            log.info("登录请求 , 直接放行");
            return true;
        }

        //3. 获取请求头中的令牌(token)。
        String jwt = request.getHeader("token");

        //4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){ //jwt为空
            log.info("获取到jwt令牌为空, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }

        //5. 解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            log.info("解析令牌失败, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }

        //6. 放行。
        log.info("令牌合法, 放行");
        return true;
    }

}

2). 配置拦截器

@Configuration  
public class WebConfig implements WebMvcConfigurer {
    //拦截器对象
    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}