本文经过一个简易安全认证示例的开发实践,理解过滤器和拦截器的工做原理。html
不少文章都将过滤器(Filter)、拦截器(Interceptor)和监听器(Listener)这三者和Spring关联起来说解,并认为过滤器(Filter)、拦截器(Interceptor)和监听器(Listener)是Spring提供的应用普遍的组件功能。java
可是严格来讲,过滤器和监听器属于Servlet范畴的API,和Spring没什么关系。git
由于过滤器继承自javax.servlet.Filter接口,监听器继承自javax.servlet.ServletContextListener接口,只有拦截器继承的是org.springframework.web.servlet.HandlerInterceptor接口。github
上面的流程图参考自网上资料,一图胜千言。看完本文之后,将对过滤器和拦截器的调用过程会有更深入理解。web
有时候内外网调用API,对安全性的要求不同,不少状况下外网调用API的种种限制在内网根本没有必要,可是网关部署的时候,可能由于成本和复杂度等问题,内外网要调用的API会部署在一块儿。redis
实现REST接口的安全性,能够经过成熟框架如Spring Security或者shiro搞定。spring
可是由于安全框架每每实现复杂(我数了下Spring Security,洋洋洒洒大概有11个核心模块,shiro的源码代码量也比较惊人)同时可能要引入复杂配置(能不能让人痛快一点),不利于中小团队的灵活快速开发、部署及问题排查。apache
不少团队本身造轮子实现安全认证,本文这个简易认证示例参考自我所在的前厂开发团队,能够认为是个基于token的安全认证服务。编程
大体设计思路以下:api
一、自定义http请求头,每次调用API都在请求头里传人一个token值
二、token放在缓存(如redis)中,根据业务和API的不一样设置不一样策略的过时时间
三、token能够设置白名单和黑名单,能够限制API调用频率,便于开发和测试,便于紧急处理异状,甚至临时关闭API
四、外网调用必须传人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实现一些通用功能,好比记录日志、判断是否登陆、权限验证等。
很简单,在request header添加自定义请求头authtoken:
@RequestMapping(value = "/getinfobyid", method = RequestMethod.POST) @ApiOperation("根据商品Id查询商品信息") @ApiImplicitParams({ @ApiImplicitParam(paramType = "header", name = "authtoken", required = true, value = "authtoken", dataType = "String"), }) public GetGoodsByGoodsIdResponse getGoodsByGoodsId(@RequestHeader String authtoken, @RequestBody GetGoodsByGoodsIdRequest request) { return _goodsApiService.getGoodsByGoodsId(request); }
加了@RequestHeader修饰的authtoken字段就能够在swagger这样的框架下显示出来。
调用后,能够根据http工具看到请求头,本文示例是authtoken(和某些框架的token区分开):
备注:不少httpclient工具都支持动态传人请求头,好比RestTemplate。
Filter接口共有三个方法,即init,doFilter和destory,看到名称就大概知道它们主要用途了,一般咱们只要在doFilter这个方法内,对Http请求进行处理:
package com.power.demo.controller.filter; import com.power.demo.common.AppConst; import com.power.demo.common.BizResult; import com.power.demo.service.contract.AuthTokenService; import com.power.demo.util.PowerLogger; import com.power.demo.util.SerializeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component public class AuthTokenFilter implements Filter { @Autowired private AuthTokenService authTokenService; @Override public void init(FilterConfig var1) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String token = req.getHeader(AppConst.AUTH_TOKEN); BizResult<String> bizResult = authTokenService.powerCheck(token); System.out.println(SerializeUtil.Serialize(bizResult)); if (bizResult.getIsOK() == true) { PowerLogger.info("auth token filter passed"); chain.doFilter(request, response); } else { throw new ServletException(bizResult.getMessage()); } } @Override public void destroy() { } }
注意,Filter这样的东西,我认为从实际分层角度,多数处理的仍是表现层偏多,不建议在Filter中直接使用数据访问层Dao,虽然这样的代码一两年前我在不少老古董项目中看到过不少次,并且<<Spring实战>>的书里也有这样写的先例。
这里就是主要业务逻辑了,示例代码只是简单写下思路,不要轻易就用于生产环境:
package com.power.demo.service.impl; import com.power.demo.cache.PowerCacheBuilder; import com.power.demo.common.BizResult; import com.power.demo.service.contract.AuthTokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @Component public class AuthTokenServiceImpl implements AuthTokenService { @Autowired private PowerCacheBuilder cacheBuilder; /* * 验证请求头token是否合法 * */ @Override public BizResult<String> powerCheck(String token) { BizResult<String> bizResult = new BizResult<>(true, "验证经过"); System.out.println("token的值为:" + token); if (StringUtils.isEmpty(token) == true) { bizResult.setFail("authtoken为空"); return bizResult; } //处理黑名单 bizResult = checkForbidList(token); if (bizResult.getIsOK() == false) { return bizResult; } //处理白名单 bizResult = checkAllowList(token); if (bizResult.getIsOK() == false) { return bizResult; } String key = String.format("Power.AuthTokenService.%s", token); //cacheBuilder.set(key, token); //cacheBuilder.set(key, token.toUpperCase()); //从缓存中取 String existToken = cacheBuilder.get(key); if (StringUtils.isEmpty(existToken) == true) { bizResult.setFail(String.format("不存在此authtoken:%s", token)); return bizResult; } //比较token是否相同 Boolean isEqual = token.equals(existToken); if (isEqual == false) { bizResult.setFail(String.format("不合法的authtoken:%s", token)); return bizResult; } //do something return bizResult; } }
用到的缓存服务能够参考这里,这个也是我在前厂的经验总结。
常见的有两种写法:
(1)、使用@WebFilter注解来标识Filter
@Order(1) @WebFilter(urlPatterns = {"/api/v1/goods/*", "/api/v1/userinfo/*"}) public class AuthTokenFilter implements Filter {
使用@WebFilter注解,还能够配合使用@Order注解,@Order注解表示执行过滤顺序,值越小,越先执行,这个Order大小在咱们编程过程当中就像处理HTTP请求的生命周期同样大有用处。固然,若是没有指定Order,则过滤器的调用顺序跟添加的过滤器顺序相反,过滤器的实现是责任链模式。
最后,在启动类上添加@ServletComponentScan 注解便可正常使用自定义过滤器了。
(2)、使用FilterRegistrationBean对Filter进行自定义注册
本文以第二种实现自定义Filter注册:
package com.power.demo.controller.filter; import com.google.common.collect.Lists; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import java.util.List; @Configuration @Component public class RestFilterConfig { @Autowired private AuthTokenFilter filter; @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(filter); //设置(模糊)匹配的url List<String> urlPatterns = Lists.newArrayList(); urlPatterns.add("/api/v1/goods/*"); urlPatterns.add("/api/v1/userinfo/*"); registrationBean.setUrlPatterns(urlPatterns); registrationBean.setOrder(1); registrationBean.setEnabled(true); return registrationBean; } }
请你们特别注意urlPatterns,属性urlPatterns指定要过滤的URL模式。对于Filter的做用区域,这个参数居功至伟。
注册好Filter,当Spring Boot启动时监测到有javax.servlet.Filter的bean时就会自动加入过滤器调用链ApplicationFilterChain。
调用一个API试试效果:
一般状况下,咱们在Spring Boot下都会自定义一个全局统一的异常管理加强GlobalExceptionHandler(和上面这个显示会略有不一样)。
根据个人实践,过滤器里抛出异常,不会被全局惟一的异常管理加强捕获到并进行处理,这个和拦截器Inteceptor以及下一篇文章介绍的自定义AOP拦截不一样。
到这里,一个经过自定义Filter实现的简易安全认证服务就搞定了。
继承接口HandlerInterceptor,实现拦截器,接口方法有下面三个:
preHandle是请求执行前执行
postHandle是请求结束执行
afterCompletion是视图渲染完成后执行
package com.power.demo.controller.interceptor; import com.power.demo.common.AppConst; import com.power.demo.common.BizResult; import com.power.demo.service.contract.AuthTokenService; import com.power.demo.util.PowerLogger; import com.power.demo.util.SerializeUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /* * 认证token拦截器 * */ @Component public class AuthTokenInterceptor implements HandlerInterceptor { @Autowired private AuthTokenService authTokenService; /* * 请求执行前执行 * */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean handleResult = false; String token = request.getHeader(AppConst.AUTH_TOKEN); BizResult<String> bizResult = authTokenService.powerCheck(token); System.out.println(SerializeUtil.Serialize(bizResult)); handleResult = bizResult.getIsOK(); PowerLogger.info("auth token interceptor拦截结果:" + handleResult); if (bizResult.getIsOK() == true) { PowerLogger.info("auth token interceptor passed"); } else { throw new Exception(bizResult.getMessage()); } return handleResult; } /* * 请求结束执行 * */ @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 { } }
示例中,咱们选择在请求执行前进行token安全认证。
认证服务就是过滤器里介绍的AuthTokenService,业务逻辑层实现复用。
定义一个InterceptorConfig类,继承自WebMvcConfigurationSupport,WebMvcConfigurerAdapter已通过时。
将AuthTokenInterceptor做为bean注入,其余设置拦截器拦截的URL和过滤器很是类似:
package com.power.demo.controller.interceptor; import com.google.common.collect.Lists; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import java.util.List; @Configuration @Component public class InterceptorConfig extends WebMvcConfigurationSupport { //WebMvcConfigurerAdapter已通过时 private static final String FAVICON_URL = "/favicon.ico"; /** * 发现若是继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。 * * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/").addResourceLocations("/**"); registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); } /** * 配置servlet处理 */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } @Override public void addInterceptors(InterceptorRegistry registry) { //设置(模糊)匹配的url List<String> urlPatterns = Lists.newArrayList(); urlPatterns.add("/api/v1/goods/*"); urlPatterns.add("/api/v1/userinfo/*"); registry.addInterceptor(authTokenInterceptor()).addPathPatterns(urlPatterns).excludePathPatterns(FAVICON_URL); super.addInterceptors(registry); } //将拦截器做为bean写入配置中 @Bean public AuthTokenInterceptor authTokenInterceptor() { return new AuthTokenInterceptor(); } }
启动应用后,调用接口就能够看到拦截器拦截的效果了。全局统一的异常管理GlobalExceptionHandler捕获异常后处理以下:
和过滤器显示的主要错误提示信息几乎同样,可是堆栈信息更加丰富。
主要区别以下:
一、拦截器主要是基于java的反射机制的,而过滤器是基于函数回调
二、拦截器不依赖于servlet容器,过滤器依赖于servlet容器
三、拦截器只能对action请求起做用,而过滤器则能够对几乎全部的请求起做用
四、拦截器能够访问action上下文、值栈里的对象,而过滤器不能访问
五、在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的话题将在下篇文章分享。