一、爲什麼需要攔截器?
在前後端分離的現在,項目中的所有的前端的頁面都需要通過調用後臺的Api進行獲取數據
接口的功能點不同,就會有很多種情況,比如說
- 涉及敏感數據(登錄,獲取個人信息,個人金額修改)相關的接口需要token驗證
- 獲取不敏感數據則不需要進行校驗
- vue等前端調用後臺api,如果沒有引入(nginx),則有可能有跨域問題
所以說需要一個攔截器去區分哪些路徑下需要token校驗,那些不需要。
二、實現思路
- 新增一個配置類繼承WebMvcConfigurer
- 在這個配置類中新增自定義邏輯的攔截器(實現HandlerInterceptor接口),同時設定哪些路徑需要調用自定義攔截器
三、具體代碼實現(示例代碼:https://github.com/zz790609619/LeetCodeRecord.git)
配置類
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;
import java.util.List;
/**
* 攔截器(可做跨域,token驗證等)
*/
@Configuration
public class WebRequestInterceptor implements WebMvcConfigurer {
/**
* 自定義攔截器()
* @param registry
*/
public void addInterceptors(InterceptorRegistry registry) {
// RequestInterceptor爲具體攔截邏輯的執行類 實現了HandlerInterceptor接口
// addPathPatterns("/test/**") 意義是訪問路徑下/test 下所有的訪問路徑都需要被RequestInterceptor攔截
// excludePathPatterns 這個訪問路徑/test/exception則不在被RequestInterceptor攔截的範圍
// /user/** user下所有路徑都包含在內 例:/user/api 、/user/api/zz
// /user/* 只有user下一層路徑包含在內 例:/user/api(包含) 、/user/api/zz(不包含)
// /test/queryUser接口則是token驗證後,把token爲xx的玩家信息放入Request中,方便接口拿取
registry.addInterceptor(new RequestInterceptor())
.addPathPatterns("/test/**")
.addPathPatterns("/test/queryUser")
.excludePathPatterns("/test/exception");
}
/**
* 跨域支持 比如說vue 的axios訪問
* @param registry
*/
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600 * 24);
}
/**
* 修改訪問路徑
* @param configurer
*/
public void configurePathMatch(PathMatchConfigurer configurer) {
// 設置爲true後,訪問路徑後加/ 也能正常訪問 /user == /user/
// configurer.setUseTrailingSlashMatch(true);
}
/**
* 內容協商機制,主要是方便一個請求路徑返回多個數據格式
* @param configurer
*/
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
/**
* 處理異步請求的。只能設置兩個值,一個超時時間(毫秒,Tomcat下默認是10000毫秒,即10秒),還有一個是AsyncTaskExecutor,異步任務執行器
* @param configurer
*/
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// //設置超時時間
// configurer.setDefaultTimeout(1000000);
// //設置異步任務執行器
// configurer.setTaskExecutor(new AsyncTaskExecutor() {
// @Override
// public void execute(Runnable runnable, long l) {
//
// }
//
// @Override
// public Future<?> submit(Runnable runnable) {
// return null;
// }
//
// @Override
// public <T> Future<T> submit(Callable<T> callable) {
// return null;
// }
//
// @Override
// public void execute(Runnable runnable) {
//
// }
// });
}
/**
* 這個接口可以實現靜態文件可以像Servlet一樣被訪問。
* @param configurer
*/
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
/**
* 增加轉化器或者格式化器。這邊不僅可以把時間轉化成你需要時區或者樣式。還可以自定義轉化器和你數據庫做交互,比如傳進來userId,經過轉化可以拿到user對象
* @param registry
*/
public void addFormatters(FormatterRegistry registry) {
}
/**
* 添加靜態資源--過濾swagger-api (開源的在線API文檔)
* @param registry
*/
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
public void addViewControllers(ViewControllerRegistry registry) {
}
public void configureViewResolvers(ViewResolverRegistry registry) {
}
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
/**
* 配置消息轉換器
* @param converters
*/
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
@Nullable
public Validator getValidator() {
return null;
}
@Nullable
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
自定義的攔截器
package com.example.demo.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.entity.model.ResponseDto;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Enumeration;
public class RequestInterceptor implements HandlerInterceptor {
/**
* 預處理回調方法,實現處理器的預處理(如檢查登陸),第三個參數爲響應的處理器,自定義Controller
* 返回值:
* true表示繼續流程(如調用下一個攔截器或處理器);
* false表示流程中斷(如登錄檢查失敗),不會繼續調用其他的攔截器或處理器,此時我們需要通過response來產生響應;
*/
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//將頭部信息都轉換成map
JSONObject map = new JSONObject();
Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = httpServletRequest.getHeader(key);
map.put(key, value);
}
map.put("token", httpServletRequest.getHeader("AUTH-TOKEN"));
//判斷從前端傳來的頭部信息中AUTH-TOKEN的值是否與我們後臺定義的token值一致
if("111".equals(map.get("token"))){
//token正確 繼續下一步攔截器(如果有)
System.out.println("token is right");
//從Spring上下文中拿到UserMapper
UserMapper userMapper= ApplicationContextUtil.getBean(UserMapper.class);
//獲取該token對應的用戶信息
User user=userMapper.getUserByToken(String.valueOf(map.get("token")));
//將用戶信息放入Request中
httpServletRequest.setAttribute("test",JSON.toJSONString(user));
return true;
}else{
//token錯誤 返回錯誤response
System.out.println("token is error");
PrintWriter writer = null;
try {
ResponseDto dto=new ResponseDto();
dto.setErrorCode(1002);
dto.setMessage("RequestInterceptor");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setHeader("Content-Type","application/json");
writer = httpServletResponse.getWriter();
//將返回的錯誤提示壓入流中
writer.write(JSON.toJSONString(dto));
writer.flush();
} catch (Exception e) {
} finally {
if (null != writer) {
writer.close();
}
return false;
}
}
}
/**
* 後處理回調方法,實現處理器的後處理(但在渲染視圖之前),此時我們可以通過modelAndView(模型和視圖對象)對模型數據進行處理或對視圖進行處理,modelAndView也可能爲null。
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 整個請求處理完畢回調方法,即在視圖渲染完畢時回調,如性能監控中我們可以在此記錄結束時間並輸出消耗時間,還可以進行一些資源清理,類似於try-catch-finally中的finally,但僅調用處理器執行鏈中
*/
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
接口Controller
package com.example.demo.controller;
import com.aliyun.openservices.shade.com.alibaba.fastjson.JSON;
import com.example.demo.entity.model.ResponseDto;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping(value = "/test")
public class UserNodeController {
@RequestMapping(value = "/index")
public String index(){
return "/index";
}
/**
* 異常報錯
* 訪問路徑:http://127.0.0.1:8090/test/exception
* 返回結果: {"errorCode": 1001,"message": "Exception:4","data": null }
*/
@RequestMapping(value = "/exception")
public String getException(){
int[] arr = {1, 2, 3};
System.out.println(arr[4]);
ResponseDto dto=new ResponseDto();
dto.setMessage("NoEnterGlobalException");
return JSON.toJSONString(dto);
}
/**
*
* 空指針異常報錯
* 訪問路徑:http://127.0.0.1:8090/test/nullPointException
* 返回結果:{"errorCode": 1002,"message": "NullPointerException:null","data": null }
*/
@RequestMapping(value = "/nullPointException")
public ResponseDto getNullPointException(){
Object obj = null;
obj.toString();
ResponseDto dto=new ResponseDto();
dto.setMessage("NoEnterGlobalNullPointException");
return dto;
}
/**
* 獲取全局變量
* 訪問路徑:http://127.0.0.1:8090/test/getGlobalParm
* 返回結果:{"ww":{"ww":"helloQ"}}
*/
@RequestMapping(value = "/getGlobalParm")
public String getGlobalParm(Model model){
Map<String, Object> map = model.asMap();
return JSON.toJSONString(map);
}
/**
* 獲取預處理後的數據
* 訪問地址及參數:http://127.0.0.1:8090/test/getPreprocessedData?helloA.errorCode=1&helloA.message=a&helloA.data=a&helloB.errorCode=2&helloB.message=b&helloB.data=b
* 返回結果:dtoA:{"data":"a","errorCode":1,"message":"a"},dtoB:{"data":"b","errorCode":2,"message":"b"}
*/
@RequestMapping(value = "/getPreprocessedData")
public String getPreprocessedData(@ModelAttribute("helloA")ResponseDto dtoA,@ModelAttribute("helloB")ResponseDto dtoB){
return "dtoA:"+JSON.toJSONString(dtoA)+",dtoB:"+JSON.toJSONString(dtoB);
}
@GetMapping("/queryUser")
public void queryUser(HttpServletRequest request,@RequestParam("token") String token){
//在攔截器中驗證token並獲取到這個token對應的信息
System.out.println(request.getAttribute("test"));
System.out.println(token);
}
}
全局異常處理類
package com.example.demo.config;
import com.example.demo.entity.model.ResponseDto;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalControllerAdvice {
/**
* 所有異常處理
* @param request
* @param e
* @return
* @throws Exception
*/
@ExceptionHandler(value = Exception.class)
public @ResponseBody
ResponseDto exceptionHandler(HttpServletRequest request,Exception e)throws Exception{
ResponseDto dto=new ResponseDto();
dto.setErrorCode(1001);
dto.setMessage("Exception:"+e.getMessage());
return dto;
}
/**
* 空指針異常處理
* @param request
* @param e
* @return
* @throws NullPointerException
*/
@ExceptionHandler(value = NullPointerException.class)
public @ResponseBody
ResponseDto exceptionHandler(HttpServletRequest request,NullPointerException e)throws NullPointerException{
ResponseDto dto=new ResponseDto();
dto.setErrorCode(1002);
dto.setMessage("NullPointerException:"+e.getMessage());
return dto;
}
/**
* 全局變量 攔截器
* @return
*/
@ModelAttribute(name="ww")
public Map<String,Object> globalData() {
HashMap<String, Object> map = new HashMap<>();
map.put("ww", "helloQ");
return map;
}
/**
* 數據預處理
* @param binder
*/
@InitBinder("helloA")
public void b(WebDataBinder binder) {
binder.setFieldDefaultPrefix("helloA.");
}
@InitBinder("helloB")
public void a(WebDataBinder binder) {
binder.setFieldDefaultPrefix("helloB.");
}
}
ApplicationContextUtil(在項目初始化的時候將Spring上下文放入ApplicationContextUtil中,方便後面定時器/攔截器等獲取實體類bean)
import org.springframework.context.ApplicationContext;
public class ApplicationContextUtil{
private static ApplicationContext applicationContext;
//在項目初始化的時候將SpringApplication.run(DemoApplication.class, args)set進去
public static ApplicationContext setApplicationContext(ApplicationContext context){
this.applicationContext=context;
}
public static object getBean(String beanId){
return applicationContext.getBean(beanId);
}
public static <T>T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}
四、測試結果
如配置類中設置的除了/test/exception ,其他的/test下的路徑都應該被我們自定義攔截器攔截
-
訪問路徑:http://127.0.0.1:8090/test/exception
結果: -
訪問路徑:http://127.0.0.1:8090/test/nullPointException
輸入正確的token的結果:
輸入錯誤token值的結果:
-
http://127.0.0.1:8090/test/queryUser?token=111 驗證用戶信息能否能在接口中獲取
控制檯如圖:
數據庫如圖: