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框架提供的認證和授權有點不一樣,當然,這種“安全”處理方式對專業人士沒什麼新意,但是可以對外擋掉很大一部分小白使用者。
二、自定義過濾器
和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);
}
加了@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;@Componentpublic 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實戰>>的書裡也有這樣寫的先例。
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;@Componentpublic 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;
}
}
用到的快取服務可以參考這裡,這個也是我在前廠的經驗總結。
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@Componentpublic 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實現的簡易安全認證服務就搞定了。
三、自定義攔截器
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攔截器
* */@Componentpublic 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,業務邏輯層實現複用。
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@Componentpublic 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捕獲異常後處理如下:
和過濾器顯示的主要錯誤提示資訊幾乎一樣,但是堆疊資訊更加豐富。
四、過濾器和攔截器區別
主要區別如下:
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。
公眾號推薦:
相關文章
- Spring 過濾器和攔截器Spring過濾器
- spring boot 新增自定義監聽器、過濾器、攔截器Spring Boot過濾器
- 談談 Spring 的過濾器和攔截器Spring過濾器
- SpringBoot中的過濾器和攔截器的實現Spring Boot過濾器
- SpringBoot實現過濾器、攔截器與切片Spring Boot過濾器
- spring中的過濾器與攔截器Spring過濾器
- spring boot 攔截器Spring Boot
- Spring Boot中攔截器的使用Spring Boot
- 攔截過濾器模式過濾器模式
- Spring Boot新增攔截器Spring Boot
- 過濾器 Filter 與 攔截器 Interceptor 的區別過濾器Filter
- springmv的過濾器和攔截器的區別是什麼Spring過濾器
- 過濾器和攔截器有啥區別,這次會了!過濾器
- vue.js新增攔截器,實現token認證(使用axios)Vue.jsiOS
- 攔截器(Interceptor)與過濾器(Filter)過濾器Filter
- 過濾器 和 攔截器 6 個區別,別再傻傻分不清了過濾器
- 過濾器 和 攔截器 6個區別,別再傻傻分不清了過濾器
- SpringBoot 攔截器、過濾器、監聽器Spring Boot過濾器
- 極簡架構模式-攔截過濾器模式架構模式過濾器
- spring mvc攔截器,spring攔截器以及AOP切面的區別和原始碼SpringMVC原始碼
- Spring MVC 中的攔截器的使用“攔截器基本配置” 和 “攔截器高階配置”SpringMVC
- springBoot的過濾器,監聽器,攔截器Spring Boot過濾器
- 聊一聊過濾器與攔截器過濾器
- 監聽器,過濾器,攔截器的執行過程和對比過濾器
- springboot系列文章之過濾器 vs 攔截器Spring Boot過濾器
- Solon 的過濾器 Filter 和兩種攔截器 Handler、 Interceptor過濾器Filter
- Mybatis 分頁:Pagehelper + 攔截器實現MyBatis
- spring攔截器Spring
- spring mvc 攔截器的使用SpringMVC
- Spring 攔截器和過濾器中自動注入為 null 的原因及解決方案Spring過濾器Null
- spring boot 實現監聽器、過濾器、全域性異常處理Spring Boot過濾器
- Spring Security認證器實現Spring
- SSM專案使用攔截器實現登入驗證功能SSM
- Spring Boot專案中如何定製攔截器Spring Boot
- Spring Boot第七彈,別再問我攔截器如何配置了!!!Spring Boot
- Spring Cloud Gateway 實現簡單自定義過濾器SpringCloudGateway過濾器
- 過濾器、攔截器、AOP、ControllerAdvcie執行順序對比過濾器Controller
- Java實現的攔截器Java