本文通过一个简易安全认证示例的开发实践,理解过滤器和拦截器的工作原理。

很多文章都将过滤器(Filter)、拦截器(Interceptor)和监听器(Listener)这三者和Spring关联起来讲解,并认为过滤器(Filter)、拦截器(Interceptor)和监听器(Listener)是Spring提供的应用广泛的组件功能。

但是严格来说,过滤器和监听器属于Servlet范畴的API,和Spring没什么关系。

因为过滤器继承自javax.servlet.Filter接口,监听器继承自javax.servlet.ServletContextListener接口,只有拦截器继承的是org.springframework.web.servlet.HandlerInterceptor接口。

上面的流程图参考自网上资料,一图胜千言。看完本文以后,将对过滤器和拦截器的调用过程会有更深刻理解。

一、安全认证设计思路

有时候内外网调用API,对安全性的要求不一样,很多情况下外网调用API的种种限制在内网根本没有必要,但是网关部署的时候,可能因为成本和复杂度等问题,内外网要调用的API会部署在一起。

实现REST接口的安全性,可以通过成熟框架如Spring Security或者shiro搞定。

但是因为安全框架往往实现复杂(我数了下Spring Security,洋洋洒洒大概有11个核心模块,shiro的源码代码量也比较惊人)同时可能要引入复杂配置(能不能让人痛快一点),不利于中小团队的灵活快速开发、部署及问题排查。

很多团队自己造轮子实现安全认证,本文这个简易认证示例参考自我所在的前厂开发团队,可以认为是个基于token的安全认证服务。

大致设计思路如下:

1、自定义http请求头,每次调用API都在请求头里传人一个token值

2、token放在缓存(如redis)中,根据业务和API的不同设置不同策略的过期时间

3、token可以设置白名单和黑名单,可以限制API调用频率,便于开发和测试,便于紧急处理异状,甚至临时关闭API

4、外网调用必须传人token,token可以和用户有关系,比如每次打开页面或者登录生成token写入请求头,页面验证cookie和token有效性等

在Spring Security框架里有两个概念,即认证授权,认证指可以访问系统的用户,而授权则是用户可以访问的资源。

实现上述简易安全认证需求,你可能需要独立出一个token服务,保证生成token全局唯一,可能包含的模块有自定义流水生成器、CRM、加解密、日志、API统计、缓存等,但是和用户(CRM)其实是弱绑定关系。某些和用户有关系的公共服务,比如我们经常用到的发送短信SMS和邮件服务,也可以通过token机制解决安全调用问题。

综上,本文的简易安全认证其实和Spring Security框架提供的认证和授权有点不一样,当然,这种“安全”处理方式对专业人士没什么新意,但是可以对外挡掉很大一部分小白用户。

二、自定义过滤器

和Spring MVC类似,Spring Boot提供了很多servlet过滤器(Filter)可使用,并且它自动添加了一些常用过滤器,比如CharacterEncodingFilter(用于处理编码问题)、HiddenHttpMethodFilter(隐藏HTTP函数)、HttpPutFormContentFilter(form表单处理)、RequestContextFilter(请求上下文)等。通常我们还会自定义Filter实现一些通用功能,比如记录日志、判断是否登录、权限验证等。

1、自定义请求头

很简单,在request header添加自定义请求头authtoken:

  1. @RequestMapping(value = "/getinfobyid", method = RequestMethod.POST)
  2. @ApiOperation("根据商品Id查询商品信息")
  3. @ApiImplicitParams({
  4. @ApiImplicitParam(paramType = "header", name = "authtoken", required = true, value = "authtoken", dataType =
  5. "String"),
  6. })
  7. public GetGoodsByGoodsIdResponse getGoodsByGoodsId(@RequestHeader String authtoken, @RequestBody GetGoodsByGoodsIdRequest request) {
  8.  
  9. return _goodsApiService.getGoodsByGoodsId(request);
  10.  
  11. }

getGoodsByGoodsId

加了@RequestHeader修饰的authtoken字段就可以在swagger这样的框架下显示出来。

调用后,可以根据http工具看到请求头,本文示例是authtoken(和某些框架的token区分开):

备注:很多httpclient工具都支持动态传人请求头,比如RestTemplate

2、实现Filter

