统一拦截
上文我们提到(登录认证(4):令牌技术),现在大部分项目都使用JWT令牌
来进行会话跟踪,来完成登录功能。有了JWT令牌
可以标识用户的登录状态,但是完整的登录逻辑如图所示:
我们还需要一个统一拦截器
,用于拦截用户的请求,在拦截器中,我们需要验证用户的JWT令牌
,来判断用户是否登录,从而进行放行或拦截。统一拦截一般有两种解决方案:Filter过滤器和Interceptor拦截器,本文先讲解Filter过滤器。
Filter过滤器
工作原理
Filter
的本意就是过滤器,在JavaWeb
中是用于过滤请求的,是JavaWeb的三大核心组件之一 (Servlet
、Filter
、Listener
)。Filter过滤器
的工作原理就是将客户端发起的对服务端资源的请求拦截下来,进行处理,从而实现一些特殊功能。
当程序使用了Filter过滤器
之后,请求想要访问服务端的资源,就必须先经过过滤器
,待过滤器处理完毕放行之后,才可以访问对应的资源。Filter过滤器
一般用于完成一些通用的操作,比如:登录校验、敏感字符处理等。
快速入门
Filter过滤器
的快速入门十分简单,主要步骤分为两步:首先需要定义过滤器,定义一个类,实现Filter接口
,并重写其中所有方法;然后需要配置过滤器,在Filter类
上添加@WebFilter注解
,配置拦截资源的路径(只有请求对应的资源才会被过滤器拦截)。假如使用SpringBoot
构建项目,还需要在引导类
上添加@ServletComponentScan注解
开启Servlet组件支持。
定义过滤器
定义Filter过滤器
的代码:
public class DemoFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("Init Filter");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {log.info("Filter 成功拦截请求");}@Overridepublic void destroy() {log.info("Destroy Filter");}
}
定义Filter过滤器
的代码很简单,首先需要定义一个类实现Filter接口,然后重写Filter接口
中的三个方法。
init方法
顾名思义,init方法
是Filter过滤器
的初始化方法,在服务器启动时,会自动创建Filter过滤器
对象,在创建过滤器对象的时候,就会自动调用init初始化方法初始化过滤器,并且这个方法只会被调用一次。
doFilter方法
doFilter方法
是过滤器的核心部分,该方法在过滤器每一次拦截到了请求之后都会被调用,一般而言,这个方法会被调用多次,每一次拦截到请求,都会调用一次doFilter方法
,而我们就可以在doFilter方法
中实现一些功能,比如说对这次请求的JWT令牌
进行解析,以此判断用户的登录状态。
destroy方法
destroy方法
是销毁过滤器的方法,当服务器关闭时,会自动调用destroy方法销毁过滤器,destroy方法也只会被调用一次。
配置过滤器
在定义Filter过滤器
之后,过滤器并不会生效,此时还需要完成Filter
过滤器的配置,过滤器的配置相对简单,只需要在Filter类
上添加@WebFilter
注解,并在其urlPatterns属性
中指定需要拦截的请求路径。
@Slf4j
@WebFilter(urlPatterns = "/*") // 配置Filter过滤器的拦截请求路径,/*表示拦截所有
public class DemoFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("Init Filter");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {log.info("Filter 成功拦截请求");}@Overridepublic void destroy() {log.info("Destroy Filter");}
}
通过/*
通配符表示拦截所有请求(所有请求都会被过滤器所拦截,可以根据不同的需求更改路径),这样就完成了Filter过滤器
的配置,当定义和配置都完成之后,Filter过滤器
就可以真正开始工作了,此时启动服务器进行测试:
如图所示,在服务器启动的时候,自动创建Filter过滤器
,并调用了其init方法
,初始化过滤器。此时向服务端发起请求:
如图所示,发起的请求成功被Filter过滤器
拦截,但是客户端并没有得到服务器响应的数据
这是因为在Filter过滤器
中,必须要执行放行操作,才可以访问到服务器中的资源,可以通过FilterChain实体类
中的doFilter
方法放行:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {log.info("Filter 成功拦截请求");filterChain.doFilter(servletRequest, servletResponse);
}
放行之后再发起请求,就可以成功访问到服务器中的资源了:
关闭服务器,发现Filter过滤器
自动调用destroy方法销毁:
登录校验过滤器
通过上文的快速入门,我们已经了解了如何创建并配置一个Filter过滤器
,那么回归到主线,使用Filter过滤器
完成登录校验功能。
实现逻辑
让我们回顾一下登录校验的实现逻辑:用户登录时需要访问登录接口login
,当用户输入的用户名和密码验证成功之后,会生成一个JWT令牌
,并且将JWT令牌
返回给客户端,客户端
会将JWT
存储起来,然后在后续的每一次请求中,都携带这个JWT令牌
到服务端,这个时候,就需要Filter过滤器
进行工作了:在登录校验中,Filter过滤器必须要验证JWT令牌
的有效性,如果令牌能够成功解析,则证明为有效令牌;反之则为无效令牌。若令牌有效,Filter过滤器
则放行请求,让它访问服务端对应的资源;如果为无效令牌,则进行拦截
,并给客户端响应一个错误的信息(401)。其流程如图所示:
这样,登录校验的实现逻辑已经比较清晰了,但需要注意一点:对于登录(login)请求,过滤器是不需要进行拦截的,因为用户发起登录请求,就是为了获得JWT令牌
,其本身是没有JWT令牌
的,如果进行拦截,那么永远都只会得到请求失败的结果。
我们上述逻辑编写登录校验过滤器
代码:
/*** 令牌校验过滤器(登录过滤器)*/
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("Init Filter");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;// 获取该请求的URLString url = httpServletRequest.getRequestURL().toString();// 判断该请求是否包含login,如果包含,说明该请求是登录,则放行if (url.contains("/login")) {log.info("login");filterChain.doFilter(httpServletRequest, httpServletResponse);return;}// 获取请求头中的token(JWT令牌)String jwt = httpServletRequest.getHeader("token");// 判断令牌是否存在,若不存在,则返回错误结果(未登录)if (jwt == null || jwt.isEmpty()) {log.info("JWT令牌为空,登录失败");httpServletResponse.setStatus(401);return;}// 解析JWT令牌,如果解析失败,则返回错误结果(未登录)try {JWTUtils.parseJWT(jwt);} catch (Exception e) {e.printStackTrace();log.info("JWT解析失败,登录失败");httpServletResponse.setStatus(401);return;}// 令牌解析成功,放行log.info("令牌解析成功,放行");filterChain.doFilter(httpServletRequest, httpServletResponse);}@Overridepublic void destroy() {log.info("Destroy Filter");}
}
我们配置的过滤器是会拦截所有的请求,但是这和我们分析的逻辑不一样,我们需要直接放行登录请求,所以说需要进行判断该请求是否是登录,可以根据请求路径进行判断。然后需要获取到这次请求的token
,如果请求没有一个请求头
叫token
,那么可以判断该请求没有携带JWT令牌
,表示该用户没有登录,所以说直接响应错误结果,无需进行判断,最后才是解析token,假如在解析过程中报错,那么肯定意味着JWT令牌
解析错误,意味着用户可能伪造或更改了令牌,也表示用户未登录,所以说不能放行,需要返回错误代码。如果成功解析JWT令牌
,那么就可以调用FilterChain
中的doFilter
方法,放行该请求,让它访问服务端对应的资源。这就是 登录校验
的整个流程。此时让我们发起请求进行测试:
如图所示,发起了一个非登录的请求,由于此时还没有登录,所以说没有JWT令牌
,所以说自然被Filter过滤器
所拦截,请求失败。
此时发起登录请求,由于我们的逻辑是登录请求直接放行,所以说这次请求就被Filter过滤器
放行了,成功获得了JWT令牌
。
当我们登录后获得了JWT令牌
,之后的请求就可以在请求头
中携带JWT令牌
发起请求,这样当我们的JWT令牌
是正确的,被成功解析之后Filter过滤器
就会放行,就可以访问到对应的资源了。
Filter过滤器使用细节
Filter过滤器执行流程
Filter过滤器
执行流程如图所示:
当过滤器拦截到了请求之后,如果希望该请求访问到服务端资源,那么就需要执行doFilter
方法进行放行,在doFilter方法所编写的代码就是放行前逻辑。但是值得注意的是——请求访问完资源之后,还会回到过滤器当中,如果有需求,回到过滤器之后还可以执行放行后逻辑,放行后逻辑就是doFilter方法
之后的代码。
Filter过滤器拦截路径
可以根据不同的需求,配置不同的拦截路径,下面介绍几种常见的拦截路径:像/login这样的拦截路径属于具体拦截
路径,只有请求路径为/login
时,拦截器才会拦截;像/emps/*这样的拦截路径属于目录拦截
,访问/emps下的所有资源的请求,都会被拦截;像/*这样的拦截路径十分简单粗暴,会拦截所有请求
。可以根据不同的需求,灵活的配置拦截路径。
过滤器链
在一个web应用程序
中,可以根据需求,配置多个拦截器,这样的多个拦截器就形成了一个过滤器链:
如图所示,在web服务端
中,定义了两个Filter过滤器
,这两个过滤器就形成了一个过滤器链,过滤器链上的过滤器会一个一个的执行,先执行第一个;再执行第二个,必须要过滤器链上的最后一个过滤器放行后(也代表着所有过滤器都必须放行),请求才能访问到对应的资源。但是当请求返回时,会根据相反的顺序,通过过滤器链,先执行过滤器2;再执行过滤器1,最后给客户端响应数据。
并且,过滤器链的执行顺序,是根据过滤器名(字符串)的自然排序的,比如AbcFilter
会先于DemoFilter
执行。
总结
Filter过滤器
是Servlet
中的技术,是用于统一拦截的
,想要在web程序
中使用过滤器,就需要定义一个过滤器,并且配置过滤器,定义其拦截路径,我们可以在Filter过滤器
的doFilter
方法中编写一些逻辑决定该过滤器的放行逻辑,从而实现登录校验功能。Spring框架
中也提供了一个类似过滤器的Interceptor拦截器
,且听下回分解。