Spring Boot使用过滤器和拦截器分别实现REST接口简易安全认证示例代码详解
本文通过一个简易安全认证示例的开发实践,理解过滤器和拦截器的工作原理。
很多文章都将过滤器(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框架提供的认证和授权有点不一样,当然,这种“安全”处理方式对专业人士没什么新意,但是可以对外挡掉很大一部分小白用户。
二、自定义filter
和spring mvc类似,spring boot提供了很多servlet过滤器(filter)可使用,并且它自动添加了一些常用过滤器,比如characterencodingfilter(用于处理编码问题)、hiddenhttpmethodfilter(隐藏http函数)、httpputformcontentfilter(form表单处理)、requestcontextfilter(请求上下文)等。通常我们还会自定义filter实现一些通用功能,比如记录日志、判断是否登录、权限验证等。
1、自定义请求头
很简单,在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); } getgoodsbygoodsid
加了@requestheader修饰的authtoken字段就可以在swagger这样的框架下显示出来。
调用后,可以根据http工具看到请求头,本文示例是authtoken(和某些框架的token区分开):
备注:很多httpclient工具都支持动态传人请求头,比如resttemplate。
2、实现filter
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() { } } authtokenfilter
注意,filter这样的东西,我认为从实际分层角度,多数处理的还是表现层偏多,不建议直接在filter中直接使用数据访问层dao,虽然这样的代码一两年前我在很多老古董项目中看到过很多次,而且<<spring实战>>的书里也有这样写的先例。
3、认证服务
这里就是主要业务逻辑了,示例代码只是简单写下思路,不要轻易就用于生产环境:
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; } } authtokenserviceimpl
用到的缓存服务可以参考这里,这个也是我在前厂的经验总结。
4、注册filter
常见的有两种写法:
(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; } } 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是视图渲染完成后执行
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 { } } authtokeninterceptor
示例中,我们选择在请求执行前进行token安全认证。
认证服务就是过滤器里介绍的authtokenservice,业务逻辑层实现复用。
2、注册拦截器
定义一个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(); } } 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 boot使用过滤器和拦截器分别实现rest接口简易安全认证,希望对大家有所帮助