Filter接口共有三个方法,即init,doFilter和destory,看到名称就大概知道它们主要用途了,通常我们只要在doFilter这个方法内,对Http请求进行处理:

  1. package com.power.demo.controller.filter;
  2.  
  3. import com.power.demo.common.AppConst;
  4. import com.power.demo.common.BizResult;
  5. import com.power.demo.service.contract.AuthTokenService;
  6. import com.power.demo.util.PowerLogger;
  7. import com.power.demo.util.SerializeUtil;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Component;
  10.  
  11. import javax.servlet.*;
  12. import javax.servlet.http.HttpServletRequest;
  13. import java.io.IOException;
  14.  
  15. @Component
  16. public class AuthTokenFilter implements Filter {
  17.  
  18. @Autowired
  19. private AuthTokenService authTokenService;
  20.  
  21. @Override
  22. public void init(FilterConfig var1) throws ServletException {
  23.  
  24. }
  25.  
  26. @Override
  27. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  28. throws IOException, ServletException {
  29. HttpServletRequest req = (HttpServletRequest) request;
  30.  
  31. String token = req.getHeader(AppConst.AUTH_TOKEN);
  32.  
  33. BizResult<String> bizResult = authTokenService.powerCheck(token);
  34.  
  35. System.out.println(SerializeUtil.Serialize(bizResult));
  36.  
  37. if (bizResult.getIsOK() == true) {
  38. PowerLogger.info("auth token filter passed");
  39.  
  40. chain.doFilter(request, response);
  41. } else {
  42. throw new ServletException(bizResult.getMessage());
  43. }
  44.  
  45. }
  46.  
  47. @Override
  48. public void destroy() {
  49.  
  50. }
  51. }

AuthTokenFilter

注意,Filter这样的东西,我认为从实际分层角度,多数处理的还是表现层偏多,不建议在Filter中直接使用数据访问层Dao,虽然这样的代码一两年前我在很多老古董项目中看到过很多次,而且<<Spring实战>>的书里也有这样写的先例。

3、认证服务

这里就是主要业务逻辑了,示例代码只是简单写下思路,不要轻易就用于生产环境:

  1. package com.power.demo.service.impl;
  2.  
  3. import com.power.demo.cache.PowerCacheBuilder;
  4. import com.power.demo.common.BizResult;
  5. import com.power.demo.service.contract.AuthTokenService;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.stereotype.Component;
  8. import org.springframework.util.StringUtils;
  9.  
  10. @Component
  11. public class AuthTokenServiceImpl implements AuthTokenService {
  12.  
  13. @Autowired
  14. private PowerCacheBuilder cacheBuilder;
  15.  
  16. /*
  17. * 验证请求头token是否合法
  18. * */
  19. @Override
  20. public BizResult<String> powerCheck(String token) {
  21.  
  22. BizResult<String> bizResult = new BizResult<>(true, "验证通过");
  23.  
  24. System.out.println("token的值为:" + token);
  25.  
  26. if (StringUtils.isEmpty(token) == true) {
  27. bizResult.setFail("authtoken为空");
  28. return bizResult;
  29. }
  30.  
  31. //处理黑名单
  32. bizResult = checkForbidList(token);
  33. if (bizResult.getIsOK() == false) {
  34. return bizResult;
  35. }
  36.  
  37. //处理白名单
  38. bizResult = checkAllowList(token);
  39. if (bizResult.getIsOK() == false) {
  40. return bizResult;
  41. }
  42.  
  43. String key = String.format("Power.AuthTokenService.%s", token);
  44.  
  45. //cacheBuilder.set(key, token);
  46. //cacheBuilder.set(key, token.toUpperCase());
  47.  
  48. //从缓存中取
  49. String existToken = cacheBuilder.get(key);
  50. if (StringUtils.isEmpty(existToken) == true) {
  51. bizResult.setFail(String.format("不存在此authtoken:%s", token));
  52. return bizResult;
  53. }
  54.  
  55. //比较token是否相同
  56. Boolean isEqual = token.equals(existToken);
  57. if (isEqual == false) {
  58. bizResult.setFail(String.format("不合法的authtoken:%s", token));
  59. return bizResult;
  60. }
  61.  
  62. //do something
  63.  
  64. return bizResult;
  65. }
  66.  
  67. }

AuthTokenServiceImpl

用到的缓存服务可以参考这里,这个也是我在前厂的经验总结。

4、注册Filter

