本文經過一個簡易安全認證示例的開發實踐,理解過濾器和攔截器的工做原理。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的話題將在下篇文章分享。