Spring Boot使用过滤器和拦截器分别实现REST接口简易安全认证

http://www.iteye.com/topic/1148174


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

 

很多文章都将过滤器(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:

Java代码   收藏代码
  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. }  

 

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

 

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

 



 

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

 

2、实现Filter

 

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

 

Java代码   收藏代码
  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.   
  48.     @Override  
  49.     public void destroy() {  
  50.   
  51.     }  
  52. }  

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

 

3、认证服务

 

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

 

Java代码   收藏代码
  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. }  

 

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

 

4、注册Filter

 

常见的有两种写法:

(1)、使用@WebFilter注解来标识Filter

Java代码   收藏代码
  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注册:

Java代码   收藏代码
  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. }  

 

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

 

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

 

调用一个API试试效果:



 

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

 

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

 

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

 

三、自定义拦截器

 

1、实现拦截器

 

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

 

preHandle是请求执行前执行

 

postHandle是请求结束执行

 

afterCompletion是视图渲染完成后执行

Java代码   收藏代码
  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. }  

 

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

 

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

 

2、注册拦截器

 

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

 

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

Java代码   收藏代码
  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.   
  52.     //将拦截器作为bean写入配置中  
  53.     @Bean  
  54.     public AuthTokenInterceptor authTokenInterceptor() {  
  55.         return new AuthTokenInterceptor();  
  56.     }  
  57. }  

 

启动应用后,调用接口就可以看到拦截器拦截的效果了。全局统一的异常管理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。

 

推荐内容:https://www.roncoo.com/course/list.html?courseName=spring

 

文章来源:https://my.oschina.net/u/3854434/blog/1824961


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值