常见的有两种写法:
(1)、使用@WebFilter注解来标识Filter

  1. @Order(1)
  2. @WebFilter(urlPatterns = {"/api/v1/goods/*", "/api/v1/userinfo/*"})
  3. public class AuthTokenFilter implements Filter {

使用@WebFilter注解,还可以配合使用@Order注解,@Order注解表示执行过滤顺序,值越小,越先执行,这个Order大小在我们编程过程中就像处理HTTP请求的生命周期一样大有用处。当然,如果没有指定Order,则过滤器的调用顺序跟添加的过滤器顺序相反,过滤器的实现是责任链模式。

最后,在启动类上添加@ServletComponentScan 注解即可正常使用自定义过滤器了。
(2)、使用FilterRegistrationBean对Filter进行自定义注册

本文以第二种实现自定义Filter注册:

  1. package com.power.demo.controller.filter;
  2.  
  3. import com.google.common.collect.Lists;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.boot.web.servlet.FilterRegistrationBean;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.stereotype.Component;
  9.  
  10. import java.util.List;
  11.  
  12. @Configuration
  13. @Component
  14. public class RestFilterConfig {
  15.  
  16. @Autowired
  17. private AuthTokenFilter filter;
  18.  
  19. @Bean
  20. public FilterRegistrationBean filterRegistrationBean() {
  21. FilterRegistrationBean registrationBean = new FilterRegistrationBean();
  22. registrationBean.setFilter(filter);
  23.  
  24. //设置(模糊)匹配的url
  25. List<String> urlPatterns = Lists.newArrayList();
  26. urlPatterns.add("/api/v1/goods/*");
  27. urlPatterns.add("/api/v1/userinfo/*");
  28. registrationBean.setUrlPatterns(urlPatterns);
  29.  
  30. registrationBean.setOrder(1);
  31. registrationBean.setEnabled(true);
  32.  
  33. return registrationBean;
  34. }
  35. }

RestFilterConfig

请大家特别注意urlPatterns,属性urlPatterns指定要过滤的URL模式。对于Filter的作用区域,这个参数居功至伟。

注册好Filter,当Spring Boot启动时监测到有javax.servlet.Filter的bean时就会自动加入过滤器调用链ApplicationFilterChain。

调用一个API试试效果:

通常情况下,我们在Spring Boot下都会自定义一个全局统一的异常管理增强GlobalExceptionHandler(和上面这个显示会略有不同)。

根据我的实践,过滤器里抛出异常,不会被全局唯一的异常管理增强捕获到并进行处理,这个和拦截器Inteceptor以及下一篇文章介绍的自定义AOP拦截不同。

到这里,一个通过自定义Filter实现的简易安全认证服务就搞定了。

三、自定义拦截器

1、实现拦截器

继承接口HandlerInterceptor,实现拦截器,接口方法有下面三个:

preHandle是请求执行前执行

postHandle是请求结束执行

afterCompletion是视图渲染完成后执行

  1. package com.power.demo.controller.interceptor;
  2.  
  3. import com.power.demo.common.AppConst;
  4. import com.power.demo.common.BizResult;
  5. import com.power.demo.service.contract.AuthTokenService;
  6. import com.power.demo.util.PowerLogger;
  7. import com.power.demo.util.SerializeUtil;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Component;
  10. import org.springframework.web.servlet.HandlerInterceptor;
  11. import org.springframework.web.servlet.ModelAndView;
  12.  
  13. import javax.servlet.http.HttpServletRequest;
  14. import javax.servlet.http.HttpServletResponse;
  15.  
  16. /*
  17. * 认证token拦截器
  18. * */
  19. @Component
  20. public class AuthTokenInterceptor implements HandlerInterceptor {
  21.  
  22. @Autowired
  23. private AuthTokenService authTokenService;
  24.  
  25. /*
  26. * 请求执行前执行
  27. * */
  28. @Override
  29. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  30.  
  31. boolean handleResult = false;
  32.  
  33. String token = request.getHeader(AppConst.AUTH_TOKEN);
  34.  
  35. BizResult<String> bizResult = authTokenService.powerCheck(token);
  36.  
  37. System.out.println(SerializeUtil.Serialize(bizResult));
  38.  
  39. handleResult = bizResult.getIsOK();
  40.  
  41. PowerLogger.info("auth token interceptor拦截结果:" + handleResult);
  42.  
  43. if (bizResult.getIsOK() == true) {
  44. PowerLogger.info("auth token interceptor passed");
  45. } else {
  46. throw new Exception(bizResult.getMessage());
  47. }
  48.  
  49. return handleResult;
  50. }
  51.  
  52. /*
  53. * 请求结束执行
  54. * */
  55. @Override
  56. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  57.  
  58. }
  59.  
  60. /*
  61. * 视图渲染完成后执行
  62. * */
  63. @Override
  64. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  65.  
  66. }
  67. }

AuthTokenInterceptor

示例中,我们选择在请求执行前进行token安全认证。

认证服务就是过滤器里介绍的AuthTokenService,业务逻辑层实现复用。

2、注册拦截器

定义一个InterceptorConfig类,继承自WebMvcConfigurationSupport,WebMvcConfigurerAdapter已经过时。

将AuthTokenInterceptor作为bean注入,其他设置拦截器拦截的URL和过滤器非常相似:

  1. package com.power.demo.controller.interceptor;
  2.  
  3. import com.google.common.collect.Lists;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.stereotype.Component;
  7. import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
  8. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  9. import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
  10. import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
  11.  
  12. import java.util.List;
  13.  
  14. @Configuration
  15. @Component
  16. public class InterceptorConfig extends WebMvcConfigurationSupport { //WebMvcConfigurerAdapter已经过时
  17.  
  18. private static final String FAVICON_URL = "/favicon.ico";
  19.  
  20. /**
  21. * 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。
  22. *
  23. * @param registry
  24. */
  25. @Override
  26. public void addResourceHandlers(ResourceHandlerRegistry registry) {
  27. registry.addResourceHandler("/").addResourceLocations("/**");
  28. registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
  29. }
  30.  
  31. /**
  32. * 配置servlet处理
  33. */
  34. @Override
  35. public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
  36. configurer.enable();
  37. }
  38.  
  39. @Override
  40. public void addInterceptors(InterceptorRegistry registry) {
  41.  
  42. //设置(模糊)匹配的url
  43. List<String> urlPatterns = Lists.newArrayList();
  44. urlPatterns.add("/api/v1/goods/*");
  45. urlPatterns.add("/api/v1/userinfo/*");
  46.  
  47. registry.addInterceptor(authTokenInterceptor()).addPathPatterns(urlPatterns).excludePathPatterns(FAVICON_URL);
  48. super.addInterceptors(registry);
  49. }
  50.  
  51. //将拦截器作为bean写入配置中
  52. @Bean
  53. public AuthTokenInterceptor authTokenInterceptor() {
  54. return new AuthTokenInterceptor();
  55. }
  56. }

InterceptorConfig

启动应用后,调用接口就可以看到拦截器拦截的效果了。全局统一的异常管理GlobalExceptionHandler捕获异常后处理如下:

和过滤器显示的主要错误提示信息几乎一样,但是堆栈信息更加丰富。

四、过滤器和拦截器区别

主要区别如下:

1、拦截器主要是基于java的反射机制的,而过滤器是基于函数回调

2、拦截器不依赖于servlet容器,过滤器依赖于servlet容器

3、拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用

4、拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问

5、在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次

参考过的一些文章,有的说“拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑”,经过实际验证,这是不对的。

注意:过滤器的触发时机是容器后,servlet之前,所以过滤器的doFilter(ServletRequest request, ServletResponse response, FilterChain chain)的入参是ServletRequest,而不是HttpServletRequest,因为过滤器是在HttpServlet之前。下面这个图,可以让你对Filter和Interceptor的执行时机有更加直观的认识:

只有经过DispatcherServlet 的请求,才会走拦截器链,自定义的Servlet请求是不会被拦截的,比如我们自定义的Servlet地址http://localhost:9090/testServlet是不会被拦截器拦截的。但不管是属于哪个Servlet,只要符合过滤器的过滤规则,过滤器都会执行。

根据上述分析,理解原理,实际操作就简单了,哪怕是ASP.NET过滤器亦然。

问题:实现更加灵活的安全认证

在Java Web下通过自定义过滤器Filter或者拦截器Interceptor配置urlPatterns,可以实现对特定匹配的API进行安全认证,比如匹配所有API、匹配某个或某几个API等,但是有时候这种匹配模式对开发人员相对不够友好。

我们可以参考Spring Security那样,通过注解+SpEL实现强大功能。

又比如在ASP.NET中,我们经常用到Authorized特性,这个特性可以加在类上,也可以作用于方法上,可以更加动态灵活地控制安全认证。

我们没有选择Spring Security,那就自己实现类似Authorized的灵活的安全认证,主要实现技术就是我们所熟知的AOP。

通过AOP方式实现更灵活的拦截的基础知识本文就先不提了,更多的关于AOP的话题将在下篇文章分享。

参考:

<<Spring实战>>

https://blog.csdn.net/qyp1314/article/details/42023725

https://blog.csdn.net/sun_t89/article/details/51916834

https://www.cnblogs.com/moonlightL/p/8126910.html

Spring Boot使用过滤器和拦截器分别实现REST接口简易安全认证的更多相关文章

  1. Spring boot 配置自己的拦截器

    框架使用的是spring boot 2.0 首先,自定义拦截器实现HandlerInterceptor接口,preHandler是在执行controller方法前执行的  此外还有两个方法,具体作用最 ...

  2. Springboot 系列(六)Spring Boot web 开发之拦截器和三大组件

    1. 拦截器 Springboot 中的 Interceptor 拦截器也就是 mvc 中的拦截器,只是省去了 xml 配置部分.并没有本质的不同,都是通过实现 HandlerInterceptor ...

  3. spring boot 学习(十二)拦截器实现IP黑名单

    拦截器实现IP黑名单 前言 最近一直在搞 Hexo+GithubPage 搭建个人博客,所以没怎么进行 SpringBoot 的学习.所以今天就将上次的”?秒防刷新”进行了一番修改.上次是采用注解加拦 ...

  4. Spring Boot 优雅的配置拦截器方式

    https://my.oschina.net/bianxin/blog/2876640 https://cs.xieyonghui.com/java/55.html 其实spring boot拦截器的 ...

  5. Spring Boot实践——三种拦截器的创建

    引用:https://blog.csdn.net/hongxingxiaonan/article/details/48090075 Spring中的拦截器 在web开发中,拦截器是经常用到的功能.它可 ...

  6. 【spring boot】在自定义拦截器中从request中获取json字符串

    又这样的需求,需要在自定义的拦截器中获取request中的数据,想获取到的是JSON字符串 那需要在拦截器中写这样一个方法 public static String getOpenApiRequest ...

  7. spring boot 过滤器、拦截器的区别与使用

    原文:https://blog.csdn.net/heweimingming/article/details/79993591 拦截器与过滤器的区别: 1.过滤器和拦截器触发时机不一样,过滤器是在请求 ...

  8. spring中的监视器,过滤器,拦截器

    1.监视器 (1)首先监视器是观察者模式的实现,在我之前的博客中有关于监视器模式的解释.监视器相当于观察者 (2)我们在springMvc中最常见的监视器 ContextLoaderlistener ...

  9. spring过滤器和拦截器的区别和联系

    一 简介 (1)过滤器: 依赖于servlet容器,是JavaEE标准,是在请求进入容器之后,还未进入Servlet之前进行预处理,并且在请求结束返回给前端这之间进行后期处理.在实现上基于函数回调,可 ...

随机推荐

  1. 编写高效的 CSS 选择器

    高效的CSS已经不是一个新的话题了,也不是我一个非得重拾的话题,但它却是我在Sky公司工作之时,所感兴趣的,关注已久的话题. 有很多人都忘记了,或在简单的说没有意识到,CSS在我们手中,既能很高效,也 ...

  2. sql的日期和时间函数–date_format

    Mysql的日期和时间函数–date_format   DATE_FORMAT(date,format)依照 format 字符串格式化 date 值.下面的修饰符可被用于 format 字符串中:修 ...

  3. poj 1141 Brackets Sequence 区间dp,分块记录

    Brackets Sequence Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 35049   Accepted: 101 ...

  4. 51单片机学习笔记(郭天祥版)(6)——键盘的作业题、AD、DA、DS18B20(这里之后看清翔的补一下好了)

    A:analog,D:digital AD,就是模拟量转换为数字量,DA就是数字量转换为模拟量 为什么要转换? 单片机是数字芯片,内部只有0和1,没法表示模拟量 比如我们如果需要2.5V怎么办?其实是 ...

  5. 4-21 嵌套选择器 、块级元素和内联元素、光标、布局-overflow

    1.嵌套选择器 p{ }: 为所有 p 元素指定一个样式.(默认,,也就是说可以被改变样式) .marked{ }: 为所有 class="marked" 的元素指定一个样式. . ...

  6. 【CSS】环形进度条

    效果图 原理剖析 1.先完成这样一个半圆(这个很简单吧) 2.overflow: hidden; 3.在中间定位一个白色的圆形做遮挡 4.完成另一半 5.使用animate配合时间完成衔接 源码 &l ...

  7. JavaSE基础知识(5)—面向对象(抽象类和接口)

    一.抽象类 1.理解 用abstract关键字定义的类,称为抽象类用abstract关键字定义的方法,称为抽象方法意义:当设计父类时,发现该父类根本不需要创建对象,并且里面有不好描述的方法.这个时候往 ...

  8. vue中v-model 与 v-bind:value

    之前一直认为,v-model相当于下方代码的语法糖,如下: <h1>{{inputValue}}</h1> <input type="text" :v ...

  9. python3 第三十二章 - 标准库概览

    1. 操作系统接口 os 模块提供很多函数与操作系统进行交互︰ >>> import os >>> os.getcwd() # 返回当前的工作目录 'C:\\Pyt ...

  10. Percona XtraBackup使用说明(转)

    Percona XtraBackup使用说明 转载出自: https://blog.csdn.net/wfs1994/article/details/80396604 XtraBackup介绍 Per ...